Complete bootstrap base frontend prototype.
- login page and playlist setting page frontend templates. - fast API route. - reading and saving config files - mock login logic for UI interaction demo
This commit is contained in:
parent
662bc11821
commit
e9fcaf508e
3
app/config.json
Normal file
3
app/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"theme": "light"
|
||||
}
|
68
app/main.py
Normal file
68
app/main.py
Normal file
@ -0,0 +1,68 @@
|
||||
import os
|
||||
from app.utils.config import load_config, save_config
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
app = FastAPI()
|
||||
templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
|
||||
|
||||
# mount static files
|
||||
# 这里的路径是相对于 main.py 文件所在的目录
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request):
|
||||
config = load_config()
|
||||
theme = config.get("theme", "auto")
|
||||
return templates.TemplateResponse("login.html", {"request": request, "theme": theme, "path": "/login"})
|
||||
|
||||
@app.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
config = load_config()
|
||||
theme = config.get("theme", "auto")
|
||||
return templates.TemplateResponse("login.html", {"request": request, "theme": theme, "path": "/login"})
|
||||
|
||||
@app.post("/login", response_class=HTMLResponse)
|
||||
async def login(
|
||||
request: Request,
|
||||
user: str = Form(...),
|
||||
pw: str = Form(...),
|
||||
url: str = Form(...),
|
||||
port: str = Form(...),
|
||||
library: str = Form(...)
|
||||
):
|
||||
config = load_config()
|
||||
theme = config.get("theme", "auto")
|
||||
# demo:假装连接成功
|
||||
if user == "admin":
|
||||
return templates.TemplateResponse("login.html", {"request": request, "message": "连接成功", "success": True, "theme": theme, "path": "/login"})
|
||||
else:
|
||||
return templates.TemplateResponse("login.html", {"request": request, "message": "连接失败:用户名错误", "success": False, "theme": theme, "path": "/login"})
|
||||
|
||||
@app.get("/playlist", response_class=HTMLResponse)
|
||||
async def get_playlist(request: Request):
|
||||
config = load_config()
|
||||
theme = config.get("theme", "auto")
|
||||
return templates.TemplateResponse("playlist.html", {"request": request, "theme": theme, "path": "/playlist"})
|
||||
|
||||
@app.post("/playlist", response_class=HTMLResponse)
|
||||
async def set_playlist(request: Request, address: str = Form(...), interval: str = Form(...)):
|
||||
config = load_config()
|
||||
theme = config.get("theme", "auto")
|
||||
# demo:返回提交的设置
|
||||
return templates.TemplateResponse("playlist.html", {
|
||||
"request": request,
|
||||
"message": f"设置成功:地址 {address},间隔 {interval} 分钟",
|
||||
"theme": theme,
|
||||
"path": "/playlist"
|
||||
})
|
||||
|
||||
@app.post("/set-theme")
|
||||
async def set_theme(theme: str = Form(...)):
|
||||
config = load_config()
|
||||
config["theme"] = theme
|
||||
save_config(config)
|
||||
return RedirectResponse("/login", status_code=303)
|
3
app/static/styles.css
Normal file
3
app/static/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
.custom-sidebar {
|
||||
max-width: 300px !important; /* 设置最大宽度为 300px */
|
||||
}
|
112
app/templates/base.html
Normal file
112
app/templates/base.html
Normal file
@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="{{ theme | default('auto') }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Plex Sync{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='styles.css') }}">
|
||||
|
||||
<!-- ✅ 图标支持(用到 svg 图标) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-auto col-md-3 bg-body-tertiary sidebar border-end vh-100 custom-sidebar">
|
||||
<div id="sidebar" class="collapse collapse-horizontal show">
|
||||
<!-- theme button -->
|
||||
<div class="position-fixed bottom-0 end-0 m-4 z-3">
|
||||
<div class="dropdown border rounded shadow" style="background-color: var(--bs-body-bg); border-color: var(--bs-border-color);">
|
||||
<button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" id="bd-theme"
|
||||
type="button" aria-expanded="false" data-bs-toggle="dropdown"
|
||||
aria-label="Toggle theme ({{ theme }})">
|
||||
<span class="me-2">
|
||||
{% if theme == 'light' %}
|
||||
🌞
|
||||
{% elif theme == 'dark' %}
|
||||
🌙
|
||||
{% else %}
|
||||
🌓
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme">
|
||||
<li>
|
||||
<form method="post" action="/set-theme">
|
||||
<button type="submit" name="theme" value="light"
|
||||
class="dropdown-item d-flex align-items-center">
|
||||
🌞 Light
|
||||
</button>
|
||||
<button type="submit" name="theme" value="dark"
|
||||
class="dropdown-item d-flex align-items-center">
|
||||
🌙 Dark
|
||||
</button>
|
||||
<button type="submit" name="theme" value="auto"
|
||||
class="dropdown-item d-flex align-items-center">
|
||||
🌓 Auto
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- sidebar contents -->
|
||||
<div class="p-3">
|
||||
<a href="/" class="d-flex align-items-center justify-content-center nav-link text-decoration-none">
|
||||
<!-- logo file-music-fill 🎵 -->
|
||||
<i class="bi bi-file-music-fill"></i>
|
||||
<!-- title -->
|
||||
<span class="d-none d-md-inline ms-2">Plex Sync</span>
|
||||
</a>
|
||||
<hr class="d-none d-md-block">
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="/login" class="nav-link {% if path == '/login' %}active{% endif %}">
|
||||
<!-- login icon -->
|
||||
<i class="bi bi-person-fill-lock"></i>
|
||||
<span class="d-none d-md-inline ms-2">登录信息</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/playlist" class="nav-link {% if path == '/playlist' %}active{% endif %}">
|
||||
<!-- playlist icon -->
|
||||
<i class="bi bi-music-note-list"></i>
|
||||
<span class="d-none d-md-inline ms-2">播放列表设置</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- main contents -->
|
||||
<main class="col ms-sm-auto px-md-4 py-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS for Bootstrap dropdown, collapse, etc -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// check system theme and apply it
|
||||
function applyAutoTheme() {
|
||||
// default as dark theme
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-bs-theme', systemTheme);
|
||||
}
|
||||
|
||||
// auto set theme
|
||||
if ("{{ theme }}" === "auto") {
|
||||
applyAutoTheme();
|
||||
// listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyAutoTheme);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
37
app/templates/login.html
Normal file
37
app/templates/login.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}登录信息{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>🔐 登录信息</h2>
|
||||
<form method="post" action="/login">
|
||||
<div class="mb-3">
|
||||
<label for="user" class="form-label">用户名</label>
|
||||
<input type="text" class="form-control" id="user" name="user">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pw" class="form-label">密码</label>
|
||||
<input type="password" class="form-control" id="pw" name="pw">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">服务器地址</label>
|
||||
<input type="text" class="form-control" id="url" name="url" placeholder="127.0.0.1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">端口</label>
|
||||
<input type="text" class="form-control" id="port" name="port" placeholder="32400">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="library" class="form-label">选择库</label>
|
||||
<select class="form-select" id="library" name="library">
|
||||
<option>Music</option>
|
||||
<option>Podcast</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">连接</button>
|
||||
</form>
|
||||
|
||||
{% if message %}
|
||||
<div class="alert alert-{{ 'success' if success else 'danger' }} mt-4">{{ message }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
26
app/templates/playlist.html
Normal file
26
app/templates/playlist.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}播放列表设置{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>📂 播放列表设置</h2>
|
||||
<form method="post" action="/playlist">
|
||||
<div class="mb-3">
|
||||
<label for="address" class="form-label">播放列表地址</label>
|
||||
<input type="text" class="form-control" id="address" name="address">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="interval" class="form-label">同步时间间隔</label>
|
||||
<select class="form-select" id="interval" name="interval">
|
||||
<option value="5">每 5 分钟</option>
|
||||
<option value="10">每 10 分钟</option>
|
||||
<option value="30">每 30 分钟</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">保存设置</button>
|
||||
</form>
|
||||
|
||||
{% if message %}
|
||||
<div class="alert alert-info mt-4">{{ message }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
14
app/utils/config.py
Normal file
14
app/utils/config.py
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config.json")
|
||||
|
||||
def load_config():
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
return {}
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_config(new_config):
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(new_config, f, indent=4, ensure_ascii=False)
|
Loading…
Reference in New Issue
Block a user