- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航 - PickModal 和 SwapModal 集成分页功能,每页20项 - 搜索/筛选切换自动回到第1页 - 现代化CSS样式:hover浮起动效、主色高亮、移动端适配 - 新增 dashboard、admin 管理模板 - 菜系(cuisine)模型和迁移 - 构建脚本 build.py 支持 PyInstaller 打包 - 前端资源重新构建
283 lines
11 KiB
Python
283 lines
11 KiB
Python
"""
|
|
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()
|