refactor: split flask app structure

This commit is contained in:
2026-07-06 14:41:16 +08:00
parent c2198aad51
commit a75e857c71
18 changed files with 1871 additions and 1390 deletions
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>管理台</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 = {
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>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body { font-family: 'Inter', sans-serif; }
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
.dot-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
background-size: 42px 42px;
}
.panel-frame {
box-shadow: none;
border: none;
}
.gradient-board {
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;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-page min-h-screen p-8 text-textMain">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-extrabold">管理员查看上传记录</h1>
<a href="{{ url_for('main.home') }}" class="px-4 py-2 rounded-lg border border-slate-200 bg-white">返回主页</a>
</div>
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-card">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-500">
<tr>
<th class="text-left px-4 py-3">用户</th>
<th class="text-left px-4 py-3">原始文件</th>
<th class="text-left px-4 py-3">上传时间</th>
<th class="text-left px-4 py-3">下载</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="border-t border-slate-100">
<td class="px-4 py-3">{{ record.user.username }}</td>
<td class="px-4 py-3">{{ record.original_filename }}</td>
<td class="px-4 py-3">{{ record.upload_time.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td class="px-4 py-3 flex gap-3">
<a class="text-primary" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}">原始文件</a>
<a class="text-primary" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}">预测结果</a>
</td>
</tr>
{% else %}
<tr><td colspan="4" class="px-4 py-8 text-center text-slate-500">暂无上传记录</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>预测历史</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 = {
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>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body { font-family: 'Inter', sans-serif; }
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
.dot-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
background-size: 42px 42px;
}
.panel-frame {
box-shadow: none;
border: none;
}
.gradient-board {
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;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-page min-h-screen p-8 text-textMain">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-extrabold">预测历史</h1>
<a href="{{ url_for('main.home') }}" class="px-4 py-2 rounded-lg border border-slate-200 bg-white">返回主页</a>
</div>
<div class="grid gap-4">
{% for record in records %}
<div class="bg-white rounded-2xl border border-slate-200 p-5 shadow-card flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<div class="font-bold">{{ record.original_filename }}</div>
<div class="text-sm text-textSub mt-1">{{ record.upload_time.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
<div class="flex flex-wrap gap-3">
<a class="px-4 py-2 rounded-lg border border-slate-200 bg-white" href="{{ url_for('main.download_file', record_id=record.id, file_type='original') }}">原始文件</a>
<a class="px-4 py-2 rounded-lg bg-primary text-white" href="{{ url_for('main.download_file', record_id=record.id, file_type='prediction') }}">预测结果</a>
</div>
</div>
{% else %}
<div class="bg-white rounded-2xl border border-slate-200 p-10 text-center text-textSub">还没有历史记录。</div>
{% endfor %}
</div>
</div>
</body>
</html>
+252
View File
@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title></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 = {
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>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body { font-family: 'Inter', sans-serif; }
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
.dot-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
background-size: 42px 42px;
}
.panel-frame {
box-shadow: none;
border: none;
}
.gradient-board {
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;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-page min-h-screen text-textMain">
<header class="sticky top-0 z-30 bg-white border-b border-line">
<div class="px-6 lg:px-10">
<div class="h-16 flex items-center justify-between gap-4">
<div class="flex items-center gap-10">
<div class="flex items-center gap-2 text-[18px] font-extrabold tracking-tight">
<span class="material-symbols-outlined text-primary">water_drop</span>
<span>供水管道健康评估系统</span>
</div>
<nav class="hidden md:flex items-center gap-7 text-[13px] font-semibold self-stretch">
<a href="{{ url_for('main.home') }}" class="flex items-center text-primary border-b-2 border-primary">主页</a>
<a href="{{ url_for('main.result_page') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">结果</a>
<a href="{{ url_for('main.history_page') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">历史</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_dashboard') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">管理</a>
{% endif %}
</nav>
</div>
<div class="flex items-center gap-4 text-slate-600">
<span class="material-symbols-outlined">notifications</span>
<span class="material-symbols-outlined">help</span>
<div class="h-8 w-px bg-slate-200"></div>
<div class="flex items-center gap-3 text-sm font-semibold">
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-xs">{{ current_user.username[:1]|upper }}</div>
<form method="post" action="{{ url_for('main.logout') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="text-textMain" type="submit">退出登录</button>
</form>
</div>
</div>
</div>
</div>
</header>
<main class="px-6 lg:px-10 py-8">
<section class="mb-8">
<h1 class="text-[40px] font-extrabold tracking-tight leading-tight">供水管道健康状态与剩余寿命评估技术导则</h1>
<p class="mt-2 text-textSub text-[15px]">上传您的数据以生成预测结果供参考。</p>
</section>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="space-y-3 mb-6">
{% for category, message in messages %}
<div class="rounded-xl px-4 py-3 text-sm border {{ 'bg-dangerSoft text-dangerText border-red-200' if category == 'error' else 'bg-blueSoft text-primary border-blue-200' }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div id="alertBox" class="hidden mb-6 rounded-xl px-4 py-3 text-sm border"></div>
<div id="mainGrid" class="grid grid-cols-1 xl:grid-cols-[1.45fr_1fr] gap-6 items-start">
<div>
<section class="bg-[#fafafa] rounded-[18px] border border-slate-200 p-7 shadow-soft">
<div class="flex items-center justify-between gap-4 mb-5">
<h2 class="text-[22px] font-bold flex items-center gap-2">
<span class="material-symbols-outlined text-primary">upload_file</span>
文件上传
</h2>
<a href="{{ url_for('main.download_template') }}" class="text-primary font-semibold text-sm flex items-center gap-1">
<span class="material-symbols-outlined text-sm">download</span>
下载模板
</a>
</div>
<form id="predictForm" action="{{ url_for('main.predict') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label id="dropZone" class="block relative rounded-[14px] border-2 border-dashed border-outline p-12 bg-white hover:border-primary hover:bg-blue-50/40 transition-all cursor-pointer text-center">
<input id="fileInput" name="file" type="file" accept=".csv,.xls,.xlsx" class="absolute inset-0 opacity-0 cursor-pointer" required>
<div class="flex flex-col items-center gap-4 pointer-events-none">
<div class="w-16 h-16 rounded-full bg-blueSoft text-primary flex items-center justify-center">
<span class="material-symbols-outlined text-3xl">cloud_upload</span>
</div>
<div>
<p class="text-[28px] font-bold">将您的数据文件拖放到此处</p>
<p class="text-[15px] text-textSub mt-1">支持 Excel (.xlsx) 和 CSV 文件</p>
<p id="selectedFileName" class="hidden mt-3 text-sm text-primary font-semibold"></p>
</div>
</div>
</label>
<div class="mt-8 flex justify-end">
<button id="submitBtn" class="bg-primary hover:bg-primaryDeep text-white px-8 py-4 rounded-[8px] font-bold flex items-center gap-3 shadow-lg shadow-blue-200 min-w-[170px] justify-center" type="submit">
<span id="submitText">分析并预测</span>
<span id="submitIcon" class="material-symbols-outlined">analytics</span>
</button>
</div>
</form>
</section>
<div class="mt-5">
<a href="{{ url_for('main.result_page') }}" class="bg-[#fafafa] rounded-[14px] border border-slate-200 p-6 flex items-center justify-between hover:bg-slate-50 hover:border-primary transition-colors">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-blueSoft border border-blue-100 flex items-center justify-center text-primary">
<span class="material-symbols-outlined">monitoring</span>
</div>
<div>
<h3 class="font-bold text-[18px]">进入结果页</h3>
<p class="text-sm text-textSub">查看完整的健康评估与重要性分析</p>
</div>
</div>
<span class="material-symbols-outlined text-slate-400">chevron_right</span>
</a>
</div>
</div>
<aside>
<section class="bg-[#fafafa] rounded-[18px] border border-slate-200 p-7 shadow-soft">
<h2 class="text-[22px] font-bold flex items-center gap-2 mb-4">
<span class="material-symbols-outlined text-[#b45309]">info</span>
数据要求
</h2>
<p class="text-[14px] text-textSub leading-7 mb-6">为确保准确的生存分析,您上传的文件必须包含以下字段。请确保数据类型严格遵循模板。</p>
<div class="space-y-3">
<div class="bg-white rounded-xl border-l-4 border-primary p-4 border border-slate-200">
<div class="text-[11px] font-extrabold uppercase tracking-[0.18em] text-primary mb-1">必填基础信息</div>
<div class="font-semibold">管道编号、管龄、状态、管材、管径</div>
</div>
<div class="bg-white rounded-xl border-l-4 border-slate-500 p-4 border border-slate-200">
<div class="text-[11px] font-extrabold uppercase tracking-[0.18em] text-slate-500 mb-1">选填历史信息</div>
<div class="font-semibold">流速、压力、温度、降雨量、位置</div>
</div>
<div class="bg-white rounded-xl border-l-4 border-[#ea580c] p-4 border border-slate-200">
<div class="text-[11px] font-extrabold uppercase tracking-[0.18em] text-[#ea580c] mb-1">选填内壁特征</div>
<div class="font-semibold">结构缺陷、功能缺陷</div>
</div>
<div class="bg-white rounded-xl border-l-4 border-red-500 p-4 border border-slate-200">
<div class="text-[11px] font-extrabold uppercase tracking-[0.18em] text-red-500 mb-1">选填运行环境</div>
<div class="font-semibold">字段名需与模板完全一致</div>
</div>
<div class="bg-blueSoft rounded-xl p-4 border border-blue-100 text-sm text-primary flex items-start gap-3">
<span class="material-symbols-outlined mt-0.5">tips_and_updates</span>
<span>上传数据类型请参考《供水管道健康状态与剩余寿命评估技术导则》附录内容</span>
</div>
</div>
</section>
</aside>
<section id="inlineResult" class="hidden bg-[#fafafa] rounded-[18px] border border-slate-200 p-6 shadow-soft self-stretch">
<div class="mb-4">
<h2 class="text-[24px] font-extrabold">最新预测结果</h2>
<p class="text-textSub text-sm mt-1">已生成预测图和 Excel 结果文件。</p>
</div>
<div class="bg-white rounded-xl border border-slate-200 p-4 mb-4">
<div class="text-xs font-semibold text-slate-500 mb-2">生存概率阶梯图</div>
<img id="resultImage" src="" alt="预测图" class="w-full h-auto object-contain">
</div>
<div id="resultImportanceWrap" class="hidden bg-white rounded-xl border border-slate-200 p-4 mb-4">
<div class="text-xs font-semibold text-slate-500 mb-2">模型输入因素重要性排序</div>
<img id="resultImportanceImage" src="" alt="重要性排序图" class="w-full h-auto object-contain">
</div>
<div class="bg-white rounded-xl border border-slate-200 p-5 mb-4">
<div class="text-xs uppercase tracking-[0.18em] text-slate-500 mb-3">结果摘要</div>
<div class="space-y-3 text-sm">
<div class="flex items-center justify-between gap-4"><span class="text-textSub">上传文件</span><span id="summaryFilename" class="font-semibold break-all text-right"></span></div>
<div class="flex items-center justify-between gap-4"><span class="text-textSub">预测样本数</span><span id="summaryCount" class="font-semibold">-</span></div>
<div class="flex items-center justify-between gap-4"><span class="text-textSub">模型</span><span class="font-semibold text-right break-all">my_survival_forest_model_quxi-10-0331</span></div>
</div>
</div>
<div class="flex flex-col gap-3">
<a id="resultPageBtn" href="{{ url_for('main.result_page') }}" class="px-5 py-3 rounded-xl border border-slate-200 bg-white font-semibold text-center">进入结果页</a>
<a id="excelBtn" href="#" class="px-5 py-3 rounded-xl bg-primary text-white font-semibold text-center">下载预测报告</a>
</div>
</section>
</div>
</main>
<footer class="px-6 lg:px-10 py-6 border-t border-line text-[12px] text-slate-500 flex items-center justify-between">
<span>预测结果仅供参考</span>
<div class="flex gap-6">
<span></span><span></span><span></span><span>文档</span>
</div>
</footer>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body>
</html>
+197
View File
@@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>{{ '注册' if mode == 'register' else '登录' }} | </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 = {
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>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body { font-family: 'Inter', sans-serif; }
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
.dot-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
background-size: 42px 42px;
}
.panel-frame {
box-shadow: none;
border: none;
}
.gradient-board {
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;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-page min-h-screen text-textMain">
<div class="min-h-screen grid lg:grid-cols-[1.4fr_1fr]">
<section class="gradient-board hidden lg:flex flex-col justify-between px-16 py-14 text-white">
<div class="flex items-center gap-2 text-[16px] font-bold">
<span class="material-symbols-outlined">water_drop</span>
<span>供水管道健康评估系统</span>
</div>
<div>
<h1 class="text-[52px] leading-[1.5] font-extrabold tracking-tight">
供水管道健康状态<br>
与剩余寿命评估<br>
技术导则
</h1>
<p class="mt-6 text-white/75 text-[15px] max-w-[440px] leading-7">上传管网数据,自动生成健康状态评估、剩余寿命预测与输入因素重要性分析。</p>
</div>
<div class="text-white/50 text-[12px]">© {{ now_year() }} 供水管道健康评估系统</div>
</section>
<section class="bg-white flex flex-col justify-between min-h-screen">
<div class="flex-1 flex items-center px-7 sm:px-12 md:px-14 py-12">
<div class="w-full max-w-[360px] mx-auto">
<h2 class="text-center text-[44px] lg:text-[40px] font-extrabold tracking-tight mb-10">系统门户</h2>
<div class="flex items-center gap-8 text-[13px] font-semibold border-b border-slate-200 mb-7">
<a href="{{ url_for('main.login') }}" class="py-3 {{ 'text-primary border-b-2 border-primary' if mode == 'login' else 'text-slate-500' }}">登录</a>
{% if allow_registration %}
<a href="{{ url_for('main.register') }}" class="py-3 {{ 'text-primary border-b-2 border-primary' if mode == 'register' else 'text-slate-500' }}">注册</a>
{% endif %}
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="space-y-3 mb-5">
{% for category, message in messages %}
<div class="rounded-xl px-4 py-3 text-sm border {{ 'bg-dangerSoft text-dangerText border-red-200' if category == 'error' else 'bg-blueSoft text-primary border-blue-200' }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if mode == 'login' %}
<form method="post" action="{{ url_for('main.login') }}" class="space-y-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label class="block text-[11px] tracking-[0.18em] uppercase text-slate-500 mb-2">身份识别 / 用户名</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">person</span>
<input name="username" required class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="输入系统 ID" />
</div>
</div>
<div>
<div class="flex items-center justify-between mb-2">
<label class="block text-[11px] tracking-[0.18em] uppercase text-slate-500">安全凭据</label>
<span class="text-[12px] text-primary font-semibold">找回密码</span>
</div>
<div class="relative">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">lock</span>
<input name="password" type="password" required class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="••••••••" />
</div>
</div>
<div>
<label class="block text-[11px] tracking-[0.18em] uppercase text-slate-500 mb-2">视觉验证</label>
<div class="grid grid-cols-[1fr_92px_28px] gap-3 items-center">
<div class="relative">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">verified_user</span>
<input name="captcha" required class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="验证码" />
</div>
<div class="rounded-xl bg-blueSoft text-textMain border border-blue-100 h-[50px] flex items-center justify-center font-black tracking-[0.18em] italic">{{ captcha }}</div>
<a href="{{ url_for('main.login') }}" class="text-slate-500 hover:text-primary text-center">
<span class="material-symbols-outlined">refresh</span>
</a>
</div>
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-500">
<input type="checkbox" name="remember" class="rounded border-slate-300 text-primary focus:ring-primary" />
保持登录状态 24 小时
</label>
<button class="w-full mt-2 rounded-xl bg-primary hover:bg-primaryDeep text-white font-bold py-4 shadow-lg shadow-blue-200 flex items-center justify-center gap-2">
登录
<span class="material-symbols-outlined text-lg">arrow_forward</span>
</button>
</form>
{% else %}
<form method="post" action="{{ url_for('main.register') }}" class="space-y-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label class="block text-[11px] tracking-[0.18em] uppercase text-slate-500 mb-2">用户名</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">badge</span>
<input name="username" required class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="设置用户名" />
</div>
</div>
<div>
<label class="block text-[11px] tracking-[0.18em] uppercase text-slate-500 mb-2">密码</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 text-lg">lock</span>
<input name="password" type="password" minlength="6" required class="w-full pl-11 pr-4 py-3.5 rounded-xl bg-[#eceff3] border border-transparent focus:border-primary focus:ring-0" placeholder="至少 6 位" />
</div>
</div>
<button class="w-full mt-2 rounded-xl bg-primary hover:bg-primaryDeep text-white font-bold py-4 shadow-lg shadow-blue-200 flex items-center justify-center gap-2">
注册
<span class="material-symbols-outlined text-lg">person_add</span>
</button>
</form>
{% endif %}
</div>
</div>
<footer class="border-t border-slate-200 bg-[#f8fafc] px-8 py-5">
<div class="flex justify-end gap-6 text-[11px] text-slate-500">
<span>系统状态</span>
<span>服务条款</span>
<span>API 文档</span>
</div>
</footer>
</section>
</div>
</body>
</html>
+226
View File
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>预测结果 </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 = {
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>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body { font-family: 'Inter', sans-serif; }
h1, h2, h3, h4 { font-family: 'Manrope', 'Inter', sans-serif; }
.dot-grid {
background-image: radial-gradient(circle at 1px 1px, rgba(148,163,184,.30) 1.2px, transparent 0);
background-size: 42px 42px;
}
.panel-frame {
box-shadow: none;
border: none;
}
.gradient-board {
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;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-page min-h-screen text-textMain">
<header class="sticky top-0 z-30 bg-white border-b border-line">
<div class="px-6 lg:px-10">
<div class="h-16 flex items-center justify-between gap-4">
<div class="flex items-center gap-10">
<div class="flex items-center gap-2 text-[18px] font-extrabold tracking-tight">
<span class="material-symbols-outlined text-primary">water_drop</span>
<span>供水管道健康评估系统</span>
</div>
<nav class="hidden md:flex items-center gap-7 text-[13px] font-semibold self-stretch">
<a href="{{ url_for('main.home') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">主页</a>
<a href="{{ url_for('main.result_page') }}" class="flex items-center text-primary border-b-2 border-primary">结果</a>
<a href="{{ url_for('main.history_page') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">历史</a>
{% if current_user.is_admin %}
<a href="{{ url_for('main.admin_dashboard') }}" class="flex items-center text-slate-500 hover:text-primary border-b-2 border-transparent">管理</a>
{% endif %}
</nav>
</div>
<div class="flex items-center gap-4 text-slate-600">
<span class="material-symbols-outlined">notifications</span>
<span class="material-symbols-outlined">help</span>
<div class="h-8 w-px bg-slate-200"></div>
<div class="flex items-center gap-3 text-sm font-semibold">
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-xs">{{ current_user.username[:1]|upper }}</div>
<form method="post" action="{{ url_for('main.logout') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="text-textMain" type="submit">退出登录</button>
</form>
</div>
</div>
</div>
</div>
</header>
<main class="px-6 lg:px-10 py-8">
{% if result %}
<div class="rounded-[14px] bg-blueSoft border border-blue-100 p-4 flex items-center justify-between gap-4 mb-8">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-xl bg-primary text-white flex items-center justify-center">
<span class="material-symbols-outlined">description</span>
</div>
<div>
<div class="font-bold">数据分析报告: <span class="text-primary font-mono">{{ result.original_filename }}</span></div>
<div class="text-xs text-textSub mt-1">基于上传数据生成的实时分析报告 • 生成于: {{ result.generated_at }}</div>
</div>
</div>
<div class="flex items-center gap-4 text-xs font-semibold">
<div class="text-successText flex items-center gap-1"><span class="material-symbols-outlined text-base">check_circle</span>数据源已验证</div>
<a href="{{ url_for('main.home') }}" class="text-primary">返回主页</a>
</div>
</div>
<div class="flex flex-col md:flex-row md:items-end justify-between gap-5 mb-6">
<h1 class="text-[64px] leading-none font-extrabold tracking-tight">预测结果</h1>
<div class="flex flex-wrap gap-3">
<a href="{{ url_for('main.home') }}" class="px-6 py-4 rounded-xl border border-slate-200 bg-white font-semibold flex items-center gap-2">
<span class="material-symbols-outlined">refresh</span>重新运行分析
</a>
<a href="{{ result.excel_url }}" class="px-6 py-4 rounded-xl bg-primary text-white font-semibold flex items-center gap-2 shadow-lg shadow-blue-200">
<span class="material-symbols-outlined">download</span>导出预测报告 (Excel)
</a>
</div>
</div>
<div class="grid lg:grid-cols-[1.08fr_.92fr] gap-6 items-start">
<section class="bg-[#fafafa] rounded-[18px] border border-slate-200 p-6 shadow-soft">
<div class="flex items-center justify-between mb-4">
<h2 class="text-[28px] font-extrabold">供水管道健康状态评估等级</h2>
<span class="px-3 py-1 rounded-full text-[11px] font-bold bg-slate-100 text-slate-500"></span>
</div>
<div class="grid grid-cols-2 xl:grid-cols-5 gap-3 mb-5 text-sm">
<div class="rounded-xl bg-dangerSoft p-3 border border-red-100"><div class="text-dangerText font-extrabold">I级</div><div class="text-xs mt-1">(0, 0.2]</div><div class="text-xs mt-1 font-semibold">管道安全风险十分严重,需立刻进行抢修或更新改造</div></div>
<div class="rounded-xl bg-orange-50 p-3 border border-orange-100"><div class="text-orange-600 font-extrabold">II级</div><div class="text-xs mt-1">(0.2, 0.4]</div><div class="text-xs mt-1 font-semibold">管道安全风险较为严重,需尽快安排检修及加频巡检</div></div>
<div class="rounded-xl bg-amber-50 p-3 border border-amber-100"><div class="text-amber-600 font-extrabold">III级</div><div class="text-xs mt-1">(0.4, 0.6]</div><div class="text-xs mt-1 font-semibold">管道安全风险较低,需安排定期巡检</div></div>
<div class="rounded-xl bg-blue-50 p-3 border border-blue-100"><div class="text-blue-600 font-extrabold">IV级</div><div class="text-xs mt-1">(0.6, 0.8]</div><div class="text-xs mt-1 font-semibold">管道安全风险较小,维持常规巡视</div></div>
<div class="rounded-xl bg-blueSoft p-3 border border-blue-100"><div class="text-primary font-extrabold">V级</div><div class="text-xs mt-1">(0.8, 1]</div><div class="text-xs mt-1 font-semibold">管道安全,维持常规巡视</div></div>
</div>
<div class="rounded-2xl bg-white border border-slate-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-[0.12em]">
<tr>
<th class="text-left px-5 py-4">管道编号</th>
<th class="text-left px-5 py-4">健康等级</th>
</tr>
</thead>
<tbody>
{% for item in result.summary_rows %}
<tr class="border-t border-slate-100">
<td class="px-5 py-4 font-semibold">{{ item.pipe_id }}</td>
<td class="px-5 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold {{ item.grade_class }}">{{ item.grade_label }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="px-5 py-4 text-xs text-textSub flex items-center justify-between">
<span>共 {{ result.sample_count }} 个样本,显示 {{ result.summary_rows|length }} 条</span>
<a href="{{ result.excel_url }}" class="text-primary font-semibold">查看完整样本列表</a>
</div>
</div>
</section>
<section class="bg-[#fafafa] rounded-[18px] border border-slate-200 p-6 shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 class="text-[28px] font-extrabold">管道剩余寿命动态评估</h2>
<p class="text-sm text-textSub mt-1">生存曲线拟合</p>
</div>
</div>
<div class="bg-white rounded-xl border border-slate-200 p-4">
<img src="{{ result.image_url }}" alt="生存概率阶梯图" class="w-full h-auto object-contain">
</div>
<div class="mt-5 bg-blueSoft rounded-xl border border-blue-100 p-4 flex items-start gap-3 text-sm text-textSub leading-6">
<span class="material-symbols-outlined text-primary mt-0.5">info</span>
<p><span class="font-bold text-textMain">分析说明:</span>{{ result.analysis_text }}</p>
</div>
</section>
</div>
{% if result.importance_url %}
<section class="bg-[#fafafa] rounded-[18px] border border-slate-200 p-6 shadow-soft mt-6">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 class="text-[28px] font-extrabold">模型输入因素重要性排序</h2>
<p class="text-sm text-textSub mt-1">各输入因素对预测结果的相对影响程度</p>
</div>
</div>
<div class="bg-white rounded-xl border border-slate-200 p-4">
<img src="{{ result.importance_url }}" alt="模型输入因素重要性排序图" class="w-full h-auto object-contain">
</div>
<div class="mt-5 bg-blueSoft rounded-xl border border-blue-100 p-4 flex items-start gap-3 text-sm text-textSub leading-6">
<span class="material-symbols-outlined text-primary mt-0.5">insights</span>
<p><span class="font-bold text-textMain">说明:</span>柱状图按重要性从高到低展示各输入因素对模型预测结果的相对贡献,数值为归一化占比,可用于辅助识别影响管道健康状态的关键因素。</p>
</div>
</section>
{% endif %}
{% else %}
<div class="rounded-2xl bg-white border border-slate-200 p-10 text-center text-textSub">当前还没有预测结果,请先从主页上传文件并运行预测。</div>
{% endif %}
</main>
<footer class="px-6 lg:px-10 py-6 border-t border-line text-[12px] text-slate-500 flex items-center justify-between">
<span>预测结果仅供参考</span>
<div class="flex gap-6"><span></span><span></span><span></span><span></span></div>
</footer>
</body>
</html>