feat: 现代化翻页组件 + 完整项目更新

- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航
- PickModal 和 SwapModal 集成分页功能,每页20项
- 搜索/筛选切换自动回到第1页
- 现代化CSS样式:hover浮起动效、主色高亮、移动端适配
- 新增 dashboard、admin 管理模板
- 菜系(cuisine)模型和迁移
- 构建脚本 build.py 支持 PyInstaller 打包
- 前端资源重新构建
This commit is contained in:
Chunyu
2026-08-28 13:44:21 +08:00
parent 65b52b052f
commit d7553520ce
51 changed files with 4318 additions and 441 deletions
+282
View File
@@ -0,0 +1,282 @@
"""
Dashboard Manager - School Meal Planner
A lightweight control panel / visualization entry point.
When run standalone, starts the server and opens the dashboard.
When run alongside the EXE (run.py), it is NOT auto-launched.
"""
import os
import sys
import threading
import time
import urllib.request
import webbrowser
try:
import tkinter as tk
from tkinter import messagebox
HAS_TKINTER = True
except ImportError:
HAS_TKINTER = False
# ---------------------------------------------------------------------------
# Environment setup (shared with run.py)
# ---------------------------------------------------------------------------
def _frozen_dir():
if getattr(sys, "frozen", False):
return sys._MEIPASS
return os.path.dirname(os.path.abspath(__file__))
def _app_dir():
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
def setup_environment():
base = _frozen_dir()
app = _app_dir()
sys.path.insert(0, base)
backend_dir = os.path.join(base, 'backend')
if os.path.isdir(backend_dir) and backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
os.environ["DJANGO_DEBUG"] = "True"
os.environ["DJANGO_SECRET_KEY"] = "exe-local-secret-key-do-not-use-in-production"
os.environ["DJANGO_ALLOWED_HOSTS"] = "127.0.0.1,localhost"
os.environ["DJANGO_DB_PATH"] = os.path.join(app, "db.sqlite3")
os.environ["DJANGO_STATIC_URL"] = "/static/"
os.environ["EXE_MODE"] = "1"
static_fe = os.path.join(base, "backend", "static", "frontend")
if not os.path.isdir(static_fe):
static_fe = os.path.join(base, "static", "frontend")
os.environ["FRONTEND_DIR"] = static_fe
def init_database():
import django
django.setup()
from django.core.management import call_command
call_command("migrate", "--run-syncdb", verbosity=0)
from meals.models import Dish
if Dish.objects.count() == 0:
try:
call_command("seed_dishes", verbosity=0)
except Exception:
pass
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username="admin").exists():
User.objects.create_superuser("admin", "admin@example.com", "admin123")
def start_server(port):
import django
django.setup()
from django.core.management import call_command
call_command("runserver", f"127.0.0.1:{port}", "--noreload", verbosity=0)
def _server_alive(port=8000):
"""Check if Django is already running on the port."""
try:
urllib.request.urlopen(f"http://127.0.0.1:{port}/admin/", timeout=2)
return True
except Exception:
return False
# ---------------------------------------------------------------------------
# GUI - Visualization Panel
# ---------------------------------------------------------------------------
BG = "#1e293b"
BG_CARD = "#334155"
ACCENT = "#38bdf8"
GREEN = "#4ade80"
RED = "#f87171"
FG_DIM = "#94a3b8"
FONT_TITLE = ("Microsoft YaHei UI", 16, "bold")
FONT_LABEL = ("Microsoft YaHei UI", 11)
FONT_BTN = ("Microsoft YaHei UI", 11, "bold")
FONT_STATUS = ("Microsoft YaHei UI", 10)
PORT = 8000
URL_DASHBOARD = f"http://127.0.0.1:{PORT}/dashboard/"
URL_FRONTEND = f"http://127.0.0.1:{PORT}"
URL_ADMIN = f"http://127.0.0.1:{PORT}/admin"
ADMIN_USER = "admin"
ADMIN_PASS = "admin123"
class ManagerApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("\u5b66\u6821\u8ba2\u9910\u83dc\u5355\u751f\u6210\u5668 - \u53ef\u89c6\u5316\u7ba1\u7406\u9762\u677f")
self.root.geometry("420x480")
self.root.resizable(False, False)
self.root.configure(bg=BG)
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
self._server_thread = None
self._build_ui()
self._check_server()
def _build_ui(self):
root = self.root
# Header
header = tk.Frame(root, bg=ACCENT, height=56)
header.pack(fill="x")
header.pack_propagate(False)
tk.Label(
header, text="\u53ef\u89c6\u5316\u7ba1\u7406\u9762\u677f",
font=FONT_TITLE, bg=ACCENT, fg="#0f172a",
).pack(expand=True)
# Status bar
self._status_frame = tk.Frame(root, bg=BG_CARD, bd=1, relief="solid")
self._status_frame.pack(fill="x", padx=16, pady=(16, 8))
tk.Label(
self._status_frame, text="\u670d\u52a1\u72b6\u6001",
font=FONT_LABEL, bg=BG_CARD, fg=FG_DIM, anchor="w",
).pack(side="left", padx=(12, 8), pady=10)
self._status_dot = tk.Canvas(
self._status_frame, width=12, height=12,
bg=BG_CARD, highlightthickness=0,
)
self._status_dot.pack(side="left", pady=10)
self._status_dot.create_oval(2, 2, 10, 10, fill=FG_DIM, outline="", tags="dot")
self._status_label = tk.Label(
self._status_frame, text="\u68c0\u6d4b\u4e2d...",
font=FONT_STATUS, bg=BG_CARD, fg=FG_DIM,
)
self._status_label.pack(side="left", padx=(4, 12), pady=10)
# Credentials
cred_frame = tk.Frame(root, bg=BG_CARD, bd=1, relief="solid")
cred_frame.pack(fill="x", padx=16, pady=4)
tk.Label(cred_frame, text="\u7ba1\u7406\u5458\u4fe1\u606f",
font=FONT_LABEL, bg=BG_CARD, fg=FG_DIM, anchor="w").pack(side="left", padx=(12, 8), pady=10)
tk.Label(cred_frame, text=f"{ADMIN_USER} / {ADMIN_PASS}",
font=FONT_BTN, bg=BG_CARD, fg=GREEN).pack(side="right", padx=(0, 12), pady=10)
# URL
url_frame = tk.Frame(root, bg=BG_CARD, bd=1, relief="solid")
url_frame.pack(fill="x", padx=16, pady=4)
tk.Label(url_frame, text="\u672c\u5730\u670d\u52a1",
font=FONT_LABEL, bg=BG_CARD, fg=FG_DIM, anchor="w").pack(side="left", padx=(12, 8), pady=10)
tk.Label(url_frame, text=f"127.0.0.1:{PORT}",
font=FONT_BTN, bg=BG_CARD, fg=ACCENT).pack(side="right", padx=(0, 12), pady=10)
# Action buttons
btn_frame = tk.Frame(root, bg=BG)
btn_frame.pack(fill="x", padx=16, pady=(16, 4))
self._make_button(btn_frame, "\U0001f4ca \u6253\u5f00\u53ef\u89c6\u5316\u9762\u677f",
lambda: self._open_url(URL_DASHBOARD)).pack(fill="x", pady=(0, 6))
self._make_button(btn_frame, "\U0001f310 \u6253\u5f00\u83dc\u5355\u751f\u6210\u5668",
lambda: self._open_url(URL_FRONTEND)).pack(fill="x", pady=(0, 6))
self._make_button(btn_frame, "\U0001f6e0\ufe0f \u6253\u5f00\u540e\u53f0\u7ba1\u7406",
lambda: self._open_url(URL_ADMIN)).pack(fill="x", pady=(0, 6))
self._make_button(btn_frame, "\U0001f37d\ufe0f \u83dc\u54c1\u7ba1\u7406",
lambda: self._open_url(f"{URL_ADMIN}/meals/dish/")).pack(fill="x", pady=(0, 6))
# Bottom
bottom_frame = tk.Frame(root, bg=BG)
bottom_frame.pack(fill="x", padx=16, pady=(12, 16))
self._make_button(bottom_frame, "\u505c\u6b62\u670d\u52a1",
self._stop_server, bg_color=RED, hover_color="#dc2626").pack(fill="x", pady=(0, 4))
self._make_button(bottom_frame, "\u91cd\u542f\u670d\u52a1",
self._restart_server, bg_color="#f59e0b", hover_color="#d97706").pack(fill="x")
def _make_button(self, parent, text, command, bg_color=ACCENT, hover_color=None):
if hover_color is None:
hover_color = bg_color
btn = tk.Label(parent, text=text, font=FONT_BTN, bg=bg_color, fg="#0f172a",
cursor="hand2", padx=12, pady=10, anchor="w")
btn.bind("<Button-1>", lambda e: command())
btn.bind("<Enter>", lambda e: btn.configure(bg=hover_color))
btn.bind("<Leave>", lambda e: btn.configure(bg=bg_color))
return btn
def _check_server(self):
"""Detect if server is already running, or start it."""
self._set_status("\u68c0\u6d4b\u4e2d...", FG_DIM)
def _worker():
if _server_alive(PORT):
self.root.after(0, lambda: self._set_status("Running", GREEN))
self.root.after(500, lambda: self._open_url(URL_DASHBOARD))
else:
# Start server ourselves
self.root.after(0, lambda: self._set_status("\u542f\u52a8\u670d\u52a1...", FG_DIM))
try:
setup_environment()
init_database()
self.root.after(0, lambda: self._set_status("Running", GREEN))
self.root.after(1000, lambda: self._open_url(URL_DASHBOARD))
start_server(PORT)
except Exception as ex:
self.root.after(0, lambda: self._set_status("\u542f\u52a8\u5931\u8d25", RED))
self.root.after(0, lambda: messagebox.showerror("\u9519\u8bef", str(ex)))
self._server_thread = threading.Thread(target=_worker, daemon=True)
self._server_thread.start()
def _stop_server(self):
messagebox.showinfo("\u63d0\u793a",
"\u670d\u52a1\u7531\u4e3b\u7a0b\u5e8f\u7ba1\u7406\u3002\u5173\u95ed\u672c\u7a97\u53e3\u5373\u53ef\u505c\u6b62\u670d\u52a1\u3002")
def _restart_server(self):
self._set_status("\u91cd\u542f\u4e2d...", FG_DIM)
def _worker():
time.sleep(1)
if _server_alive(PORT):
self.root.after(0, lambda: self._set_status("Running", GREEN))
else:
self.root.after(0, lambda: self._set_status("\u672a\u8fde\u63a5", RED))
threading.Thread(target=_worker, daemon=True).start()
def _open_url(self, url):
webbrowser.open(url)
def _set_status(self, text, color):
self._status_label.configure(text=text, fg=color)
self._status_dot.itemconfig("dot", fill=color)
def _on_close(self):
self.root.destroy()
def run(self):
self.root.mainloop()
def main():
setup_environment()
init_database()
if HAS_TKINTER:
app = ManagerApp()
app.run()
else:
print()
print("=" * 56)
print(" \u5b66\u6821\u8ba2\u9910\u83dc\u5355\u751f\u6210\u5668 - \u53ef\u89c6\u5316\u7ba1\u7406\u9762\u677f")
print("=" * 56)
print(f" \u53ef\u89c6\u5316\u9762\u677f: {URL_DASHBOARD}")
print(f" \u83dc\u5355\u751f\u6210\u5668: {URL_FRONTEND}")
print(f" \u540e\u53f0\u7ba1\u7406: {URL_ADMIN}")
print(f" \u7ba1\u7406\u5458\u8d26\u53f7: {ADMIN_USER} / {ADMIN_PASS}")
print("-" * 56)
if _server_alive(PORT):
print(" \u670d\u52a1\u5df2\u8fd0\u884c\uff0c\u6253\u5f00\u6d4f\u89c8\u5668...")
threading.Thread(target=lambda: (time.sleep(1), webbrowser.open(URL_DASHBOARD)), daemon=True).start()
else:
print(" \u670d\u52a1\u672a\u8fd0\u884c\uff0c\u6b63\u5728\u542f\u52a8...")
threading.Thread(target=lambda: (time.sleep(3), webbrowser.open(URL_DASHBOARD)), daemon=True).start()
try:
start_server(PORT)
except KeyboardInterrupt:
print("\n\u670d\u52a1\u5df2\u505c\u6b62")
if __name__ == "__main__":
main()