feat: update assessment portal

This commit is contained in:
2026-07-06 16:53:06 +08:00
parent a75e857c71
commit f385f9747b
17 changed files with 1866 additions and 884 deletions
+294
View File
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>{% block title %}供水管道健康评估系统{% endblock %}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<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 %}
</head>
<body class="flex min-h-screen flex-col bg-page text-textMain antialiased">
<header class="sticky top-0 z-30 border-b border-line bg-white/95 backdrop-blur">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="h-16 flex items-center justify-between gap-4">
<a href="{{ url_for('main.home') }}" class="flex min-w-0 items-center gap-2 text-[16px] font-extrabold tracking-tight">
<span class="material-symbols-outlined text-primary">water_drop</span>
<span class="truncate">供水管道健康评估系统</span>
</a>
<nav class="hidden md:flex h-full items-center gap-1 text-sm font-semibold">
<a href="{{ url_for('main.home') }}" class="flex h-full items-center px-3 {{ 'text-primary border-b-2 border-primary' if active_page == 'home' else 'text-slate-500 border-b-2 border-transparent hover:text-primary' }}">主页</a>
<a href="{{ url_for('main.result_page') }}" class="flex h-full items-center px-3 {{ 'text-primary border-b-2 border-primary' if active_page == 'result' else 'text-slate-500 border-b-2 border-transparent hover:text-primary' }}">结果</a>
<a href="{{ url_for('main.history_page') }}" class="flex h-full items-center px-3 {{ 'text-primary border-b-2 border-primary' if active_page == 'history' else 'text-slate-500 border-b-2 border-transparent hover:text-primary' }}">历史</a>
<a href="{{ url_for('main.reference_page') }}" class="flex h-full items-center px-3 {{ 'text-primary border-b-2 border-primary' if active_page == 'reference' else 'text-slate-500 border-b-2 border-transparent hover:text-primary' }}">文档</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_dashboard') }}" class="flex h-full items-center px-3 {{ 'text-primary border-b-2 border-primary' if active_page == 'admin' else 'text-slate-500 border-b-2 border-transparent hover:text-primary' }}">管理</a>
{% endif %}
</nav>
<div class="flex items-center gap-3">
<div class="hidden sm:flex items-center gap-2 text-sm font-semibold text-slate-600">
<div class="h-8 w-8 rounded-full bg-primary text-white flex items-center justify-center text-xs">{{ current_user.username[:1]|upper }}</div>
<span class="max-w-[120px] truncate">{{ current_user.username }}</span>
</div>
<form method="post" action="{{ url_for('main.logout') }}" data-clear-home-state>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="ui-btn ui-btn-sm ui-btn-secondary font-semibold text-slate-600" type="submit">退出</button>
</form>
</div>
</div>
</div>
<nav class="md:hidden border-t border-line bg-white">
<div class="mx-auto max-w-7xl px-2 flex overflow-x-auto text-sm font-semibold">
<a href="{{ url_for('main.home') }}" class="whitespace-nowrap px-3 py-3 {{ 'text-primary' if active_page == 'home' else 'text-slate-500' }}">主页</a>
<a href="{{ url_for('main.result_page') }}" class="whitespace-nowrap px-3 py-3 {{ 'text-primary' if active_page == 'result' else 'text-slate-500' }}">结果</a>
<a href="{{ url_for('main.history_page') }}" class="whitespace-nowrap px-3 py-3 {{ 'text-primary' if active_page == 'history' else 'text-slate-500' }}">历史</a>
<a href="{{ url_for('main.reference_page') }}" class="whitespace-nowrap px-3 py-3 {{ 'text-primary' if active_page == 'reference' else 'text-slate-500' }}">文档</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_dashboard') }}" class="whitespace-nowrap px-3 py-3 {{ 'text-primary' if active_page == 'admin' else 'text-slate-500' }}">管理</a>
{% endif %}
</div>
</nav>
</header>
{% with flashed_messages = get_flashed_messages(with_categories=true) %}
<div id="alertBox" class="pointer-events-none fixed left-1/2 top-20 z-50 hidden w-[calc(100vw-2rem)] max-w-[430px] -translate-x-1/2 translate-y-3 rounded-lg border bg-white p-4 opacity-0 shadow-[0_18px_45px_rgba(15,23,42,.16)] transition-all duration-200 ease-out sm:top-24" role="status" aria-live="polite">
<div class="flex items-start gap-3">
<span id="alertIconWrap" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md">
<span id="alertIcon" class="material-symbols-outlined text-xl">priority_high</span>
</span>
<div class="min-w-0 flex-1 pt-0.5">
<div id="alertTitle" class="text-sm font-extrabold text-textMain"></div>
<div id="alertMessage" class="mt-1 text-sm leading-5 text-textSub"></div>
</div>
<button id="alertClose" class="ui-icon-btn -mr-1 -mt-1 text-slate-400 hover:bg-slate-100 hover:text-slate-700" type="button" aria-label="关闭通知">
<span class="material-symbols-outlined text-base">close</span>
</button>
</div>
</div>
<script>
window.__flashMessages = {{ flashed_messages|tojson }};
</script>
{% 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">
{% block content %}{% endblock %}
</main>
<footer class="border-t border-line bg-white">
<div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between text-xs text-slate-500">
<span>预测结果仅供参考</span>
<span>© {{ now_year() }} 供水管道健康评估系统</span>
</div>
</footer>
<script>
(() => {
const alertBox = document.getElementById('alertBox');
const alertIconWrap = document.getElementById('alertIconWrap');
const alertIcon = document.getElementById('alertIcon');
const alertTitle = document.getElementById('alertTitle');
const alertMessage = document.getElementById('alertMessage');
const alertClose = document.getElementById('alertClose');
let alertTimer = null;
let alertHideTimer = null;
if (!alertBox || !alertIconWrap || !alertIcon || !alertTitle || !alertMessage || !alertClose) return;
function hideAlert() {
clearTimeout(alertTimer);
clearTimeout(alertHideTimer);
alertBox.classList.remove('opacity-100', 'translate-y-0');
alertBox.classList.add('pointer-events-none', 'opacity-0', 'translate-y-3');
alertHideTimer = setTimeout(() => {
alertBox.classList.add('hidden');
}, 200);
}
function showAppNotification(message, type = 'info', title) {
clearTimeout(alertTimer);
clearTimeout(alertHideTimer);
alertBox.classList.remove('hidden', 'border-red-200', 'border-blue-200');
alertIconWrap.classList.remove('bg-dangerSoft', 'text-dangerText', 'bg-blueSoft', 'text-primary');
if (type === 'error') {
alertBox.classList.add('border-red-200');
alertIconWrap.classList.add('bg-dangerSoft', 'text-dangerText');
alertIcon.textContent = 'priority_high';
alertTitle.textContent = title || '操作未完成';
} else {
alertBox.classList.add('border-blue-200');
alertIconWrap.classList.add('bg-blueSoft', 'text-primary');
alertIcon.textContent = 'check_circle';
alertTitle.textContent = title || '操作成功';
}
alertMessage.textContent = message;
requestAnimationFrame(() => {
alertBox.classList.remove('pointer-events-none', 'opacity-0', 'translate-y-3');
alertBox.classList.add('opacity-100', 'translate-y-0');
});
alertTimer = setTimeout(hideAlert, 10000);
}
alertClose.addEventListener('click', hideAlert);
window.showAppNotification = showAppNotification;
window.hideAppNotification = hideAlert;
const flashedMessages = window.__flashMessages || [];
if (flashedMessages.length) {
const [category, message] = flashedMessages[flashedMessages.length - 1];
showAppNotification(message, category === 'error' ? 'error' : 'info');
}
document.querySelectorAll('[data-clear-home-state]').forEach((form) => {
form.addEventListener('submit', () => {
try {
sessionStorage.removeItem('pipelineLifetime.homeState');
} catch (err) {
// Ignore storage failures during logout.
}
});
});
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>