feat: optimize database admin UI with improved navigation

 Major Improvements:
- Layout optimization: Reduced navigation panel to 350px, maximized content area
- Navigation centralization: All operations in left panel with unified workflow
- History stack management: Implemented step-by-step back navigation
- Loading animations: Added loading spinners for all async operations
- Scrollable lists: Added custom scrollbar for long theme lists (max-height: 600px)
- Breadcrumb navigation: Visual path tracking with quick jump functionality

🎨 User Experience:
- Navigation paths show current position (e.g., Home › 市级 › 开办电影院)
- Clickable breadcrumbs for fast navigation to any step
- "Back" button for sequential navigation
- "Home" button to reset all selections
- Custom scrollbar styling matching UI design
- Responsive design with proper overflow handling

🔧 Technical Implementation:
- Step state machine (1→2→3→4 workflow)
- History stack for multi-step navigation
- Dynamic breadcrumb generation
- Smart state cleanup on quick jumps
- Loading states for all API operations

📁 Files Modified:
- static/db_admin.html: Complete UI/UX overhaul (734 lines)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Codex Agent 2025-10-30 09:54:36 +08:00
parent cbefb81a35
commit 506ea5ce98
1 changed files with 336 additions and 69 deletions

View File

@ -95,7 +95,7 @@
.content-area {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 350px 1fr;
gap: 30px;
min-height: 600px;
}
@ -113,13 +113,103 @@
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-controls {
display: flex;
gap: 8px;
}
.back-button {
padding: 4px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.back-button:hover:not(:disabled) {
background: #e0e0e0;
border-color: #999;
}
.back-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.breadcrumb {
background: white;
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 15px;
border: 1px solid #e0e0e0;
font-size: 14px;
color: #666;
}
.breadcrumb-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.breadcrumb-item a {
color: #667eea;
text-decoration: none;
cursor: pointer;
transition: color 0.3s;
}
.breadcrumb-item a:hover {
color: #5568d3;
text-decoration: underline;
}
.breadcrumb-separator {
color: #ccc;
margin: 0 4px;
}
.breadcrumb-current {
color: #333;
font-weight: 500;
}
.selection-area {
background: white;
border-radius: 8px;
padding: 20px;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
/* 自定义滚动条样式 */
.selection-area::-webkit-scrollbar {
width: 8px;
}
.selection-area::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.selection-area::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 4px;
transition: background 0.3s;
}
.selection-area::-webkit-scrollbar-thumb:hover {
background: #a0a0a0;
}
.item-list {
@ -332,6 +422,11 @@
.content-area {
grid-template-columns: 1fr;
}
.panel:first-child {
max-height: 300px;
overflow-y: auto;
}
}
</style>
</head>
@ -366,20 +461,26 @@
<div class="content-area">
<div class="panel">
<h2>选择区域</h2>
<h2 id="navTitle">
<span>选择区域</span>
<div class="nav-controls">
<button class="back-button" id="backButton" onclick="goBack()" disabled>← 上一步</button>
</div>
</h2>
<div class="breadcrumb" id="breadcrumb"></div>
<div class="selection-area">
<div id="regionList" class="item-list"></div>
<div id="navList" class="item-list"></div>
</div>
</div>
<div class="panel">
<h2>主题/许可详情</h2>
<div class="details-area">
<h2 id="detailsTitle">详情内容</h2>
<div class="details-area" id="detailsArea">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<p>请先选择地区以查看可用主题</p>
<p id="emptyMessage">请选择区域开始导航</p>
</div>
</div>
</div>
@ -387,22 +488,35 @@
</div>
<script>
// 导航状态管理
let currentStep = 1; // 1=区域, 2=主题, 3=许可, 4=详情
let historyStack = []; // 历史记录栈
let currentRegion = null;
let currentTheme = null;
let currentPermit = null;
// 步骤配置
const steps = {
1: { title: '选择区域', loadData: loadRegions },
2: { title: '选择主题', loadData: (region) => loadThemes(region.id, region.name) },
3: { title: '选择许可', loadData: (theme) => loadPermits(theme.id, theme.name) },
4: { title: '许可详情', loadData: null }
};
// 加载地区列表
async function loadRegions() {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载地区列表...';
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/regions');
const data = await response.json();
if (data.success) {
const regionList = document.getElementById('regionList');
regionList.innerHTML = '';
navList.innerHTML = '';
if (data.data.regions.length === 0) {
regionList.innerHTML = '<div class="error">未找到地区数据</div>';
navList.innerHTML = '<div class="error">未找到地区数据</div>';
return;
}
@ -410,35 +524,23 @@
const li = document.createElement('li');
li.innerHTML = `
<span class="item-name">${region.name}</span>
<span class="item-count">加载中...</span>
<span class="item-count">点击选择</span>
`;
li.onclick = () => selectRegion(region.id, region.name, li);
regionList.appendChild(li);
li.onclick = () => selectRegion(region.id, region.name);
navList.appendChild(li);
});
} else {
document.getElementById('regionList').innerHTML = `<div class="error">加载地区失败:${data.message}</div>`;
navList.innerHTML = `<div class="error">加载地区失败:${data.message}</div>`;
}
} catch (error) {
document.getElementById('regionList').innerHTML = `<div class="error">网络错误:${error.message}</div>`;
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
}
}
// 选择地区
async function selectRegion(regionId, regionName, element) {
// 更新UI
document.querySelectorAll('#regionList li').forEach(li => li.classList.remove('active'));
element.classList.add('active');
// 更新步骤指示器
updateStepIndicator(2);
currentRegion = { id: regionId, name: regionName };
currentTheme = null;
currentPermit = null;
// 清空并加载主题
const detailsArea = document.querySelector('.details-area');
detailsArea.innerHTML = '<div class="loading"></div>加载主题列表...';
// 加载主题列表
async function loadThemes(regionId, regionName) {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载主题列表...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/themes?region=${regionId}`);
@ -448,45 +550,33 @@
const themes = data.data.themes;
if (themes.length === 0) {
detailsArea.innerHTML = `<div class="error">地区 "${regionName}" 下没有可用的主题</div>`;
navList.innerHTML = `<div class="error">地区 "${regionName}" 下没有可用的主题</div>`;
return;
}
// 创建主题列表
let html = '<div class="item-list">';
themes.forEach(theme => {
html += `
<li onclick="selectTheme('${theme.id}', '${theme.name}', '${theme.option_id}', this)">
<li onclick="selectTheme('${theme.id}', '${theme.name.replace(/'/g, "\\'")}')">
<span class="item-name">${theme.name}</span>
<span class="item-count">点击查看许可</span>
<span class="item-count">点击选择</span>
</li>
`;
});
html += '</div>';
detailsArea.innerHTML = html;
navList.innerHTML = html;
} else {
detailsArea.innerHTML = `<div class="error">加载主题失败:${data.message}</div>`;
navList.innerHTML = `<div class="error">加载主题失败:${data.message}</div>`;
}
} catch (error) {
detailsArea.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
}
}
// 选择主题
async function selectTheme(themeId, themeName, optionId, element) {
// 更新UI
document.querySelectorAll('.details-area .item-list li').forEach(li => li.classList.remove('active'));
element.classList.add('active');
// 更新步骤指示器
updateStepIndicator(3);
currentTheme = { id: themeId, name: themeName };
currentPermit = null;
// 加载许可列表
const detailsArea = document.querySelector('.details-area');
detailsArea.innerHTML = '<div class="loading"></div>加载许可列表...';
async function loadPermits(themeId, themeName) {
const navList = document.getElementById('navList');
navList.innerHTML = '<div class="loading"></div>加载许可列表...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}&theme=${themeId}`);
@ -496,48 +586,210 @@
const permits = data.data.permits;
if (permits.length === 0) {
detailsArea.innerHTML = `<div class="error">主题 "${themeName}" 下没有可用的许可</div>`;
navList.innerHTML = `<div class="error">主题 "${themeName}" 下没有可用的许可</div>`;
return;
}
// 创建许可列表
let html = '<div class="item-list">';
permits.forEach(permit => {
const riskCount = permit.risks ? permit.risks.length : 0;
html += `
<li onclick="selectPermit('${permit.id}', '${permit.name.replace(/'/g, "\\'")}', '${themeId}', this)">
<li onclick="selectPermit('${permit.id}', '${permit.name.replace(/'/g, "\\'")}', '${themeId}')">
<span class="item-name">${permit.name}</span>
<span class="item-count">${riskCount} 个风险</span>
</li>
`;
});
html += '</div>';
detailsArea.innerHTML = html;
navList.innerHTML = html;
} else {
detailsArea.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
navList.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
}
} catch (error) {
detailsArea.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
navList.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
}
}
// 选择地区
async function selectRegion(regionId, regionName) {
// 保存到历史栈
historyStack.push({ step: currentStep, region: currentRegion });
currentRegion = { id: regionId, name: regionName };
currentTheme = null;
currentPermit = null;
// 更新步骤
goToStep(2);
}
// 选择主题
async function selectTheme(themeId, themeName) {
// 保存到历史栈
historyStack.push({ step: currentStep, theme: currentTheme });
currentTheme = { id: themeId, name: themeName };
currentPermit = null;
// 更新步骤
goToStep(3);
}
// 选择许可
async function selectPermit(permitId, permitName, themeId, element) {
// 更新UI
document.querySelectorAll('.details-area .item-list li').forEach(li => li.classList.remove('active'));
element.classList.add('active');
async function selectPermit(permitId, permitName, themeId) {
// 保存到历史栈
historyStack.push({ step: currentStep, permit: currentPermit });
currentPermit = { id: permitId, name: permitName, themeId: themeId };
// 更新步骤
goToStep(4);
}
// 跳转到指定步骤
async function goToStep(step) {
currentStep = step;
// 更新导航标题
document.getElementById('navTitle').querySelector('span').textContent = steps[step].title;
// 更新上一步按钮
const backButton = document.getElementById('backButton');
backButton.disabled = historyStack.length === 0;
// 更新步骤指示器
updateStepIndicator(4);
updateStepIndicator(step);
currentPermit = { id: permitId, name: permitName };
// 更新面包屑导航
updateBreadcrumb();
// 加载许可详情
const detailsArea = document.querySelector('.details-area');
// 清空详情区域
const detailsArea = document.getElementById('detailsArea');
if (step === 1) {
detailsArea.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<p id="emptyMessage">请选择区域开始导航</p>
</div>
`;
}
// 加载数据
if (step === 1) {
await loadRegions();
} else if (step === 2) {
await loadThemes(currentRegion.id, currentRegion.name);
} else if (step === 3) {
await loadPermits(currentTheme.id, currentTheme.name);
} else if (step === 4) {
await showPermitDetails();
}
}
// 更新面包屑导航
function updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
let html = '';
// 总是显示"首页"
html += `
<span class="breadcrumb-item">
<a onclick="goHome()">首页</a>
</span>
`;
// 显示当前选择的路径
if (currentRegion) {
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 2) {
html += `
<span class="breadcrumb-item">
<a onclick="quickJump(2)">${currentRegion.name}</a>
</span>
`;
} else {
html += `
<span class="breadcrumb-item">
<span class="breadcrumb-current">${currentRegion.name}</span>
</span>
`;
}
}
if (currentTheme) {
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 3) {
html += `
<span class="breadcrumb-item">
<a onclick="quickJump(3)">${currentTheme.name}</a>
</span>
`;
} else {
html += `
<span class="breadcrumb-item">
<span class="breadcrumb-current">${currentTheme.name}</span>
</span>
`;
}
}
if (currentPermit) {
html += '<span class="breadcrumb-separator"></span>';
if (currentStep > 4) {
html += `
<span class="breadcrumb-item">
<a onclick="quickJump(4)">${currentPermit.name}</a>
</span>
`;
} else {
html += `
<span class="breadcrumb-item">
<span class="breadcrumb-current">${currentPermit.name}</span>
</span>
`;
}
}
breadcrumb.innerHTML = html;
}
// 快速跳转到指定步骤
function quickJump(targetStep) {
// 清空历史栈中比目标步骤更晚的记录
while (historyStack.length > 0 && historyStack[historyStack.length - 1].step >= targetStep) {
historyStack.pop();
}
// 清理后续状态
if (targetStep <= 2) {
currentTheme = null;
currentPermit = null;
}
if (targetStep <= 3) {
currentPermit = null;
}
goToStep(targetStep);
}
// 返回首页
function goHome() {
currentRegion = null;
currentTheme = null;
currentPermit = null;
historyStack = [];
goToStep(1);
}
// 显示许可详情
async function showPermitDetails() {
const detailsArea = document.getElementById('detailsArea');
detailsArea.innerHTML = '<div class="loading"></div>加载许可详情...';
try {
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&theme=${themeId}&permit=${permitId}`);
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permit-details?region=${currentRegion.id}&theme=${currentPermit.themeId}&permit=${currentPermit.id}`);
const data = await response.json();
if (data.success) {
@ -550,6 +802,21 @@
}
}
// 回退到上一步
function goBack() {
if (historyStack.length === 0) return;
const prev = historyStack.pop();
// 恢复状态
if (prev.region) currentRegion = prev.region;
if (prev.theme) currentTheme = prev.theme;
if (prev.permit) currentPermit = prev.permit;
// 跳转到上一步
goToStep(prev.step);
}
// 渲染许可详情
function renderPermitDetails(permit) {
const detailsArea = document.querySelector('.details-area');
@ -622,7 +889,7 @@
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', () => {
loadRegions();
goToStep(1);
});
</script>
</body>