feat: 现代化翻页组件 + 完整项目更新
- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航 - PickModal 和 SwapModal 集成分页功能,每页20项 - 搜索/筛选切换自动回到第1页 - 现代化CSS样式:hover浮起动效、主色高亮、移动端适配 - 新增 dashboard、admin 管理模板 - 菜系(cuisine)模型和迁移 - 构建脚本 build.py 支持 PyInstaller 打包 - 前端资源重新构建
This commit is contained in:
+282
@@ -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()
|
||||
Reference in New Issue
Block a user