fix: resolve server deployment image 404 errors and enhance admin UI
- Add dedicated image serving API endpoint (/admin/images/<filename>) with security whitelist - Update image paths from /static/ to /fs-ai-asistant/api/workflow/lawrisk/admin/images/ - Add permit import sample file download endpoint - Enhance import wizard UI with template/sample preview section - Add risk count column to unbound permits table - Filter out "不涉及" (not applicable) theme from theme list - Improve permit import UX with better visual organization This ensures images load correctly in server deployments (nginx, gunicorn) by using the same API prefix as other admin resources, avoiding static file routing issues. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
347af34bfc
commit
fe911592e0
|
|
@ -56,6 +56,7 @@ from lawrisk.services.auth_service import (
|
|||
)
|
||||
from lawrisk.services.template_service import (
|
||||
get_permit_template_path,
|
||||
get_permit_sample_path,
|
||||
get_permit_template_metadata,
|
||||
overwrite_permit_template,
|
||||
)
|
||||
|
|
@ -1078,6 +1079,45 @@ def admin_permit_import_template():
|
|||
return jsonify({"success": False, "message": "模板文件暂时无法下载"}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/permit-import/sample', methods=['GET'])
|
||||
def admin_permit_import_sample():
|
||||
"""Provide the Excel import sample file for download."""
|
||||
sample_path = get_permit_sample_path()
|
||||
|
||||
if not os.path.exists(sample_path):
|
||||
return jsonify({"success": False, "message": "样表文件不存在,请联系管理员"}), 404
|
||||
|
||||
try:
|
||||
return send_file(
|
||||
sample_path,
|
||||
as_attachment=True,
|
||||
download_name='风险提示表(仅销售预包装食品备案,市场监管部门)(样表).xlsx',
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"admin_permit_import_sample error: {exc}")
|
||||
return jsonify({"success": False, "message": "样表文件暂时无法下载"}), 500
|
||||
|
||||
|
||||
@v2_bp.route('/admin/images/<filename>', methods=['GET'])
|
||||
def admin_image(filename):
|
||||
"""Serve admin UI image files."""
|
||||
# 安全检查:只允许特定的文件名,防止路径遍历攻击
|
||||
allowed_files = {'empty_table.png', 'sample_table.png'}
|
||||
if filename not in allowed_files:
|
||||
return jsonify({"success": False, "message": "文件不存在"}), 404
|
||||
|
||||
image_path = os.path.join(_project_root(), 'static', 'images', filename)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
return jsonify({"success": False, "message": f"图片文件 {filename} 不存在"}), 404
|
||||
|
||||
try:
|
||||
return send_file(image_path, mimetype='image/png')
|
||||
except Exception as exc:
|
||||
print(f"admin_image error for {filename}: {exc}")
|
||||
return jsonify({"success": False, "message": "图片文件暂时无法加载"}), 500
|
||||
|
||||
|
||||
def _build_import_preview_response(session_token: str):
|
||||
"""Internal helper to build preview response JSON."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from typing import Any, Dict
|
|||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "data", "template")
|
||||
PERMIT_TEMPLATE_FILENAME = "风险提示表 模板.xlsx"
|
||||
PERMIT_SAMPLE_FILENAME = "风险提示表(仅销售预包装食品备案,市场监管部门)(样表).xlsx"
|
||||
TEMPLATE_META_FILENAME = "template_meta.json"
|
||||
|
||||
|
||||
|
|
@ -18,6 +19,11 @@ def get_permit_template_path() -> str:
|
|||
return os.path.join(TEMPLATE_DIR, PERMIT_TEMPLATE_FILENAME)
|
||||
|
||||
|
||||
def get_permit_sample_path() -> str:
|
||||
"""Return the absolute path to the permit import sample file."""
|
||||
return os.path.join(TEMPLATE_DIR, PERMIT_SAMPLE_FILENAME)
|
||||
|
||||
|
||||
def _meta_path() -> str:
|
||||
return os.path.join(TEMPLATE_DIR, TEMPLATE_META_FILENAME)
|
||||
|
||||
|
|
|
|||
|
|
@ -2770,6 +2770,63 @@
|
|||
<span>📥</span> 许可导入
|
||||
</h2>
|
||||
<!-- <p style="color: #666; margin-bottom: 20px;">通过Excel文件批量导入许可数据,支持多区划批量处理、主题绑定、预览确认等功能。</p> -->
|
||||
|
||||
<!-- 导入模板与样表示例 -->
|
||||
<div class="sample-excel-preview-section" style="margin-bottom: 24px;">
|
||||
<div class="sample-excel-preview-title" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>💡</span>
|
||||
<span>导入模板与样表示例</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a href="/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/template"
|
||||
download
|
||||
style="display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px;
|
||||
background: #2c5282; color: white; border-radius: 6px; text-decoration: none;
|
||||
font-size: 13px; font-weight: 600; transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(44, 82, 130, 0.2);
|
||||
white-space: nowrap;"
|
||||
onmouseover="this.style.background='#3b82f6'; this.style.boxShadow='0 4px 8px rgba(59, 130, 246, 0.3)'; this.style.transform='translateY(-1px)';"
|
||||
onmouseout="this.style.background='#2c5282'; this.style.boxShadow='0 2px 4px rgba(44, 82, 130, 0.2)'; this.style.transform='translateY(0)';">
|
||||
<span>📥</span>
|
||||
<span>下载导入模板</span>
|
||||
</a>
|
||||
<a href="/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/sample"
|
||||
download
|
||||
style="display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px;
|
||||
background: #2c5282; color: white; border-radius: 6px; text-decoration: none;
|
||||
font-size: 13px; font-weight: 600; transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(44, 82, 130, 0.2);
|
||||
white-space: nowrap;"
|
||||
onmouseover="this.style.background='#3b82f6'; this.style.boxShadow='0 4px 8px rgba(59, 130, 246, 0.3)'; this.style.transform='translateY(-1px)';"
|
||||
onmouseout="this.style.background='#2c5282'; this.style.boxShadow='0 2px 4px rgba(44, 82, 130, 0.2)'; this.style.transform='translateY(0)';">
|
||||
<span>📥</span>
|
||||
<span>下载样表</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sample-excel-preview-container" style="display: flex; gap: 24px; padding: 20px; background: #f8fafc;">
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">1</span>
|
||||
空表 (空模板)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/fs-ai-asistant/api/workflow/lawrisk/admin/images/empty_table.png')">
|
||||
<img src="/fs-ai-asistant/api/workflow/lawrisk/admin/images/empty_table.png" alt="空表" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">2</span>
|
||||
样表示例 (含填报说明)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/fs-ai-asistant/api/workflow/lawrisk/admin/images/sample_table.png')">
|
||||
<img src="/fs-ai-asistant/api/workflow/lawrisk/admin/images/sample_table.png" alt="样表示例" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0;">
|
||||
<div class="drag-drop-area" onclick="triggerFileUpload()" id="dragDropArea">
|
||||
<div class="drag-drop-icon">📄</div>
|
||||
|
|
@ -4495,13 +4552,6 @@
|
|||
html += '<h3><span>📄</span> 上传 Excel</h3>';
|
||||
html += '<div class="import-upload-area">';
|
||||
html += '<input type="file" accept=".xlsx,.xlsm" onchange="handleImportFile(this)">';
|
||||
html += '<div class="import-template-wrapper">';
|
||||
html += '<a class="import-template-button" href="/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/template" download>';
|
||||
html += '<span class="import-template-icon">📥</span>';
|
||||
html += '<span class="import-template-text"><span class="import-template-title">下载导入模板</span><span class="import-template-subtitle">包含示例字段与填写说明</span></span>';
|
||||
html += '<span class="import-template-badge">推荐</span>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
if (state.sessionId) {
|
||||
const sheetCount = state.sheetSummaries ? state.sheetSummaries.length : 0;
|
||||
html += `<div class="import-meta">当前会话:${escapeHtml(state.filename || '(未命名)')} | Sheet ${sheetCount} 个 | 风险 ${state.totalRows || 0} 条</div>`;
|
||||
|
|
@ -4578,36 +4628,6 @@
|
|||
html += '<div class="loading" style="margin: 12px 0;"><span class="loading-icon"></span>正在准备预览数据...</div>';
|
||||
}
|
||||
|
||||
// 添加样表示例
|
||||
html += '<div class="sample-excel-preview-section">';
|
||||
html += '<div class="sample-excel-preview-title"><span>💡</span> 导入模板与样表示例</div>';
|
||||
html += getSampleExcelHtml();
|
||||
html += '</div>';
|
||||
|
||||
function getSampleExcelHtml() {
|
||||
return `
|
||||
<div class="sample-excel-preview-container" style="display: flex; gap: 24px; padding: 20px; background: #f8fafc;">
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">1</span>
|
||||
空表 (空模板)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/static/images/empty_table.png')">
|
||||
<img src="/static/images/empty_table.png" alt="空表" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; background: white; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<span style="background: #3b82f6; color: white; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px;">2</span>
|
||||
样表示例 (含填报说明)
|
||||
</div>
|
||||
<div style="flex: 1; background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); cursor: zoom-in;" onclick="openImageZoom('/static/images/sample_table.png')">
|
||||
<img src="/static/images/sample_table.png" alt="样表示例" style="width: 100%; height: auto; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.05));">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const nextDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.uploading || state.previewLoading;
|
||||
html += '<div class="import-actions">';
|
||||
html += '<div class="import-hint">提示:系统将根据配置规则自动匹配主题,进入下一步预览结果。</div>';
|
||||
|
|
@ -7659,6 +7679,7 @@
|
|||
<tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align: left;">
|
||||
<th style="padding: 12px;">许可(备案)事项名称</th>
|
||||
<th style="padding: 12px; width: 100px;">实施层级</th>
|
||||
<th style="padding: 12px; width: 120px;">提示条款数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -7676,10 +7697,19 @@
|
|||
? "background: #e0f2fe; color: #0369a1;" // Blue for Municipal
|
||||
: "background: #fee2e2; color: #b91c1c;"; // Red for District
|
||||
|
||||
// Add risk count display logic
|
||||
const riskCount = typeof p.risk_count === 'number' ? p.risk_count : 0;
|
||||
const riskBadgeStyle = riskCount > 0
|
||||
? "background: #fef3c7; color: #92400e;" // Amber - has risks
|
||||
: "background: #f3f4f6; color: #6b7280;"; // Gray - no risks
|
||||
|
||||
html += `
|
||||
<tr style="border-bottom: 1px solid #f1f5f9;">
|
||||
<td style="padding: 12px; color: #475569;">${escapeHtml(p.permit_name)}</td>
|
||||
<td style="padding: 12px; white-space: nowrap;"><span class="tab-badge" style="${badgeStyle}">${implLevel}</span></td>
|
||||
<td style="padding: 12px; text-align: center; white-space: nowrap;">
|
||||
<span class="tab-badge" style="${riskBadgeStyle}">${riskCount}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue