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:
parent
cbefb81a35
commit
506ea5ce98
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue