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