refactor: remove duplicated route and UI code
This commit is contained in:
@@ -8,6 +8,9 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
# Frontend dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Runtime logs
|
# Runtime logs
|
||||||
*.log
|
*.log
|
||||||
server.out.log
|
server.out.log
|
||||||
|
|||||||
+73
-51
@@ -31,6 +31,21 @@ REGISTRATION_SETTING_KEY = "allow_registration"
|
|||||||
RECORDS_PER_PAGE = 10
|
RECORDS_PER_PAGE = 10
|
||||||
|
|
||||||
|
|
||||||
|
def render_auth_template(mode: str, status_code: int = 200, captcha: str = ""):
|
||||||
|
return render_template("login.html", mode=mode, captcha=captcha), status_code
|
||||||
|
|
||||||
|
|
||||||
|
def render_login_error(message: str, status_code: int = 400):
|
||||||
|
flash(message, "error")
|
||||||
|
session["captcha"] = new_captcha()
|
||||||
|
return render_auth_template("login", status_code, session["captcha"])
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin() -> None:
|
||||||
|
if not current_user.is_admin:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
def registration_allowed() -> bool:
|
def registration_allowed() -> bool:
|
||||||
return AppSetting.get_bool(
|
return AppSetting.get_bool(
|
||||||
REGISTRATION_SETTING_KEY,
|
REGISTRATION_SETTING_KEY,
|
||||||
@@ -45,6 +60,37 @@ def requested_page() -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def paginated_uploads(query, endpoint: str):
|
||||||
|
page = requested_page()
|
||||||
|
pagination = (
|
||||||
|
query.order_by(UploadRecord.upload_time.desc())
|
||||||
|
.paginate(page=page, per_page=RECORDS_PER_PAGE, error_out=False)
|
||||||
|
)
|
||||||
|
if pagination.pages and page > pagination.pages:
|
||||||
|
return pagination, redirect(url_for(endpoint, page=pagination.pages))
|
||||||
|
return pagination, None
|
||||||
|
|
||||||
|
|
||||||
|
def prediction_result_payload(artifacts, record: UploadRecord) -> dict:
|
||||||
|
image_url = url_for("static", filename=f"images/{artifacts.image_filename}")
|
||||||
|
importance_url = (
|
||||||
|
url_for("static", filename=f"images/{artifacts.importance_filename}")
|
||||||
|
if artifacts.importance_filename
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"original_filename": artifacts.original_filename,
|
||||||
|
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"image_url": image_url,
|
||||||
|
"importance_url": importance_url,
|
||||||
|
"excel_url": url_for("main.download_file", record_id=record.id, file_type="prediction"),
|
||||||
|
"result_url": url_for("main.result_page"),
|
||||||
|
"sample_count": int(artifacts.sample_count),
|
||||||
|
"summary_rows": artifacts.summary_rows[:6],
|
||||||
|
"analysis_text": artifacts.analysis_text,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def index():
|
def index():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
@@ -59,22 +105,18 @@ def login():
|
|||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
session["captcha"] = new_captcha()
|
session["captcha"] = new_captcha()
|
||||||
return render_template("login.html", mode="login", captcha=session["captcha"])
|
return render_auth_template("login", captcha=session["captcha"])
|
||||||
|
|
||||||
username = request.form.get("username", "").strip()
|
username = request.form.get("username", "").strip()
|
||||||
password = request.form.get("password", "")
|
password = request.form.get("password", "")
|
||||||
captcha_input = request.form.get("captcha", "").strip().upper()
|
captcha_input = request.form.get("captcha", "").strip().upper()
|
||||||
|
|
||||||
if captcha_input != session.get("captcha", ""):
|
if captcha_input != session.get("captcha", ""):
|
||||||
flash("验证码错误", "error")
|
return render_login_error("验证码错误")
|
||||||
session["captcha"] = new_captcha()
|
|
||||||
return render_template("login.html", mode="login", captcha=session["captcha"]), 400
|
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
if not user or not user.check_password(password):
|
if not user or not user.check_password(password):
|
||||||
flash("用户名或密码错误", "error")
|
return render_login_error("用户名或密码错误")
|
||||||
session["captcha"] = new_captcha()
|
|
||||||
return render_template("login.html", mode="login", captcha=session["captcha"]), 400
|
|
||||||
|
|
||||||
login_user(user, remember=bool(request.form.get("remember")))
|
login_user(user, remember=bool(request.form.get("remember")))
|
||||||
return redirect(url_for("main.home"))
|
return redirect(url_for("main.home"))
|
||||||
@@ -83,24 +125,24 @@ def login():
|
|||||||
@bp.route("/register", methods=["GET", "POST"])
|
@bp.route("/register", methods=["GET", "POST"])
|
||||||
def register():
|
def register():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template("login.html", mode="register", captcha="")
|
return render_auth_template("register")
|
||||||
|
|
||||||
if not registration_allowed():
|
if not registration_allowed():
|
||||||
flash("当前未开放自助注册,请联系管理员。", "error")
|
flash("当前未开放自助注册,请联系管理员。", "error")
|
||||||
return render_template("login.html", mode="register", captcha=""), 403
|
return render_auth_template("register", 403)
|
||||||
|
|
||||||
username = request.form.get("username", "").strip()
|
username = request.form.get("username", "").strip()
|
||||||
password = request.form.get("password", "")
|
password = request.form.get("password", "")
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
flash("用户名不能为空", "error")
|
flash("用户名不能为空", "error")
|
||||||
return render_template("login.html", mode="register", captcha=""), 400
|
return render_auth_template("register", 400)
|
||||||
if len(password) < 6:
|
if len(password) < 6:
|
||||||
flash("密码至少需要 6 位", "error")
|
flash("密码至少需要 6 位", "error")
|
||||||
return render_template("login.html", mode="register", captcha=""), 400
|
return render_auth_template("register", 400)
|
||||||
if User.query.filter_by(username=username).first():
|
if User.query.filter_by(username=username).first():
|
||||||
flash("用户名已存在", "error")
|
flash("用户名已存在", "error")
|
||||||
return render_template("login.html", mode="register", captcha=""), 400
|
return render_auth_template("register", 400)
|
||||||
|
|
||||||
user = User(username=username, is_admin=False)
|
user = User(username=username, is_admin=False)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
@@ -108,7 +150,7 @@ def register():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("注册成功,请登录", "info")
|
flash("注册成功,请登录", "info")
|
||||||
session["captcha"] = new_captcha()
|
session["captcha"] = new_captcha()
|
||||||
return render_template("login.html", mode="login", captcha=session["captcha"])
|
return render_auth_template("login", captcha=session["captcha"])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/logout", methods=["POST"])
|
@bp.route("/logout", methods=["POST"])
|
||||||
@@ -127,14 +169,12 @@ def home():
|
|||||||
@bp.route("/history")
|
@bp.route("/history")
|
||||||
@login_required
|
@login_required
|
||||||
def history_page():
|
def history_page():
|
||||||
page = requested_page()
|
pagination, page_redirect = paginated_uploads(
|
||||||
pagination = (
|
UploadRecord.query.filter_by(user_id=current_user.id),
|
||||||
UploadRecord.query.filter_by(user_id=current_user.id)
|
"main.history_page",
|
||||||
.order_by(UploadRecord.upload_time.desc())
|
|
||||||
.paginate(page=page, per_page=RECORDS_PER_PAGE, error_out=False)
|
|
||||||
)
|
)
|
||||||
if pagination.pages and page > pagination.pages:
|
if page_redirect:
|
||||||
return redirect(url_for("main.history_page", page=pagination.pages))
|
return page_redirect
|
||||||
return render_template(
|
return render_template(
|
||||||
"history.html",
|
"history.html",
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
@@ -145,16 +185,13 @@ def history_page():
|
|||||||
@bp.route("/admin")
|
@bp.route("/admin")
|
||||||
@login_required
|
@login_required
|
||||||
def admin_dashboard():
|
def admin_dashboard():
|
||||||
if not current_user.is_admin:
|
require_admin()
|
||||||
abort(403)
|
pagination, page_redirect = paginated_uploads(
|
||||||
page = requested_page()
|
UploadRecord.query.options(joinedload(UploadRecord.user)),
|
||||||
pagination = (
|
"main.admin_dashboard",
|
||||||
UploadRecord.query.options(joinedload(UploadRecord.user))
|
|
||||||
.order_by(UploadRecord.upload_time.desc())
|
|
||||||
.paginate(page=page, per_page=RECORDS_PER_PAGE, error_out=False)
|
|
||||||
)
|
)
|
||||||
if pagination.pages and page > pagination.pages:
|
if page_redirect:
|
||||||
return redirect(url_for("main.admin_dashboard", page=pagination.pages))
|
return page_redirect
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin.html",
|
"admin.html",
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
@@ -166,8 +203,7 @@ def admin_dashboard():
|
|||||||
@bp.route("/admin/registration", methods=["POST"])
|
@bp.route("/admin/registration", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def update_registration_setting():
|
def update_registration_setting():
|
||||||
if not current_user.is_admin:
|
require_admin()
|
||||||
abort(403)
|
|
||||||
|
|
||||||
allow_registration = request.form.get("allow_registration") == "on"
|
allow_registration = request.form.get("allow_registration") == "on"
|
||||||
AppSetting.set_bool(REGISTRATION_SETTING_KEY, allow_registration)
|
AppSetting.set_bool(REGISTRATION_SETTING_KEY, allow_registration)
|
||||||
@@ -195,11 +231,11 @@ def download_file(record_id: int, file_type: str):
|
|||||||
abort(404)
|
abort(404)
|
||||||
if not (current_user.is_admin or current_user.id == record.user_id):
|
if not (current_user.is_admin or current_user.id == record.user_id):
|
||||||
abort(403)
|
abort(403)
|
||||||
if file_type == "original":
|
file_path = {
|
||||||
file_path = record.saved_path
|
"original": record.saved_path,
|
||||||
elif file_type == "prediction":
|
"prediction": record.prediction_path,
|
||||||
file_path = record.prediction_path
|
}.get(file_type)
|
||||||
else:
|
if file_path is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -267,21 +303,7 @@ def predict():
|
|||||||
db.session.add(record)
|
db.session.add(record)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
last_result = {
|
last_result = prediction_result_payload(artifacts, record)
|
||||||
"original_filename": artifacts.original_filename,
|
|
||||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"image_url": url_for("static", filename=f"images/{artifacts.image_filename}"),
|
|
||||||
"importance_url": (
|
|
||||||
url_for("static", filename=f"images/{artifacts.importance_filename}")
|
|
||||||
if artifacts.importance_filename
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"excel_url": url_for("main.download_file", record_id=record.id, file_type="prediction"),
|
|
||||||
"result_url": url_for("main.result_page"),
|
|
||||||
"sample_count": int(artifacts.sample_count),
|
|
||||||
"summary_rows": artifacts.summary_rows[:6],
|
|
||||||
"analysis_text": artifacts.analysis_text,
|
|
||||||
}
|
|
||||||
session["last_result"] = last_result
|
session["last_result"] = last_result
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
|
|||||||
Generated
+1049
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build:css": "tailwindcss -i ./static/css/app.src.css -o ./static/css/app.css --minify",
|
||||||
|
"watch:css": "tailwindcss -i ./static/css/app.src.css -o ./static/css/app.css --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,176 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url('../fonts/material-symbols-outlined.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
min-width: 1em;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 44px;
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .5rem;
|
||||||
|
border-radius: .375rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0 1rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-lg {
|
||||||
|
min-height: 52px;
|
||||||
|
height: 52px;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-sm {
|
||||||
|
min-height: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-field {
|
||||||
|
min-height: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-primary {
|
||||||
|
background: #005EB8;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 18px 34px rgba(15, 23, 42, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-primary:hover {
|
||||||
|
background: #0c4188;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-secondary {
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
color: #334155;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-secondary:hover {
|
||||||
|
border-color: #005EB8;
|
||||||
|
color: #005EB8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn:disabled,
|
||||||
|
.ui-btn[aria-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-primary:disabled {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-icon,
|
||||||
|
.ui-btn .material-symbols-outlined {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex: 0 0 28px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: .375rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background-color .15s ease, color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-action-row {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .75rem;
|
||||||
|
border-radius: .375rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 0 .75rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
transition: border-color .15s ease, color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-action-row:hover {
|
||||||
|
border-color: #005EB8;
|
||||||
|
color: #005EB8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 20px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 2px solid rgba(255,255,255,.35);
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin .75s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,43 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./templates/**/*.html',
|
||||||
|
'./static/js/**/*.js'
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#005EB8',
|
||||||
|
primaryDeep: '#0c4188',
|
||||||
|
page: '#f3f5f8',
|
||||||
|
card: '#ffffff',
|
||||||
|
line: '#e2e8f0',
|
||||||
|
textMain: '#0f172a',
|
||||||
|
textSub: '#64748b',
|
||||||
|
blueSoft: '#eaf3ff',
|
||||||
|
bluePanel: '#1d4f9a',
|
||||||
|
outline: '#c7ced8',
|
||||||
|
successSoft: '#e9f8ee',
|
||||||
|
successText: '#16a34a',
|
||||||
|
warnSoft: '#fff4e8',
|
||||||
|
warnText: '#c2410c',
|
||||||
|
dangerSoft: '#fff0f0',
|
||||||
|
dangerText: '#dc2626',
|
||||||
|
lowCard: '#f8fafc'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
headline: ['system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'sans-serif'],
|
||||||
|
body: ['system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'sans-serif']
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
soft: '0 18px 34px rgba(15, 23, 42, .06)',
|
||||||
|
panel: '0 1px 2px rgba(15, 23, 42, .04)',
|
||||||
|
card: '0 10px 25px rgba(15, 23, 42, .06)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/forms')
|
||||||
|
]
|
||||||
|
};
|
||||||
+4
-140
@@ -4,146 +4,7 @@
|
|||||||
<title>{% block title %}供水管道健康评估系统{% endblock %}</title>
|
<title>{% block title %}供水管道健康评估系统{% endblock %}</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<link href="{{ url_for('static', filename='css/app.css') }}" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@700;800&display=swap" rel="stylesheet" />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: '#005EB8',
|
|
||||||
primaryDeep: '#0c4188',
|
|
||||||
page: '#f3f5f8',
|
|
||||||
card: '#ffffff',
|
|
||||||
line: '#e2e8f0',
|
|
||||||
textMain: '#0f172a',
|
|
||||||
textSub: '#64748b',
|
|
||||||
blueSoft: '#eaf3ff',
|
|
||||||
outline: '#c7ced8',
|
|
||||||
successSoft: '#e9f8ee',
|
|
||||||
successText: '#16a34a',
|
|
||||||
dangerSoft: '#fff0f0',
|
|
||||||
dangerText: '#dc2626'
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
headline: ['Manrope', 'Inter', 'sans-serif'],
|
|
||||||
body: ['Inter', 'sans-serif']
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
soft: '0 18px 34px rgba(15, 23, 42, .06)',
|
|
||||||
panel: '0 1px 2px rgba(15, 23, 42, .04)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body { font-family: 'Inter', sans-serif; }
|
|
||||||
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
|
|
||||||
.material-symbols-outlined {
|
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.ui-btn {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: .5rem;
|
|
||||||
min-height: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: .375rem;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0 1rem;
|
|
||||||
font-size: .875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease;
|
|
||||||
}
|
|
||||||
.ui-btn-sm {
|
|
||||||
min-height: 40px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 .75rem;
|
|
||||||
}
|
|
||||||
.ui-btn-primary {
|
|
||||||
background: #005EB8;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 18px 34px rgba(15, 23, 42, .06);
|
|
||||||
}
|
|
||||||
.ui-btn-primary:hover { background: #0c4188; }
|
|
||||||
.ui-btn-secondary {
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
background: #fff;
|
|
||||||
color: #334155;
|
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
|
||||||
}
|
|
||||||
.ui-btn-secondary:hover {
|
|
||||||
border-color: #005EB8;
|
|
||||||
color: #005EB8;
|
|
||||||
}
|
|
||||||
.ui-btn:disabled,
|
|
||||||
.ui-btn[aria-disabled="true"] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .7;
|
|
||||||
}
|
|
||||||
.ui-btn-primary:disabled { background: #cbd5e1; }
|
|
||||||
.ui-btn-icon,
|
|
||||||
.ui-btn .material-symbols-outlined {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex: 0 0 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.ui-icon-btn {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-flex;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
flex: 0 0 28px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: .375rem;
|
|
||||||
line-height: 1;
|
|
||||||
transition: background-color .15s ease, color .15s ease;
|
|
||||||
}
|
|
||||||
.ui-action-row {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
min-height: 44px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: .75rem;
|
|
||||||
border-radius: .375rem;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
padding: 0 .75rem;
|
|
||||||
font-size: .875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
transition: border-color .15s ease, color .15s ease;
|
|
||||||
}
|
|
||||||
.ui-action-row:hover {
|
|
||||||
border-color: #005EB8;
|
|
||||||
color: #005EB8;
|
|
||||||
}
|
|
||||||
.spinner {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex: 0 0 20px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 2px solid rgba(255,255,255,.35);
|
|
||||||
border-top-color: #fff;
|
|
||||||
animation: spin .75s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
|
||||||
</style>
|
|
||||||
{% block head_extra %}{% endblock %}
|
{% block head_extra %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="flex min-h-screen flex-col bg-page text-textMain antialiased">
|
<body class="flex min-h-screen flex-col bg-page text-textMain antialiased">
|
||||||
@@ -206,6 +67,9 @@
|
|||||||
<script>
|
<script>
|
||||||
window.__flashMessages = {{ flashed_messages|tojson }};
|
window.__flashMessages = {{ flashed_messages|tojson }};
|
||||||
</script>
|
</script>
|
||||||
|
<div class="sr-only" aria-hidden="true">
|
||||||
|
{% for category, message in flashed_messages %}{{ message }}{% endfor %}
|
||||||
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<main class="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
<main class="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||||
|
|||||||
+1
-109
@@ -4,108 +4,8 @@
|
|||||||
<title>{{ '注册' if mode == 'register' else '登录' }} | 供水管道健康评估系统</title>
|
<title>{{ '注册' if mode == 'register' else '登录' }} | 供水管道健康评估系统</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<link href="{{ url_for('static', filename='css/app.css') }}" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@700;800&display=swap" rel="stylesheet" />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: '#005EB8',
|
|
||||||
primaryDeep: '#0c4188',
|
|
||||||
page: '#f3f5f8',
|
|
||||||
card: '#ffffff',
|
|
||||||
line: '#e5e7eb',
|
|
||||||
textMain: '#0f172a',
|
|
||||||
textSub: '#64748b',
|
|
||||||
blueSoft: '#eaf3ff',
|
|
||||||
bluePanel: '#1d4f9a',
|
|
||||||
outline: '#c7ced8',
|
|
||||||
successSoft: '#e9f8ee',
|
|
||||||
successText: '#16a34a',
|
|
||||||
warnSoft: '#fff4e8',
|
|
||||||
warnText: '#c2410c',
|
|
||||||
dangerSoft: '#fff0f0',
|
|
||||||
dangerText: '#dc2626',
|
|
||||||
lowCard: '#f8fafc'
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
headline: ['Manrope', 'Inter', 'sans-serif'],
|
|
||||||
body: ['Inter', 'sans-serif']
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
soft: '0 24px 24px -12px rgba(24,28,30,.06)',
|
|
||||||
card: '0 10px 25px rgba(15, 23, 42, .06)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
<style>
|
||||||
.material-symbols-outlined {
|
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.ui-btn {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: .5rem;
|
|
||||||
min-height: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: .375rem;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0 1rem;
|
|
||||||
font-size: .875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease;
|
|
||||||
}
|
|
||||||
.ui-btn-lg {
|
|
||||||
min-height: 52px;
|
|
||||||
height: 52px;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
.ui-btn-field {
|
|
||||||
min-height: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
.ui-btn-primary {
|
|
||||||
background: #005EB8;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 18px 34px rgba(15, 23, 42, .06);
|
|
||||||
}
|
|
||||||
.ui-btn-primary:hover { background: #0c4188; }
|
|
||||||
.ui-btn-secondary {
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
background: #fff;
|
|
||||||
color: #334155;
|
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
|
||||||
}
|
|
||||||
.ui-btn-secondary:hover {
|
|
||||||
border-color: #005EB8;
|
|
||||||
color: #005EB8;
|
|
||||||
}
|
|
||||||
.ui-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .7;
|
|
||||||
}
|
|
||||||
.ui-btn-primary:disabled { background: #cbd5e1; }
|
|
||||||
.ui-btn .material-symbols-outlined {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex: 0 0 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.password-toggle {
|
.password-toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .75rem;
|
right: .75rem;
|
||||||
@@ -144,8 +44,6 @@
|
|||||||
background: #fff7f7 !important;
|
background: #fff7f7 !important;
|
||||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, .12) !important;
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, .12) !important;
|
||||||
}
|
}
|
||||||
body { font-family: 'Inter', sans-serif; }
|
|
||||||
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
|
|
||||||
.dot-grid {
|
.dot-grid {
|
||||||
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
|
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
|
||||||
background-size: 42px 42px;
|
background-size: 42px 42px;
|
||||||
@@ -157,11 +55,6 @@
|
|||||||
.gradient-board {
|
.gradient-board {
|
||||||
background: linear-gradient(180deg, #2455a3 0%, #123e7d 100%);
|
background: linear-gradient(180deg, #2455a3 0%, #123e7d 100%);
|
||||||
}
|
}
|
||||||
.spinner {
|
|
||||||
width: 18px; height: 18px; border-radius: 9999px;
|
|
||||||
border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
|
|
||||||
animation: spin .75s linear infinite;
|
|
||||||
}
|
|
||||||
.auth-alert {
|
.auth-alert {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -203,7 +96,6 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-page min-h-screen text-textMain">
|
<body class="bg-page min-h-screen text-textMain">
|
||||||
|
|||||||
Reference in New Issue
Block a user