feat: 实现数据库维护功能
## 新增功能 ### 1. 后端API路由 (lawrisk/api/v2.py) - 添加了5个新的管理API端点: * GET /admin/regions - 获取地区列表 * GET /admin/themes - 获取主题列表(按地区筛选) * GET /admin/permits - 获取许可列表(按地区和主题筛选) * GET /admin/permit-details - 获取许可详细信息 * GET /admin/test - 测试路由 ### 2. 前端管理界面 (static/db_admin.html) - 实现了完整的数据库维护管理页面 - 4步操作流程:地区选择 → 主题列表 → 许可列表 → 详细信息展示 - 现代化UI设计,包括: * 渐变背景和响应式布局 * 平滑动画过渡效果 * 实时数据加载提示 * 完整的许可信息展示(许可状态、经营范围、法律风险等) ## 技术实现 - RESTful API设计,返回标准JSON格式 - 直接从PostgreSQL数据库读取数据 - 所有API已通过curl和Flask测试客户端验证 ## 测试结果 在端口8888上测试通过: - admin/regions: 1个地区 - admin/themes: 57个主题 - admin/permits: 6个许可 - admin/permit-details: 完整许可信息和3个风险记录 - 静态页面: 成功加载 ## 使用方法 ```bash # 启动服务 PORT=8888 python app.py & # 访问管理界面 http://localhost:8888/static/db_admin.html # API调用示例 curl http://localhost:8888/fs-ai-asistant/api/workflow/lawrisk/admin/regions ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bfda66afc1
commit
cbefb81a35
|
|
@ -6,7 +6,12 @@ from flask import Blueprint, jsonify, request
|
|||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from lawrisk.services.lawrisk_v2_service import search_v2, list_regions
|
||||
from lawrisk.services.licensing_repo import list_permits_for_region
|
||||
from lawrisk.services.licensing_repo import (
|
||||
list_permits_for_region,
|
||||
load_permits_and_risks,
|
||||
list_region_theme_options,
|
||||
load_theme_payload,
|
||||
)
|
||||
from lawrisk.services.lawrisk_service import suggest_questions_embed
|
||||
|
||||
v2_bp = Blueprint('lawrisk_v2', __name__, url_prefix='/fs-ai-asistant/api/workflow/lawrisk')
|
||||
|
|
@ -58,7 +63,7 @@ def lawrisk_search_v2():
|
|||
return jsonify({"success": False, "message": str(e), "data": {}}), 500
|
||||
|
||||
|
||||
@v2_bp.get('/v2/regions')
|
||||
@v2_bp.route('/v2/regions', methods=['GET'])
|
||||
def lawrisk_regions():
|
||||
"""Get list of available regions."""
|
||||
try:
|
||||
|
|
@ -125,3 +130,123 @@ def _extract_params():
|
|||
region_value = payload.get("region") or payload.get("region_id")
|
||||
|
||||
return query, debug_flag, top_k_int, mode_value, region_value
|
||||
|
||||
|
||||
@v2_bp.route('/admin/test', methods=['GET'])
|
||||
def admin_test():
|
||||
"""Simple test route."""
|
||||
return jsonify({"success": True, "message": "Test route works!"})
|
||||
|
||||
|
||||
@v2_bp.route('/test-simple', methods=['GET'])
|
||||
def test_simple():
|
||||
"""Very simple test."""
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@v2_bp.route('/admin/regions', methods=['GET'])
|
||||
def admin_regions():
|
||||
"""Get all regions for database maintenance."""
|
||||
try:
|
||||
regions = list_regions()
|
||||
return jsonify({"success": True, "data": {"regions": regions}})
|
||||
except Exception as exc:
|
||||
print(f"admin_regions error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/themes', methods=['GET'])
|
||||
def admin_themes():
|
||||
"""Get themes for a specific region."""
|
||||
region_value = request.args.get("region") or request.args.get("region_id")
|
||||
|
||||
if not region_value or (isinstance(region_value, str) and not region_value.strip()):
|
||||
return jsonify({"success": False, "message": "region is required", "data": {}}), 400
|
||||
|
||||
region_token = region_value.strip() if isinstance(region_value, str) else str(region_value)
|
||||
|
||||
try:
|
||||
catalog = list_region_theme_options()
|
||||
region_id_lower = region_token.lower()
|
||||
|
||||
themes = []
|
||||
seen_theme_ids = set()
|
||||
|
||||
for item in catalog:
|
||||
if (item["region_id"] == region_token or
|
||||
item["region_id"].lower() == region_id_lower or
|
||||
item["region_name"].lower() == region_id_lower):
|
||||
theme_id = item["theme_id"]
|
||||
if theme_id not in seen_theme_ids:
|
||||
seen_theme_ids.add(theme_id)
|
||||
themes.append({
|
||||
"id": theme_id,
|
||||
"name": item["theme_name"],
|
||||
"option_id": item["option_id"]
|
||||
})
|
||||
|
||||
return jsonify({"success": True, "data": {"region": region_token, "themes": themes}})
|
||||
except Exception as exc:
|
||||
print(f"admin_themes error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/permits', methods=['GET'])
|
||||
def admin_permits():
|
||||
"""Get permits for a specific region-theme combination."""
|
||||
region_value = request.args.get("region") or request.args.get("region_id")
|
||||
theme_value = request.args.get("theme") or request.args.get("theme_id")
|
||||
|
||||
if not region_value or not theme_value:
|
||||
return jsonify({"success": False, "message": "region and theme are required", "data": {}}), 400
|
||||
|
||||
region_token = region_value.strip() if isinstance(region_value, str) else str(region_value)
|
||||
theme_token = theme_value.strip() if isinstance(theme_value, str) else str(theme_value)
|
||||
|
||||
try:
|
||||
permits = load_permits_and_risks(region_token, theme_token)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"region": region_token,
|
||||
"theme": theme_token,
|
||||
"permits": permits
|
||||
}
|
||||
})
|
||||
except Exception as exc:
|
||||
print(f"admin_permits error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/permit-details', methods=['GET'])
|
||||
def admin_permit_details():
|
||||
"""Get detailed information for a specific permit."""
|
||||
region_value = request.args.get("region") or request.args.get("region_id")
|
||||
theme_value = request.args.get("theme") or request.args.get("theme_id")
|
||||
permit_value = request.args.get("permit") or request.args.get("permit_id")
|
||||
|
||||
if not region_value or not theme_value or not permit_value:
|
||||
return jsonify({"success": False, "message": "region, theme, and permit are required", "data": {}}), 400
|
||||
|
||||
region_token = region_value.strip() if isinstance(region_value, str) else str(region_value)
|
||||
theme_token = theme_value.strip() if isinstance(theme_value, str) else str(theme_value)
|
||||
permit_token = permit_value.strip() if isinstance(permit_value, str) else str(permit_value)
|
||||
|
||||
try:
|
||||
permits = load_permits_and_risks(region_token, theme_token, permit_token)
|
||||
|
||||
if not permits:
|
||||
return jsonify({"success": False, "message": "Permit not found", "data": {}}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"region": region_token,
|
||||
"theme": theme_token,
|
||||
"permit": permits[0]
|
||||
}
|
||||
})
|
||||
except Exception as exc:
|
||||
print(f"admin_permit_details error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
|
|
|||
|
|
@ -0,0 +1,629 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据库维护页面 - LawRisk</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #e0e7ff;
|
||||
color: #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.step.active .step-number {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step.active .step-label {
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #ccc;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.selection-area {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
list-style: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.item-list li {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-list li:hover {
|
||||
background: #e0e7ff;
|
||||
border-color: #667eea;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.item-list li.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.item-list li.active:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 12px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.item-list li.active .item-count {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.details-area {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 15px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.details-content {
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-section h3::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #667eea;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #667eea;
|
||||
line-height: 1.8;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.risk-item h4 {
|
||||
color: #d32f2f;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.risk-field {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.risk-field strong {
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.risk-field p {
|
||||
color: #555;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.scope-item {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.permit-status {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #c62828;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🗃️ 数据库维护系统</h1>
|
||||
<p>LawRisk 法律风险提示系统 - 数据库维护与查询工具</p>
|
||||
</div>
|
||||
|
||||
<div class="step-indicator">
|
||||
<div class="step active" id="step1">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-label">选择地区</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step2">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">选择主题</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step3">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">选择许可</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step4">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-label">查看详情</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="panel">
|
||||
<h2>选择区域</h2>
|
||||
<div class="selection-area">
|
||||
<div id="regionList" class="item-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>主题/许可详情</h2>
|
||||
<div class="details-area">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRegion = null;
|
||||
let currentTheme = null;
|
||||
let currentPermit = null;
|
||||
|
||||
// 加载地区列表
|
||||
async function loadRegions() {
|
||||
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 = '';
|
||||
|
||||
if (data.data.regions.length === 0) {
|
||||
regionList.innerHTML = '<div class="error">未找到地区数据</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.data.regions.forEach(region => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `
|
||||
<span class="item-name">${region.name}</span>
|
||||
<span class="item-count">加载中...</span>
|
||||
`;
|
||||
li.onclick = () => selectRegion(region.id, region.name, li);
|
||||
regionList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
document.getElementById('regionList').innerHTML = `<div class="error">加载地区失败:${data.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('regionList').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>加载主题列表...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/themes?region=${regionId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const themes = data.data.themes;
|
||||
|
||||
if (themes.length === 0) {
|
||||
detailsArea.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)">
|
||||
<span class="item-name">${theme.name}</span>
|
||||
<span class="item-count">点击查看许可</span>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
detailsArea.innerHTML = html;
|
||||
} else {
|
||||
detailsArea.innerHTML = `<div class="error">加载主题失败:${data.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
detailsArea.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>加载许可列表...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/fs-ai-asistant/api/workflow/lawrisk/admin/permits?region=${currentRegion.id}&theme=${themeId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const permits = data.data.permits;
|
||||
|
||||
if (permits.length === 0) {
|
||||
detailsArea.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)">
|
||||
<span class="item-name">${permit.name}</span>
|
||||
<span class="item-count">${riskCount} 个风险</span>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
detailsArea.innerHTML = html;
|
||||
} else {
|
||||
detailsArea.innerHTML = `<div class="error">加载许可失败:${data.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
detailsArea.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择许可
|
||||
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');
|
||||
|
||||
// 更新步骤指示器
|
||||
updateStepIndicator(4);
|
||||
|
||||
currentPermit = { id: permitId, name: permitName };
|
||||
|
||||
// 加载许可详情
|
||||
const detailsArea = document.querySelector('.details-area');
|
||||
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 data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderPermitDetails(data.data.permit);
|
||||
} else {
|
||||
detailsArea.innerHTML = `<div class="error">加载详情失败:${data.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
detailsArea.innerHTML = `<div class="error">网络错误:${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染许可详情
|
||||
function renderPermitDetails(permit) {
|
||||
const detailsArea = document.querySelector('.details-area');
|
||||
let html = '<div class="details-content">';
|
||||
|
||||
// 许可基本信息
|
||||
html += `
|
||||
<div class="detail-section">
|
||||
<h3>许可信息</h3>
|
||||
<div class="detail-content">
|
||||
<p><strong>许可名称:</strong>${permit.name}</p>
|
||||
${permit.permit_status ? `<p style="margin-top: 10px;"><strong>许可状态:</strong><span class="permit-status ${permit.permit_status === 'active' ? 'status-active' : 'status-inactive'}">${permit.permit_status}</span></p>` : ''}
|
||||
${permit.subitem_summary ? `<p style="margin-top: 10px;"><strong>子项说明:</strong>${permit.subitem_summary}</p>` : ''}
|
||||
${permit.responsible_contact ? `<p style="margin-top: 10px;"><strong>负责部门:</strong>${permit.responsible_contact}</p>` : ''}
|
||||
${permit.jurisdiction_scope ? `<p style="margin-top: 10px;"><strong>管辖范围:</strong>${permit.jurisdiction_scope}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 经营范围
|
||||
if (permit.business_scopes && permit.business_scopes.length > 0) {
|
||||
html += '<div class="detail-section"><h3>经营范围</h3><div class="detail-content">';
|
||||
permit.business_scopes.forEach(scope => {
|
||||
html += `<div class="scope-item">${scope.description}</div>`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// 法律风险
|
||||
if (permit.risks && permit.risks.length > 0) {
|
||||
html += '<div class="detail-section"><h3>法律风险</h3><div class="detail-content">';
|
||||
permit.risks.forEach(risk => {
|
||||
html += `
|
||||
<div class="risk-item">
|
||||
<h4>风险 ${risk.id}</h4>
|
||||
${risk.risk_content ? `<div class="risk-field"><strong>风险内容:</strong><p>${risk.risk_content}</p></div>` : ''}
|
||||
${risk.legal_basis ? `<div class="risk-field"><strong>法律依据:</strong><p>${risk.legal_basis}</p></div>` : ''}
|
||||
${risk.document_no ? `<div class="risk-field"><strong>文件编号:</strong><p>${risk.document_no}</p></div>` : ''}
|
||||
${risk.summary ? `<div class="risk-field"><strong>摘要:</strong><div style="margin-top: 5px;">${risk.summary}</div></div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
} else {
|
||||
html += `
|
||||
<div class="detail-section">
|
||||
<h3>法律风险</h3>
|
||||
<div class="detail-content">
|
||||
<p style="color: #999;">暂无法律风险信息</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
detailsArea.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新步骤指示器
|
||||
function updateStepIndicator(step) {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const stepElement = document.getElementById(`step${i}`);
|
||||
if (i <= step) {
|
||||
stepElement.classList.add('active');
|
||||
} else {
|
||||
stepElement.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadRegions();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue