优化页面
All checks were successful
Java Maven 3.9.9 & JDK 26 CI/CD Pipeline / build-and-deploy (push) Successful in 1m20s

This commit is contained in:
Lee 2026-06-08 23:37:55 +08:00
parent 172c2a098f
commit fe95fce777
3 changed files with 511 additions and 284 deletions

View File

@ -14,23 +14,27 @@
--safe-bottom: env(safe-area-inset-bottom, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px); --safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px); --safe-right: env(safe-area-inset-right, 0px);
--nav-height: 52px; --nav-height: 56px;
--color-primary: #6366f1; --color-primary: #6366f1;
--color-bg: #f1f5f9; --color-primary-light: #818cf8;
--color-primary-dark: #4f46e5;
--color-bg: #f8fafc;
--color-surface: #ffffff; --color-surface: #ffffff;
--color-text: #1e293b; --color-text: #1e293b;
--color-text-secondary: #64748b; --color-text-secondary: #64748b;
--color-text-muted: #94a3b8; --color-text-muted: #94a3b8;
--color-border: #e2e8f0; --color-border: #e2e8f0;
--radius-lg: 20px; --radius-xl: 24px;
--radius-lg: 18px;
--radius-md: 14px; --radius-md: 14px;
--radius-sm: 10px; --radius-sm: 10px;
--radius-full: 999px; --radius-full: 999px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06); --shadow-sm: 0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03);
--shadow-md: 0 4px 12px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04);
--font-xs: 13px; --font-xs: 13px;
--font-sm: 14px; --font-sm: 14px;
--font-base: 17px; --font-base: 16px;
--font-lg: 19px; --font-lg: 18px;
} }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@ -43,20 +47,30 @@
/* ==================== 导航 ==================== */ /* ==================== 导航 ==================== */
.navbar { .navbar {
background: var(--color-surface); border-bottom: 1px solid var(--color-border); background: rgba(255,255,255,0.78);
border-bottom: 1px solid rgba(226,232,240,0.6);
height: var(--nav-height); padding: 0 var(--safe-left) 0 var(--safe-right); height: var(--nav-height); padding: 0 var(--safe-left) 0 var(--safe-right);
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
position: sticky; top: 0; z-index: 100; position: sticky; top: 0; z-index: 100;
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(24px) saturate(1.4);
background: rgba(255,255,255,0.85); -webkit-backdrop-filter: blur(24px) saturate(1.4);
} }
.navbar .brand { font-size: 17px; font-weight: 700; color: var(--color-text); letter-spacing: -0.3px; padding-left: 20px; } .navbar .brand {
.navbar .nav-links { padding-right: 16px; display: flex; gap: 6px; } font-size: 18px; font-weight: 800; color: var(--color-text);
letter-spacing: -0.4px; padding-left: 20px;
display: flex; align-items: center; gap: 6px;
}
.navbar .brand-dot {
width: 8px; height: 8px; border-radius: 50%;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light));
}
.navbar .nav-links { padding-right: 16px; display: flex; gap: 4px; }
.navbar .nav-links a { .navbar .nav-links a {
color: var(--color-text-secondary); text-decoration: none; font-size: var(--font-sm); color: var(--color-text-secondary); text-decoration: none; font-size: var(--font-sm);
padding: 8px 14px; border-radius: var(--radius-sm); transition: all 0.2s; font-weight: 500; white-space: nowrap; padding: 8px 14px; border-radius: var(--radius-sm); transition: all 0.2s;
font-weight: 500; white-space: nowrap;
} }
.navbar .nav-links a:hover, .navbar .nav-links a:active { background: #f1f5f9; color: var(--color-primary); } .navbar .nav-links a:hover { background: rgba(99,102,241,0.06); color: var(--color-primary); }
/* ==================== 页面容器 ==================== */ /* ==================== 页面容器 ==================== */
.page-container { .page-container {
@ -64,87 +78,118 @@
padding: 16px 12px calc(24px + var(--safe-bottom)); padding: 16px 12px calc(24px + var(--safe-bottom));
} }
/* ==================== 筛选栏 — 紧凑 ==================== */ /* ==================== 筛选栏 ==================== */
.filter-bar { .filter-bar {
background: var(--color-surface); border-radius: var(--radius-md); background: var(--color-surface); border-radius: var(--radius-xl);
padding: 12px 14px; box-shadow: var(--shadow-sm); margin-bottom: 12px; padding: 18px 18px 14px;
box-shadow: var(--shadow-md);
border: 1px solid rgba(226,232,240,0.5);
margin-bottom: 14px;
animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
} }
.filter-section { margin-bottom: 10px; }
.filter-section:last-of-type { margin-bottom: 10px; } .filter-section { margin-bottom: 14px; }
.filter-section:last-of-type { margin-bottom: 14px; }
.filter-section-title { .filter-section-title {
font-size: 11px; font-weight: 700; color: var(--color-text-muted); font-size: 11px; font-weight: 700; color: var(--color-text-muted);
text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px;
padding-left: 2px;
} }
/* 日期输入 */ /* 日期输入 */
.date-row { .date-row {
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; margin-bottom: 2px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px;
} }
.date-row input[type="date"] { .date-row input[type="date"] {
border: 1.5px solid var(--color-border); border-radius: var(--radius-sm); border: 1.5px solid var(--color-border); border-radius: var(--radius-sm);
font-size: var(--font-sm); padding: 0 10px; transition: border-color 0.2s; font-size: var(--font-sm); padding: 0 12px;
background: #f8fafc; width: 100%; min-width: 0; height: 40px; min-height: 40px; transition: all 0.2s; background: #f8fafc;
-webkit-appearance: none; line-height: 40px; width: 100%; min-width: 0; height: 42px; min-height: 42px;
-webkit-appearance: none; line-height: 42px; color: var(--color-text);
} }
.date-row input[type="date"]:focus { .date-row input[type="date"]:focus {
border-color: var(--color-primary); outline: none; border-color: var(--color-primary); outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); background: white;
box-shadow: 0 0 0 3px rgba(99,102,241,0.08);
} }
/* Chip 多选标签组 */ /* Chip 多选标签组 */
.chip-group { .chip-group {
display: flex; flex-wrap: wrap; gap: 5px; display: flex; flex-wrap: wrap; gap: 6px;
max-height: 88px; overflow-y: auto; -webkit-overflow-scrolling: touch; max-height: 96px; overflow-y: auto; -webkit-overflow-scrolling: touch;
padding: 2px 0; padding: 2px 0;
} }
.chip-group::-webkit-scrollbar { width: 3px; }
.chip-group::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; }
.chip-group .chip { .chip-group .chip {
display: inline-flex; align-items: center; gap: 3px; display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: var(--radius-full); padding: 6px 12px; border-radius: var(--radius-full);
border: 1.5px solid var(--color-border); font-size: var(--font-xs); border: 1.5px solid var(--color-border); font-size: var(--font-xs);
cursor: pointer; user-select: none; -webkit-user-select: none; cursor: pointer; user-select: none; -webkit-user-select: none;
transition: all 0.15s; background: #fafbfc; color: var(--color-text-secondary); transition: all 0.2s ease; background: #fafbfc;
white-space: nowrap; min-height: 30px; color: var(--color-text-secondary); white-space: nowrap; min-height: 32px;
font-weight: 500;
} }
.chip-group .chip:active { transform: scale(0.96); } .chip-group .chip:active { transform: scale(0.95); }
.chip-group .chip input[type="checkbox"] { .chip-group .chip input[type="checkbox"] {
width: 14px; height: 14px; accent-color: var(--color-primary); margin: 0; flex-shrink: 0; width: 14px; height: 14px; accent-color: var(--color-primary); margin: 0; flex-shrink: 0;
} }
.chip-group .chip.selected { .chip-group .chip.selected {
background: #eef2ff; border-color: var(--color-primary); color: var(--color-primary); font-weight: 600; background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
} border-color: var(--color-primary); color: var(--color-primary); font-weight: 600;
.chip-group .chip-count { box-shadow: 0 2px 8px rgba(99,102,241,0.1);
font-size: 10px; color: var(--color-text-muted); margin-left: auto;
} }
/* 按钮行 */ /* 按钮行 */
.btn-group-row { .btn-group-row { display: flex; gap: 10px; margin-top: 16px; }
display: flex; gap: 8px; margin-top: 12px;
}
.btn-reset { .btn-reset {
background: var(--color-surface); color: var(--color-text-secondary); background: var(--color-surface); color: var(--color-text-secondary);
border: 1.5px solid var(--color-border); padding: 10px 18px; border-radius: var(--radius-sm); border: 1.5px solid var(--color-border); padding: 11px 20px; border-radius: var(--radius-sm);
font-weight: 500; font-size: var(--font-sm); transition: all 0.2s; font-weight: 600; font-size: var(--font-sm); transition: all 0.2s;
display: flex; align-items: center; justify-content: center; gap: 4px; white-space: nowrap; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px;
white-space: nowrap; cursor: pointer;
} }
.btn-reset:active { background: #f1f5f9; } .btn-reset:hover { border-color: var(--color-text-muted); color: var(--color-text); }
.btn-reset:active { background: #f1f5f9; transform: scale(0.98); }
.btn-search { .btn-search {
flex: 1; background: var(--color-primary); color: white; border: none; flex: 1; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
padding: 10px 20px; border-radius: var(--radius-sm); font-weight: 600; font-size: var(--font-sm); color: white; border: none; padding: 11px 20px; border-radius: var(--radius-sm);
transition: background 0.2s, transform 0.1s; cursor: pointer; font-weight: 700; font-size: var(--font-sm);
display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 5px;
box-shadow: 0 4px 14px rgba(99,102,241,0.25);
position: relative; overflow: hidden;
} }
.btn-search:active { background: #4f46e5; transform: scale(0.98); } .btn-search::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, transparent 0%, rgba(255,255,255,0.12) 100%);
pointer-events: none;
}
.btn-search:hover { box-shadow: 0 6px 20px rgba(99,102,241,0.35); transform: translateY(-1px); }
.btn-search:active { transform: translateY(0) scale(0.98); box-shadow: 0 2px 8px rgba(99,102,241,0.25); }
/* ==================== 汇总栏 ==================== */ /* ==================== 汇总栏 ==================== */
.summary-bar { .summary-bar {
background: var(--color-surface); border-radius: var(--radius-sm); background: var(--color-surface); border-radius: var(--radius-lg);
padding: 10px 16px; margin-bottom: 10px; display: none; padding: 14px 18px; margin-bottom: 12px; display: none;
align-items: center; justify-content: space-between; align-items: center; justify-content: space-between;
box-shadow: var(--shadow-sm); animation: fadeInUp 0.3s ease; box-shadow: var(--shadow-sm);
border: 1px solid rgba(226,232,240,0.5);
animation: fadeInUp 0.35s cubic-bezier(0.16, 1, 0.3, 1);
} }
.summary-bar.visible { display: flex; } .summary-bar.visible { display: flex; }
.summary-amount { font-size: var(--font-base); font-weight: 700; color: var(--color-primary); } .summary-amount {
.summary-count { font-size: var(--font-xs); color: var(--color-text-secondary); font-weight: 500; } font-size: var(--font-lg); font-weight: 800;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.summary-count {
font-size: var(--font-xs); color: var(--color-text-secondary); font-weight: 500;
background: #f1f5f9; padding: 4px 10px; border-radius: var(--radius-full);
}
@keyframes fadeInUp { @keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); } from { opacity: 0; transform: translateY(8px); }
@ -153,40 +198,79 @@
/* ==================== 表格 ==================== */ /* ==================== 表格 ==================== */
.table-card { .table-card {
background: var(--color-surface); border-radius: var(--radius-md); background: var(--color-surface); border-radius: var(--radius-xl);
overflow: hidden; box-shadow: var(--shadow-sm); overflow: hidden; box-shadow: var(--shadow-md);
border: 1px solid rgba(226,232,240,0.5);
animation: fadeInUp 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both;
} }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; } .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-card table { width: 100%; min-width: 500px; margin: 0; border-collapse: collapse; } .table-card table { width: 100%; min-width: 520px; margin: 0; border-collapse: collapse; }
.table-card table thead th { .table-card table thead th {
background: #f8fafc; font-size: 11px; font-weight: 700; color: var(--color-text-secondary); background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
text-transform: uppercase; letter-spacing: 0.4px; padding: 4px 6px; font-size: 11px; font-weight: 700; color: var(--color-text-secondary);
border-bottom: 2px solid var(--color-border); white-space: nowrap; position: sticky; top: 0; text-transform: uppercase; letter-spacing: 0.6px; padding: 12px 14px;
border-bottom: 2px solid var(--color-border); white-space: nowrap;
position: sticky; top: 0;
} }
.table-card table tbody td { .table-card table thead th:first-child { padding-left: 18px; }
padding: 3px 6px; border-bottom: 1px solid #f1f5f9; .table-card table thead th:last-child { padding-right: 18px; text-align: right; }
font-size: var(--font-xs); color: var(--color-text); vertical-align: middle;
}
.table-card table tbody tr:last-child td { border-bottom: none; }
.table-card table tbody tr:active { background: #f8fafc; }
.category-badge {
display: inline-block; background: #eef2ff; color: var(--color-primary);
padding: 2px 8px; border-radius: var(--radius-full); font-size: 11px; font-weight: 600; white-space: nowrap;
}
.amount-cell { font-weight: 600; color: #ef4444; text-align: right; font-variant-numeric: tabular-nums; }
.empty-msg { text-align: center; padding: 40px 20px; color: var(--color-text-muted); font-size: var(--font-sm); } .table-card table tbody td {
.loading-bar { text-align: center; padding: 36px 20px; display: none; } padding: 12px 14px; border-bottom: 1px solid #f1f5f9;
font-size: var(--font-sm); color: var(--color-text); vertical-align: middle;
transition: background 0.15s;
}
.table-card table tbody td:first-child { padding-left: 18px; font-weight: 500; }
.table-card table tbody td:last-child { padding-right: 18px; }
.table-card table tbody tr:last-child td { border-bottom: none; }
.table-card table tbody tr:hover { background: #f8fafc; }
.table-card table tbody tr:active { background: #f1f5f9; }
.category-badge {
display: inline-block;
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
color: var(--color-primary);
padding: 3px 10px; border-radius: var(--radius-full);
font-size: 11px; font-weight: 600; white-space: nowrap;
}
.amount-cell {
font-weight: 700; color: #ef4444; text-align: right;
font-variant-numeric: tabular-nums; font-size: var(--font-sm);
}
.empty-msg {
text-align: center; padding: 48px 20px; color: var(--color-text-muted);
font-size: var(--font-sm); display: flex; flex-direction: column;
align-items: center; gap: 8px;
}
.empty-msg i { font-size: 32px; opacity: 0.4; }
.loading-bar {
text-align: center; padding: 40px 20px; display: none;
}
.loading-bar.show { display: block; } .loading-bar.show { display: block; }
.loading-bar .spinner-border { width: 28px; height: 28px; border-width: 3px; color: var(--color-primary); } .loading-bar .spinner-border {
width: 28px; height: 28px; border-width: 3px; color: var(--color-primary);
}
.loading-bar p {
margin-top: 12px; color: var(--color-text-muted); font-size: var(--font-sm);
}
/* ==================== 响应式 ==================== */
@media (min-width: 768px) {
.page-container { padding: 24px 20px 32px; }
.filter-bar { padding: 20px 22px 16px; }
.table-card table thead th { padding: 14px 16px; }
.table-card table tbody td { padding: 14px 16px; }
}
</style> </style>
</head> </head>
<body> <body>
<nav class="navbar"> <nav class="navbar">
<span class="brand">📝 TraceCD</span> <span class="brand"><span class="brand-dot"></span> TraceCD</span>
<div class="nav-links"> <div class="nav-links">
<a href="/">🎤 语音</a> <a href="/"><i class="bi bi-mic"></i> 语音</a>
<a href="/logout">退出</a> <a href="/logout">退出</a>
</div> </div>
</nav> </nav>
@ -217,7 +301,7 @@
<input type="date" id="filterEndTime" placeholder="结束"> <input type="date" id="filterEndTime" placeholder="结束">
</div> </div>
</div> </div>
<!-- 按钮:重置在左,查询在右 --> <!-- 按钮 -->
<div class="btn-group-row"> <div class="btn-group-row">
<button class="btn-reset" onclick="resetFilters()"> <button class="btn-reset" onclick="resetFilters()">
<i class="bi bi-arrow-clockwise"></i> 重置 <i class="bi bi-arrow-clockwise"></i> 重置
@ -232,7 +316,7 @@
<div class="summary-bar" id="summaryBar"> <div class="summary-bar" id="summaryBar">
<span class="summary-count" id="summaryCount"></span> <span class="summary-count" id="summaryCount"></span>
<div> <div>
<span style="color:var(--color-text-secondary);font-size:var(--font-xs);">合计 </span> <span style="color:var(--color-text-secondary);font-size:var(--font-xs);margin-right:4px;">合计</span>
<span class="summary-amount" id="summaryAmount"></span> <span class="summary-amount" id="summaryAmount"></span>
</div> </div>
</div> </div>
@ -241,7 +325,7 @@
<div class="table-card"> <div class="table-card">
<div class="loading-bar" id="loadingBar"> <div class="loading-bar" id="loadingBar">
<div class="spinner-border" role="status"></div> <div class="spinner-border" role="status"></div>
<p style="margin-top:10px;color:var(--color-text-muted);font-size:var(--font-sm);">查询中...</p> <p>查询中...</p>
</div> </div>
<div class="table-scroll"> <div class="table-scroll">
<table id="resultTable" style="display:none;"> <table id="resultTable" style="display:none;">
@ -253,7 +337,10 @@
<tbody id="resultTbody"></tbody> <tbody id="resultTbody"></tbody>
</table> </table>
</div> </div>
<div class="empty-msg" id="emptyMsg">点击「查询」查看事项数据</div> <div class="empty-msg" id="emptyMsg">
<i class="bi bi-inbox"></i>
<span>点击「查询」查看事项数据</span>
</div>
</div> </div>
</div> </div>
@ -261,7 +348,6 @@
// ==================== 初始化 Chip 选择器 ==================== // ==================== 初始化 Chip 选择器 ====================
document.addEventListener('DOMContentLoaded', loadFilterOptions); document.addEventListener('DOMContentLoaded', loadFilterOptions);
// 存储每个 filter 组的 chip 容器 ID 映射
const chipMap = { const chipMap = {
person: 'chipPerson', person: 'chipPerson',
location: 'chipLocation', location: 'chipLocation',
@ -283,7 +369,6 @@
const label = document.createElement('label'); const label = document.createElement('label');
label.className = 'chip'; label.className = 'chip';
label.innerHTML = '<input type="checkbox" value="' + escAttr(v) + '"> ' + escHtml(v); label.innerHTML = '<input type="checkbox" value="' + escAttr(v) + '"> ' + escHtml(v);
// 点击 label 切换 selected 样式
label.addEventListener('change', function(e) { label.addEventListener('change', function(e) {
if (e.target.checked) { if (e.target.checked) {
label.classList.add('selected'); label.classList.add('selected');
@ -331,8 +416,8 @@
renderResults(data); renderResults(data);
} catch (err) { } catch (err) {
console.error('查询失败:', err); console.error('查询失败:', err);
emptyMsg.textContent = '查询失败,请重试'; emptyMsg.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>查询失败,请重试</span>';
emptyMsg.style.display = 'block'; emptyMsg.style.display = 'flex';
} finally { } finally {
loadingBar.classList.remove('show'); loadingBar.classList.remove('show');
} }
@ -352,8 +437,8 @@
if (!data.records || data.records.length === 0) { if (!data.records || data.records.length === 0) {
resultTable.style.display = 'none'; resultTable.style.display = 'none';
emptyMsg.textContent = '未找到匹配的事项记录'; emptyMsg.innerHTML = '<i class="bi bi-inbox"></i><span>未找到匹配的事项记录</span>';
emptyMsg.style.display = 'block'; emptyMsg.style.display = 'flex';
summaryBar.classList.remove('visible'); summaryBar.classList.remove('visible');
return; return;
} }
@ -381,7 +466,6 @@
} }
function resetFilters() { function resetFilters() {
// 取消所有 chip 勾选
['chipPerson', 'chipLocation', 'chipCategory'].forEach(id => { ['chipPerson', 'chipLocation', 'chipCategory'].forEach(id => {
const container = document.getElementById(id); const container = document.getElementById(id);
if (!container) return; if (!container) return;
@ -394,8 +478,9 @@
document.getElementById('filterEndTime').value = ''; document.getElementById('filterEndTime').value = '';
document.getElementById('resultTable').style.display = 'none'; document.getElementById('resultTable').style.display = 'none';
document.getElementById('emptyMsg').textContent = '点击「查询」查看事项数据'; const emptyMsg = document.getElementById('emptyMsg');
document.getElementById('emptyMsg').style.display = 'block'; emptyMsg.innerHTML = '<i class="bi bi-inbox"></i><span>点击「查询」查看事项数据</span>';
emptyMsg.style.display = 'flex';
document.getElementById('summaryBar').classList.remove('visible'); document.getElementById('summaryBar').classList.remove('visible');
} }

View File

@ -14,25 +14,28 @@
--safe-bottom: env(safe-area-inset-bottom, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px); --safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px); --safe-right: env(safe-area-inset-right, 0px);
--mic-size: clamp(72px, 20vw, 92px); --mic-size: clamp(72px, 20vw, 88px);
--mic-color: #6366f1; --mic-color: #6366f1;
--mic-color-light: #818cf8;
--mic-recording: #ef4444; --mic-recording: #ef4444;
--nav-height: 52px; --nav-height: 56px;
--color-bg: #f1f5f9; --color-bg: #f8fafc;
--color-surface: #ffffff; --color-surface: #ffffff;
--color-text: #1e293b; --color-text: #1e293b;
--color-text-secondary: #64748b; --color-text-secondary: #64748b;
--color-text-muted: #94a3b8; --color-text-muted: #94a3b8;
--color-border: #e2e8f0; --color-border: #e2e8f0;
--radius-lg: 20px; --radius-xl: 24px;
--radius-lg: 18px;
--radius-md: 14px; --radius-md: 14px;
--radius-sm: 10px; --radius-sm: 10px;
--radius-full: 999px; --radius-full: 999px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06); --shadow-sm: 0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03);
--font-xs: 14px; --shadow-md: 0 4px 12px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04);
--font-sm: 15px; --font-xs: 13px;
--font-base: 18px; --font-sm: 14px;
--font-lg: 20px; --font-base: 16px;
--font-lg: 18px;
} }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@ -45,101 +48,143 @@
/* ==================== 顶部导航 ==================== */ /* ==================== 顶部导航 ==================== */
.navbar { .navbar {
background: rgba(255,255,255,0.85); border-bottom: 1px solid var(--color-border); background: rgba(255,255,255,0.78);
border-bottom: 1px solid rgba(226,232,240,0.6);
height: var(--nav-height); padding: 0 var(--safe-left) 0 var(--safe-right); height: var(--nav-height); padding: 0 var(--safe-left) 0 var(--safe-right);
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
position: sticky; top: 0; z-index: 100; position: sticky; top: 0; z-index: 100;
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
} }
.navbar .brand { font-size: 18px; font-weight: 700; color: var(--color-text); letter-spacing: -0.3px; padding-left: 20px; } .navbar .brand {
.navbar .nav-links { padding-right: 16px; display: flex; gap: 6px; } font-size: 18px; font-weight: 800; color: var(--color-text);
letter-spacing: -0.4px; padding-left: 20px;
display: flex; align-items: center; gap: 6px;
}
.navbar .brand-dot {
width: 8px; height: 8px; border-radius: 50%;
background: linear-gradient(135deg, var(--mic-color), var(--mic-color-light));
}
.navbar .nav-links { padding-right: 16px; display: flex; gap: 4px; }
.navbar .nav-links a { .navbar .nav-links a {
color: var(--color-text-secondary); text-decoration: none; font-size: var(--font-sm); color: var(--color-text-secondary); text-decoration: none; font-size: var(--font-sm);
padding: 8px 14px; border-radius: var(--radius-sm); transition: all 0.2s; font-weight: 500; white-space: nowrap; padding: 8px 14px; border-radius: var(--radius-sm); transition: all 0.2s;
font-weight: 500; white-space: nowrap;
} }
.navbar .nav-links a:hover, .navbar .nav-links a:active { background: #f1f5f9; color: var(--mic-color); } .navbar .nav-links a:hover { background: rgba(99,102,241,0.06); color: var(--mic-color); }
/* ==================== 通知栏 ==================== */ /* ==================== 通知栏 ==================== */
.notification { .notification {
position: fixed; top: calc(var(--nav-height) + 12px + var(--safe-top)); left: 50%; position: fixed; top: calc(var(--nav-height) + 12px + var(--safe-top)); left: 50%;
transform: translateX(-50%) translateY(-8px); z-index: 9999; transform: translateX(-50%) translateY(-8px); z-index: 9999;
background: #10b981; color: white; padding: 10px 20px; border-radius: var(--radius-full); background: var(--color-surface); color: #059669;
padding: 10px 20px; border-radius: var(--radius-full);
font-size: var(--font-sm); font-weight: 600; font-size: var(--font-sm); font-weight: 600;
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.35); box-shadow: 0 8px 24px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04);
border: 1px solid rgba(16,185,129,0.2);
opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease; opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease;
max-width: calc(100vw - 40px); text-align: center; max-width: calc(100vw - 40px); text-align: center;
display: flex; align-items: center; gap: 8px;
} }
.notification.show { opacity: 1; transform: translateX(-50%) translateY(0); } .notification.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.notification.error { background: #ef4444; box-shadow: 0 4px 16px rgba(239, 68, 68, 0.35); } .notification.error {
color: #dc2626; border-color: rgba(239,68,68,0.2);
}
/* ==================== 内容区 ==================== */ /* ==================== 内容区 ==================== */
.content-area { .content-area {
max-width: 640px; margin: 0 auto; max-width: 640px; margin: 0 auto;
padding: 10px 8px calc(var(--mic-size) + 20px + var(--safe-bottom) + 40px); padding: 16px 12px calc(var(--mic-size) + 24px + var(--safe-bottom) + 48px);
} }
/* ==================== 空状态 ==================== */ /* ==================== 空状态 ==================== */
.empty-state { text-align: center; padding: 40px 16px 24px; color: var(--color-text-muted); } .empty-state {
.empty-state .empty-icon { font-size: 52px; margin-bottom: 14px; display: block; opacity: 0.6; } text-align: center; padding: 48px 20px 32px;
.empty-state .empty-title { font-size: var(--font-lg); font-weight: 600; color: var(--color-text); margin-bottom: 6px; } animation: fadeInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1);
.empty-state .empty-desc { font-size: var(--font-sm); color: var(--color-text-muted); line-height: 1.4; } }
.empty-icon-wrap {
display: inline-flex; align-items: center; justify-content: center;
width: 80px; height: 80px; border-radius: 24px;
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 50%, #ede9fe 100%);
box-shadow: 0 4px 16px rgba(99,102,241,0.12);
margin-bottom: 20px; font-size: 36px;
}
.empty-state .empty-title {
font-size: var(--font-lg); font-weight: 700; color: var(--color-text);
margin-bottom: 8px; letter-spacing: -0.3px;
}
.empty-state .empty-desc {
font-size: var(--font-sm); color: var(--color-text-muted); line-height: 1.6;
}
/* ==================== 查询结果 ==================== */ /* ==================== 查询结果 ==================== */
.result-card { .result-card {
background: transparent; border-radius: 0; background: transparent; border-radius: 0;
padding: 0; box-shadow: none; padding: 0; box-shadow: none;
display: none; animation: fadeInUp 0.35s ease; display: none; animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
} }
.result-card.visible { display: block; } .result-card.visible { display: block; }
/* 卡片列表 */ /* 卡片列表 */
.records-list { .records-list { display: flex; flex-direction: column; gap: 8px; }
display: -webkit-flex; display: flex;
-webkit-flex-direction: column; flex-direction: column;
gap: 5px;
}
.record-card { .record-card {
border-radius: 12px; padding: 10px 14px; background: var(--color-surface); border-radius: var(--radius-lg);
display: -webkit-flex; display: flex; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px;
-webkit-flex-direction: column; flex-direction: column;
gap: 2px;
border-left: 4px solid #e2e8f0; border-left: 4px solid #e2e8f0;
box-shadow: 0 1px 2px rgba(0,0,0,0.03); box-shadow: var(--shadow-sm);
-webkit-tap-highlight-color: rgba(0,0,0,0.05); transition: all 0.2s ease;
animation: cardSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
} }
.record-card:active { opacity: 0.85; } .record-card:nth-child(1) { animation-delay: 0s; }
.record-card:nth-child(2) { animation-delay: 0.05s; }
.record-card:nth-child(3) { animation-delay: 0.1s; }
.record-card:nth-child(4) { animation-delay: 0.15s; }
.record-card:nth-child(5) { animation-delay: 0.2s; }
@keyframes cardSlideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.record-card:active { transform: scale(0.99); opacity: 0.9; }
.card-top { .card-top {
display: -webkit-flex; display: flex; display: flex; justify-content: space-between; align-items: baseline;
-webkit-justify-content: space-between; justify-content: space-between; }
-webkit-align-items: baseline; align-items: baseline; .card-time {
font-size: 14px; color: var(--color-text-secondary); font-weight: 500;
display: flex; align-items: center; gap: 6px;
}
.card-time .cat-emoji { font-size: 18px; }
.card-amount {
font-size: 20px; font-weight: 800; color: #ef4444; white-space: nowrap;
font-variant-numeric: tabular-nums; letter-spacing: -0.3px;
} }
.card-time { font-size: 16px; color: #64748b; font-weight: 500; }
.card-time .cat-emoji { margin-right: 4px; font-size: 18px; }
.card-amount { font-size: 20px; font-weight: 700; color: #ef4444; white-space: nowrap; }
.card-detail { .card-detail {
font-size: 15px; color: #334155; line-height: 1.5; font-size: 14px; color: #334155; line-height: 1.5;
display: -webkit-flex; display: flex; display: flex; flex-wrap: wrap; gap: 4px 10px;
-webkit-flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 8px;
} }
.card-detail .meta-tag { .card-detail .meta-tag {
font-size: 13px; color: #94a3b8; white-space: nowrap; font-size: 12px; color: var(--color-text-muted);
background: #f1f5f9; padding: 2px 8px; border-radius: var(--radius-full);
} }
/* 底部总结 */ /* 底部总结 */
.summary-footer { .summary-footer {
margin-top: 10px; padding: 14px 16px; margin-top: 14px; padding: 16px 20px;
background: #eef2ff; border-radius: 12px; background: linear-gradient(135deg, #eef2ff 0%, #ede9fe 100%);
text-align: center; font-size: 16px; font-weight: 600; border: 1px solid rgba(99,102,241,0.1);
color: #4f46e5; line-height: 1.5; border-radius: var(--radius-lg);
text-align: center; font-size: 15px; font-weight: 600;
color: #4f46e5; line-height: 1.6;
animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both;
} }
/* ==================== 聊天回复 ==================== */ /* ==================== 聊天回复 ==================== */
.chat-reply { .chat-reply {
background: var(--color-surface); border-radius: var(--radius-sm); background: var(--color-surface); border-radius: var(--radius-lg);
padding: 12px 12px; box-shadow: var(--shadow-sm); padding: 16px 18px; box-shadow: var(--shadow-md);
font-size: var(--font-sm); line-height: 1.6; color: var(--color-text); font-size: var(--font-sm); line-height: 1.7; color: var(--color-text);
display: none; animation: fadeInUp 0.35s ease; display: none; animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
border: 1px solid rgba(226,232,240,0.5);
} }
.chat-reply.visible { display: block; } .chat-reply.visible { display: block; }
.chat-reply p { margin: 0; } .chat-reply p { margin: 0; }
@ -152,61 +197,80 @@
/* ==================== 麦克风按钮区域 ==================== */ /* ==================== 麦克风按钮区域 ==================== */
.mic-bottom-bar { .mic-bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;
background: linear-gradient(180deg, transparent 0%, var(--color-bg) 50%, var(--color-bg) 100%); background: linear-gradient(180deg, transparent 0%, var(--color-bg) 40%, var(--color-bg) 100%);
padding: 16px 0 calc(24px + var(--safe-bottom)); padding: 20px 0 calc(28px + var(--safe-bottom));
display: flex; flex-direction: column; align-items: center; pointer-events: none; display: flex; flex-direction: column; align-items: center; pointer-events: none;
} }
.mic-container { position: relative; pointer-events: auto; } .mic-container { position: relative; pointer-events: auto; }
.mic-btn { .mic-btn {
width: var(--mic-size); height: var(--mic-size); border-radius: 50%; width: var(--mic-size); height: var(--mic-size); border-radius: 50%;
background: var(--mic-color); border: none; color: white; background: linear-gradient(135deg, var(--mic-color) 0%, var(--mic-color-light) 100%);
font-size: calc(var(--mic-size) * 0.4); cursor: pointer; border: none; color: white;
box-shadow: 0 4px 24px rgba(99, 102, 241, 0.4); font-size: calc(var(--mic-size) * 0.38); cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease; box-shadow:
0 6px 24px rgba(99,102,241,0.35),
0 2px 8px rgba(99,102,241,0.2),
inset 0 1px 0 rgba(255,255,255,0.2);
transition: transform 0.15s ease, box-shadow 0.15s ease;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; position: relative; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
position: relative;
}
.mic-btn::before {
content: ''; position: absolute; inset: -6px; border-radius: 50%;
background: rgba(99,102,241,0.08); z-index: -1;
transition: all 0.3s ease;
}
.mic-btn:active {
transform: scale(0.92);
box-shadow: 0 3px 12px rgba(99,102,241,0.3), inset 0 1px 0 rgba(255,255,255,0.2);
} }
.mic-btn:active { transform: scale(0.94); box-shadow: 0 2px 12px rgba(99, 102, 241, 0.3); transition: transform 0.08s ease; }
.mic-btn.recording { .mic-btn.recording {
background: var(--mic-recording); box-shadow: 0 4px 28px rgba(239, 68, 68, 0.5); background: linear-gradient(135deg, var(--mic-recording) 0%, #f87171 100%);
box-shadow:
0 6px 28px rgba(239,68,68,0.45),
0 2px 8px rgba(239,68,68,0.25);
animation: micPulse 1.6s ease-in-out infinite; animation: micPulse 1.6s ease-in-out infinite;
} }
.mic-btn.recording::before { background: rgba(239,68,68,0.1); }
@keyframes micPulse { @keyframes micPulse {
0%, 100% { box-shadow: 0 4px 28px rgba(239, 68, 68, 0.5); } 0%, 100% { box-shadow: 0 6px 28px rgba(239,68,68,0.45), 0 2px 8px rgba(239,68,68,0.25); }
50% { box-shadow: 0 4px 28px rgba(239, 68, 68, 0.25), 0 0 0 16px rgba(239, 68, 68, 0.08); } 50% { box-shadow: 0 6px 28px rgba(239,68,68,0.2), 0 0 0 14px rgba(239,68,68,0.06); }
} }
/* 底部状态行:提示文字 + 处理中指示器 */ /* 底部状态行 */
.mic-status-row { .mic-status-row {
display: flex; align-items: center; gap: 10px; margin-top: 10px; display: flex; align-items: center; gap: 10px; margin-top: 12px;
pointer-events: none; user-select: none; -webkit-user-select: none; pointer-events: none; user-select: none; -webkit-user-select: none;
} }
.mic-hint { .mic-hint {
font-size: var(--font-xs); color: var(--color-text-muted); font-weight: 500; font-size: var(--font-xs); color: var(--color-text-muted); font-weight: 500;
} }
.mic-hint.recording-hint { color: var(--mic-recording); font-weight: 600; animation: hintBlink 1.2s ease-in-out infinite; } .mic-hint.recording-hint {
color: var(--mic-recording); font-weight: 600;
animation: hintBlink 1.2s ease-in-out infinite;
}
@keyframes hintBlink { @keyframes hintBlink {
0%, 100% { opacity: 1; } 50% { opacity: 0.5; } 0%, 100% { opacity: 1; } 50% { opacity: 0.4; }
} }
/* 处理中指示器 */ /* 处理中指示器 */
.processing-indicator { .processing-indicator { display: none; align-items: center; gap: 6px; }
display: none; align-items: center; gap: 4px;
}
.processing-indicator.show { display: flex; } .processing-indicator.show { display: flex; }
.processing-dot { .processing-dot {
width: 6px; height: 6px; border-radius: 50%; background: var(--mic-color); width: 5px; height: 5px; border-radius: 50%; background: var(--mic-color);
animation: dotBounce 1.2s ease-in-out infinite; animation: dotBounce 1.2s ease-in-out infinite;
} }
.processing-dot:nth-child(2) { animation-delay: 0.15s; } .processing-dot:nth-child(2) { animation-delay: 0.15s; }
.processing-dot:nth-child(3) { animation-delay: 0.3s; } .processing-dot:nth-child(3) { animation-delay: 0.3s; }
@keyframes dotBounce { @keyframes dotBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } 0%, 80%, 100% { transform: scale(0.5); opacity: 0.3; }
40% { transform: scale(1); opacity: 1; } 40% { transform: scale(1); opacity: 1; }
} }
.processing-text { font-size: var(--font-xs); color: var(--mic-color); font-weight: 500; } .processing-text { font-size: var(--font-xs); color: var(--mic-color); font-weight: 600; }
@media (min-width: 768px) { @media (min-width: 768px) {
.content-area { padding: 32px 20px 120px; } .content-area { padding: 28px 20px 120px; }
.navbar .brand { padding-left: 24px; } .navbar .brand { padding-left: 24px; }
} }
</style> </style>
@ -214,9 +278,9 @@
<body> <body>
<nav class="navbar"> <nav class="navbar">
<span class="brand">📝 TraceCD</span> <span class="brand"><span class="brand-dot"></span> TraceCD</span>
<div class="nav-links"> <div class="nav-links">
<a href="/browse">📋 浏览</a> <a href="/browse"><i class="bi bi-grid-3x3-gap"></i> 浏览</a>
<a href="/logout">退出</a> <a href="/logout">退出</a>
</div> </div>
</nav> </nav>
@ -225,7 +289,7 @@
<div class="content-area" id="contentArea"> <div class="content-area" id="contentArea">
<div class="empty-state" id="emptyState"> <div class="empty-state" id="emptyState">
<span class="empty-icon">🎤</span> <div class="empty-icon-wrap">🎤</div>
<div class="empty-title">语音记账助手</div> <div class="empty-title">语音记账助手</div>
<div class="empty-desc">按住下方按钮说话<br>录入事项或查询记录</div> <div class="empty-desc">按住下方按钮说话<br>录入事项或查询记录</div>
</div> </div>
@ -252,21 +316,18 @@
// ==================== 麦克风权限预检 ==================== // ==================== 麦克风权限预检 ====================
(async function checkMicPermission() { (async function checkMicPermission() {
try { try {
// 先检查当前权限状态
if (navigator.permissions) { if (navigator.permissions) {
const status = await navigator.permissions.query({ name: 'microphone' }); const status = await navigator.permissions.query({ name: 'microphone' });
if (status.state === 'granted') return; // 已有权限 if (status.state === 'granted') return;
if (status.state === 'denied') { if (status.state === 'denied') {
showNotification('麦克风权限已被拒绝,请在系统设置中开启', true); showNotification('麦克风权限已被拒绝,请在系统设置中开启', true);
return; return;
} }
} }
// 请求权限
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(t => t.stop()); stream.getTracks().forEach(t => t.stop());
} catch (err) { } catch (err) {
console.log('麦克风权限预检:', err.message); console.log('麦克风权限预检:', err.message);
// 不强制弹通知,用户使用时还会再次触发权限请求
} }
})(); })();
@ -379,7 +440,6 @@
hapticFeedback('warning'); hapticFeedback('warning');
return; return;
} }
// 显示底部处理中指示器(不阻塞操作)
processing.classList.add('show'); processing.classList.add('show');
micHint.style.display = 'none'; micHint.style.display = 'none';
await sendAudio(wavBlob); await sendAudio(wavBlob);
@ -552,7 +612,8 @@
let notifyTimer = null; let notifyTimer = null;
function showNotification(message, isError) { function showNotification(message, isError) {
if (notifyTimer) clearTimeout(notifyTimer); if (notifyTimer) clearTimeout(notifyTimer);
notification.textContent = message; const icon = isError ? '✕' : '✓';
notification.innerHTML = '<span style="font-weight:800;margin-right:2px;">' + icon + '</span> ' + escapeHtml(message);
notification.className = 'notification' + (isError ? ' error' : ''); notification.className = 'notification' + (isError ? ' error' : '');
void notification.offsetWidth; void notification.offsetWidth;
notification.classList.add('show'); notification.classList.add('show');

View File

@ -7,21 +7,24 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>登录 - TraceCD</title> <title>登录 - TraceCD</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
:root { :root {
--safe-top: env(safe-area-inset-top, 0px); --safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px);
--color-primary: #6366f1; --color-primary: #6366f1;
--color-bg: #f1f5f9; --color-primary-dark: #4f46e5;
--color-surface: #ffffff; --color-primary-light: #818cf8;
--color-bg: #f8fafc;
--color-surface: rgba(255, 255, 255, 0.72);
--color-text: #1e293b; --color-text: #1e293b;
--color-text-secondary: #64748b; --color-text-secondary: #64748b;
--color-text-muted: #94a3b8; --color-text-muted: #94a3b8;
--color-border: #e2e8f0; --color-border: rgba(226, 232, 240, 0.6);
--radius-lg: 24px; --radius-xl: 28px;
--radius-sm: 12px; --radius-lg: 18px;
--font-sm: 15px; --radius-md: 14px;
--font-base: 17px; --radius-sm: 10px;
} }
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@ -29,137 +32,215 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--color-bg); background: var(--color-bg);
min-height: 100vh; min-height: 100vh; min-height: 100dvh;
min-height: 100dvh; margin: 0; display: flex; align-items: center; justify-content: center;
margin: 0; padding: 24px; -webkit-font-smoothing: antialiased;
display: flex; overflow: hidden; position: relative;
align-items: center;
justify-content: center;
padding: 24px;
-webkit-font-smoothing: antialiased;
} }
/* 顶部装饰 */ /* ==================== 渐变背景 ==================== */
body::before { .bg-gradient {
content: ''; position: fixed; inset: 0; z-index: -2;
position: fixed; background:
top: 0; radial-gradient(ellipse 80% 60% at 20% 10%, rgba(99,102,241,0.12) 0%, transparent 60%),
left: 0; radial-gradient(ellipse 60% 50% at 80% 20%, rgba(168,85,247,0.10) 0%, transparent 50%),
right: 0; radial-gradient(ellipse 70% 40% at 50% 90%, rgba(59,130,246,0.08) 0%, transparent 50%),
height: 40vh; linear-gradient(160deg, #f0f4ff 0%, #f8fafc 40%, #faf5ff 70%, #f0f9ff 100%);
background: linear-gradient(180deg, #eef2ff 0%, #e0e7ff 40%, var(--color-bg) 100%);
z-index: -1;
} }
/* 浮动光斑 */
.bg-orb {
position: fixed; border-radius: 50%; filter: blur(80px); z-index: -1;
animation: orbFloat 20s ease-in-out infinite;
}
.bg-orb-1 { width: 400px; height: 400px; top: -100px; left: -80px; background: rgba(99,102,241,0.12); }
.bg-orb-2 { width: 300px; height: 300px; bottom: -60px; right: -60px; background: rgba(168,85,247,0.10); animation-delay: -7s; }
.bg-orb-3 { width: 250px; height: 250px; top: 40%; left: 60%; background: rgba(59,130,246,0.08); animation-delay: -14s; }
@keyframes orbFloat {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -20px) scale(1.05); }
66% { transform: translate(-20px, 15px) scale(0.95); }
}
/* ==================== 登录卡片 ==================== */
.login-card { .login-card {
background: var(--color-surface); background: var(--color-surface);
border-radius: var(--radius-lg); backdrop-filter: blur(24px) saturate(1.4);
padding: 36px 24px 32px; -webkit-backdrop-filter: blur(24px) saturate(1.4);
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04); border: 1px solid rgba(255,255,255,0.6);
width: 100%; border-radius: var(--radius-xl);
max-width: 380px; padding: 40px 28px 32px;
animation: cardIn 0.5s ease; box-shadow:
0 8px 32px rgba(0,0,0,0.06),
0 2px 8px rgba(0,0,0,0.04),
inset 0 1px 0 rgba(255,255,255,0.5);
width: 100%; max-width: 400px;
animation: cardEnter 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes cardEnter {
from { opacity: 0; transform: translateY(24px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
} }
@keyframes cardIn { /* Logo 区域 */
from { opacity: 0; transform: translateY(20px); } .login-header { text-align: center; margin-bottom: 32px; }
.login-header .app-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 72px; height: 72px; border-radius: 20px; font-size: 36px;
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
box-shadow: 0 4px 12px rgba(99,102,241,0.15);
margin-bottom: 16px; animation: iconPop 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both;
}
@keyframes iconPop {
from { opacity: 0; transform: scale(0.6); }
to { opacity: 1; transform: scale(1); }
}
.login-header h1 {
font-size: 24px; font-weight: 800; color: var(--color-text);
margin: 0 0 4px; letter-spacing: -0.5px;
}
.login-header .subtitle {
font-size: 14px; color: var(--color-text-muted); margin: 0;
font-weight: 400; letter-spacing: 0.2px;
}
/* ==================== 输入框 ==================== */
.input-group-custom {
position: relative; margin-bottom: 20px;
animation: fadeInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.25s both;
}
.input-group-custom:last-of-type { animation-delay: 0.32s; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.login-card .app-icon { .input-group-custom label {
text-align: center; display: block; font-size: 13px; font-weight: 600;
font-size: 48px; color: var(--color-text-secondary); margin-bottom: 6px;
margin-bottom: 8px; letter-spacing: 0.2px;
} }
.login-card h1 {
font-size: 22px; .input-wrapper {
font-weight: 700; position: relative; display: flex; align-items: center;
text-align: center; }
margin-bottom: 4px; .input-wrapper .input-icon {
position: absolute; left: 14px; font-size: 18px;
color: var(--color-text-muted); pointer-events: none;
transition: color 0.2s;
}
.input-wrapper input {
width: 100%; border: 1.5px solid var(--color-border);
border-radius: var(--radius-md); font-size: 15px;
padding: 13px 14px 13px 42px;
background: rgba(248,250,252,0.6);
transition: all 0.25s ease;
color: var(--color-text); color: var(--color-text);
letter-spacing: -0.3px;
} }
.login-card .subtitle { .input-wrapper input::placeholder { color: var(--color-text-muted); font-weight: 400; }
text-align: center; .input-wrapper input:focus {
font-size: var(--font-sm); outline: none; border-color: var(--color-primary);
color: var(--color-text-muted);
margin-bottom: 28px;
}
.login-card label {
font-size: var(--font-sm);
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 6px;
display: block;
}
.login-card .form-control {
border: 1.5px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: var(--font-base);
padding: 12px 16px;
transition: border-color 0.2s, box-shadow 0.2s;
background: #f8fafc;
}
.login-card .form-control:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
background: white; background: white;
box-shadow: 0 0 0 4px rgba(99,102,241,0.08), 0 2px 8px rgba(99,102,241,0.06);
} }
.login-card .mb-3 { margin-bottom: 18px; } .input-wrapper input:focus ~ .input-icon,
.input-wrapper input:focus + .input-icon { color: var(--color-primary); }
.login-card .btn-login { /* ==================== 按钮 ==================== */
width: 100%; .btn-login {
background: var(--color-primary); width: 100%; border: none; padding: 14px;
color: white; border-radius: var(--radius-md); font-size: 15px;
border: none; font-weight: 700; cursor: pointer; letter-spacing: 0.5px;
padding: 14px; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
border-radius: var(--radius-sm); color: white; position: relative; overflow: hidden;
font-size: var(--font-base); box-shadow: 0 4px 16px rgba(99,102,241,0.3);
font-weight: 700; transition: all 0.25s ease;
transition: background 0.2s, transform 0.1s; animation: fadeInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.4s both;
cursor: pointer;
letter-spacing: 0.3px;
} }
.login-card .btn-login:active { .btn-login:hover {
background: #4f46e5; box-shadow: 0 6px 24px rgba(99,102,241,0.4);
transform: scale(0.98); transform: translateY(-1px);
}
.btn-login:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 2px 8px rgba(99,102,241,0.3);
}
.btn-login::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, transparent 0%, rgba(255,255,255,0.15) 100%);
pointer-events: none;
} }
.login-card .alert { /* ==================== 错误提示 ==================== */
border-radius: var(--radius-sm); .alert-custom {
font-size: var(--font-sm); display: flex; align-items: center; gap: 10px;
padding: 12px 16px; background: linear-gradient(135deg, #fef2f2 0%, #fff1f2 100%);
margin-bottom: 20px; border: 1px solid rgba(239,68,68,0.15);
border: none; border-radius: var(--radius-md);
padding: 12px 16px; margin-bottom: 20px;
font-size: 14px; color: #dc2626; font-weight: 500;
animation: shakeIn 0.4s ease;
} }
.login-card .alert-danger { .alert-custom i { font-size: 18px; flex-shrink: 0; }
background: #fef2f2; @keyframes shakeIn {
color: #dc2626; 0% { opacity: 0; transform: translateX(-8px); }
40% { transform: translateX(4px); }
70% { transform: translateX(-2px); }
100% { opacity: 1; transform: translateX(0); }
}
/* ==================== 底部装饰 ==================== */
.login-footer {
text-align: center; margin-top: 24px;
font-size: 12px; color: var(--color-text-muted);
animation: fadeInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.5s both;
}
/* ==================== 响应式 ==================== */
@media (min-width: 768px) {
.login-card { padding: 48px 36px 36px; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="login-card"> <div class="bg-gradient"></div>
<div class="bg-orb bg-orb-1"></div>
<div class="bg-orb bg-orb-2"></div>
<div class="bg-orb bg-orb-3"></div>
<div class="login-card">
<div class="login-header">
<div class="app-icon">📝</div> <div class="app-icon">📝</div>
<h1>TraceCD</h1> <h1>TraceCD</h1>
<p class="subtitle">语音智能记账助手</p> <p class="subtitle">语音智能记账助手</p>
</div>
<div th:if="${error}" class="alert alert-danger" th:text="${error}"></div> <div th:if="${error}" class="alert-custom">
<i class="bi bi-exclamation-circle-fill"></i>
<span th:text="${error}"></span>
</div>
<form method="post" action="/login" autocomplete="off"> <form method="post" action="/login" autocomplete="off">
<div class="mb-3"> <div class="input-group-custom">
<label for="username">用户名</label> <label for="username">用户名</label>
<input type="text" class="form-control" id="username" name="username" <div class="input-wrapper">
<i class="bi bi-person input-icon"></i>
<input type="text" id="username" name="username"
placeholder="请输入用户名" required autofocus autocomplete="username"> placeholder="请输入用户名" required autofocus autocomplete="username">
</div> </div>
<div class="mb-3" style="margin-bottom:24px;"> </div>
<div class="input-group-custom" style="margin-bottom:28px;">
<label for="password">密码</label> <label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password" <div class="input-wrapper">
<i class="bi bi-lock input-icon"></i>
<input type="password" id="password" name="password"
placeholder="请输入密码" required autocomplete="current-password"> placeholder="请输入密码" required autocomplete="current-password">
</div> </div>
</div>
<button type="submit" class="btn-login">登 录</button> <button type="submit" class="btn-login">登 录</button>
</form> </form>
</div>
<div class="login-footer">语音记录 · 智能分析 · 轻松管理</div>
</div>
</body> </body>
</html> </html>