- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航 - PickModal 和 SwapModal 集成分页功能,每页20项 - 搜索/筛选切换自动回到第1页 - 现代化CSS样式:hover浮起动效、主色高亮、移动端适配 - 新增 dashboard、admin 管理模板 - 菜系(cuisine)模型和迁移 - 构建脚本 build.py 支持 PyInstaller 打包 - 前端资源重新构建
297 lines
9.7 KiB
HTML
297 lines
9.7 KiB
HTML
{% extends "admin/base.html" %}
|
|
{% load i18n static jazzmin %}
|
|
{% get_jazzmin_settings request as jazzmin_settings %}
|
|
{% get_jazzmin_ui_tweaks as jazzmin_ui %}
|
|
|
|
{% block title %}{% trans "Change password" %} | {{ jazzmin_settings.site_title }}{% endblock %}
|
|
|
|
{% block bodyclass %}password-change-page{% endblock %}
|
|
|
|
{% block extrastyle %}
|
|
<style>
|
|
.password-change-page .app-content {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding-top: 32px;
|
|
padding-bottom: 48px;
|
|
}
|
|
.password-card {
|
|
width: 100%;
|
|
max-width: 480px;
|
|
background: var(--bs-card-bg, #fff);
|
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 6px 16px rgba(0,0,0,.04);
|
|
overflow: hidden;
|
|
}
|
|
.password-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 24px 28px 0;
|
|
}
|
|
.password-card-header .header-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
|
flex-shrink: 0;
|
|
}
|
|
.password-card-header h3 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
color: var(--bs-body-color, #212529);
|
|
}
|
|
.password-card-header .subtitle {
|
|
font-size: 13px;
|
|
color: var(--bs-secondary-color, #6c757d);
|
|
margin-top: 2px;
|
|
}
|
|
.password-card-body {
|
|
padding: 20px 28px 28px;
|
|
}
|
|
.password-card-body .form-label {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
margin-bottom: 6px;
|
|
color: var(--bs-body-color, #212529);
|
|
}
|
|
.password-card-body .form-control {
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
font-size: 14px;
|
|
transition: border-color .15s ease, box-shadow .15s ease;
|
|
}
|
|
.password-card-body .form-control:focus {
|
|
border-color: #818cf8;
|
|
box-shadow: 0 0 0 3px rgba(129,140,248,.2);
|
|
}
|
|
.input-group .form-control {
|
|
border-top-right-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
.input-group .btn-toggle-pw {
|
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
border-left: none;
|
|
border-radius: 0 6px 6px 0;
|
|
background: var(--bs-tertiary-bg, #f8f9fa);
|
|
color: var(--bs-secondary-color, #6c757d);
|
|
padding: 0 14px;
|
|
cursor: pointer;
|
|
transition: background .15s, color .15s;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.input-group .btn-toggle-pw:hover {
|
|
background: #e9ecef;
|
|
color: var(--bs-body-color, #212529);
|
|
}
|
|
.pw-strength {
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: #e9ecef;
|
|
margin-top: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.pw-strength-bar {
|
|
height: 100%;
|
|
width: 0;
|
|
border-radius: 2px;
|
|
transition: width .25s ease, background .25s ease;
|
|
}
|
|
.pw-strength-text {
|
|
font-size: 12px;
|
|
margin-top: 4px;
|
|
color: var(--bs-secondary-color, #6c757d);
|
|
}
|
|
.errorlist {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 4px 0 0;
|
|
}
|
|
.errorlist li {
|
|
font-size: 12px;
|
|
color: var(--bs-danger, #dc3545);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.non-field-errors {
|
|
background: #fef2f2;
|
|
border: 1px solid #fecaca;
|
|
border-radius: 6px;
|
|
padding: 12px 14px;
|
|
margin-bottom: 20px;
|
|
color: var(--bs-danger, #dc3545);
|
|
font-size: 13px;
|
|
}
|
|
.btn-submit {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
transition: opacity .15s, transform .1s;
|
|
margin-top: 8px;
|
|
}
|
|
.btn-submit:hover { opacity: .9; }
|
|
.btn-submit:active { transform: scale(.98); }
|
|
.form-hint-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px 16px;
|
|
margin-top: 4px;
|
|
}
|
|
.form-hint-row span {
|
|
font-size: 12px;
|
|
color: var(--bs-secondary-color, #6c757d);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.form-hint-row .check { color: var(--bs-success, #198754); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block page_content %}
|
|
<div class="app-content">
|
|
<div class="password-card">
|
|
<div class="password-card-header">
|
|
<div class="header-icon"><i class="fas fa-shield-halved"></i></div>
|
|
<div>
|
|
<h3>{% trans "Change password" %}</h3>
|
|
<div class="subtitle">{{ request.user }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="password-card-body">
|
|
{% if form.errors %}
|
|
{% if form.non_field_errors %}
|
|
<div class="non-field-errors">
|
|
{% for error in form.non_field_errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<form method="post">
|
|
{% csrf_token %}
|
|
|
|
<div class="mb-3">
|
|
<label for="id_old_password" class="form-label">{% trans "Old password" %}</label>
|
|
<div class="input-group">
|
|
<input type="password" name="old_password" id="id_old_password" class="form-control" autofocus required autocomplete="current-password">
|
|
<button type="button" class="btn-toggle-pw" onclick="togglePw(this)" aria-label="Toggle">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
{% if form.old_password.errors %}<ul class="errorlist">{% for e in form.old_password.errors %}<li>{{ e }}</li>{% endfor %}</ul>{% endif %}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="id_new_password1" class="form-label">{% trans "New password" %}</label>
|
|
<div class="input-group">
|
|
<input type="password" name="new_password1" id="id_new_password1" class="form-control" required autocomplete="new-password" oninput="checkStrength(this.value)">
|
|
<button type="button" class="btn-toggle-pw" onclick="togglePw(this)" aria-label="Toggle">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="pw-strength"><div class="pw-strength-bar" id="pwBar"></div></div>
|
|
<div class="pw-strength-text" id="pwText"></div>
|
|
<div class="form-hint-row" id="pwHints">
|
|
<span id="hintLen"><i class="fas fa-circle" style="font-size:6px"></i> 至小8位</span>
|
|
<span id="hintUpper"><i class="fas fa-circle" style="font-size:6px"></i> 含大写字母</span>
|
|
<span id="hintLower"><i class="fas fa-circle" style="font-size:6px"></i> 含小写字母</span>
|
|
<span id="hintDigit"><i class="fas fa-circle" style="font-size:6px"></i> 含数字</span>
|
|
</div>
|
|
{% if form.new_password1.errors %}<ul class="errorlist">{% for e in form.new_password1.errors %}<li>{{ e }}</li>{% endfor %}</ul>{% endif %}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="id_new_password2" class="form-label">{% trans "Password confirmation" %}</label>
|
|
<div class="input-group">
|
|
<input type="password" name="new_password2" id="id_new_password2" class="form-control" required autocomplete="new-password" oninput="checkMatch()">
|
|
<button type="button" class="btn-toggle-pw" onclick="togglePw(this)" aria-label="Toggle">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div id="matchHint" style="font-size:12px;margin-top:4px;color:var(--bs-secondary-color,#6c757d)"></div>
|
|
{% if form.new_password2.errors %}<ul class="errorlist">{% for e in form.new_password2.errors %}<li>{{ e }}</li>{% endfor %}</ul>{% endif %}
|
|
</div>
|
|
|
|
<button type="submit" class="btn-submit">{% trans "Change my password" %}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extrajs %}
|
|
<script>
|
|
function togglePw(btn) {
|
|
var input = btn.parentElement.querySelector("input");
|
|
var icon = btn.querySelector("i");
|
|
if (input.type === "password") {
|
|
input.type = "text";
|
|
icon.classList.replace("fa-eye", "fa-eye-slash");
|
|
} else {
|
|
input.type = "password";
|
|
icon.classList.replace("fa-eye-slash", "fa-eye");
|
|
}
|
|
}
|
|
function checkStrength(pw) {
|
|
var bar = document.getElementById("pwBar");
|
|
var text = document.getElementById("pwText");
|
|
var score = 0;
|
|
if (pw.length >= 8) score++;
|
|
if (pw.length >= 12) score++;
|
|
if (/[A-Z]/.test(pw)) score++;
|
|
if (/[a-z]/.test(pw)) score++;
|
|
if (/[0-9]/.test(pw)) score++;
|
|
if (/[^A-Za-z0-9]/.test(pw)) score++;
|
|
var pct = pw.length === 0 ? 0 : Math.min(score / 6 * 100, 100);
|
|
bar.style.width = pct + "%";
|
|
var colors = ["#dc3545","#dc3545","#fd7e14","#ffc107","#198754","#198754","#198754"];
|
|
var labels = ["","\u5f31","\u4e00\u822c","\u826f\u597d","\u5f3a","\u5f88\u5f3a","\u5f88\u5f3a"];
|
|
var idx = Math.min(Math.floor(score), colors.length - 1);
|
|
bar.style.background = colors[idx];
|
|
text.textContent = pw.length ? labels[idx] : "";
|
|
text.style.color = colors[idx];
|
|
setHint("hintLen", pw.length >= 8);
|
|
setHint("hintUpper", /[A-Z]/.test(pw));
|
|
setHint("hintLower", /[a-z]/.test(pw));
|
|
setHint("hintDigit", /[0-9]/.test(pw));
|
|
checkMatch();
|
|
}
|
|
function setHint(id, ok) {
|
|
var el = document.getElementById(id);
|
|
var txt = el.textContent.trim();
|
|
if (ok) {
|
|
el.innerHTML = '<i class="fas fa-check check"></i> ' + txt;
|
|
} else {
|
|
el.innerHTML = '<i class="fas fa-circle" style="font-size:6px"></i> ' + txt;
|
|
}
|
|
}
|
|
function checkMatch() {
|
|
var hint = document.getElementById("matchHint");
|
|
var pw1 = document.getElementById("id_new_password1").value;
|
|
var pw2 = document.getElementById("id_new_password2").value;
|
|
if (!pw2) { hint.textContent = ""; return; }
|
|
if (pw1 === pw2) {
|
|
hint.innerHTML = '<span style="color:var(--bs-success,#198754)"><i class="fas fa-check-circle"></i> \u5bc6\u7801\u5339\u914d</span>';
|
|
} else {
|
|
hint.innerHTML = '<span style="color:var(--bs-danger,#dc3545)"><i class="fas fa-times-circle"></i> \u5bc6\u7801\u4e0d\u4e00\u81f4</span>';
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|