fs-lawrisk/static/permit_upload.html

769 lines
26 KiB
HTML
Raw Normal View History

<!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>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: "Microsoft YaHei", "PingFang SC", -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #f7fafc 0%, #e2e8f0 100%);
min-height: 100vh;
color: #1f2937;
}
.page {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px 60px;
}
.header {
background: #fff;
border-radius: 18px;
padding: 28px;
box-shadow: 0 20px 60px rgba(79, 70, 229, 0.12);
margin-bottom: 24px;
position: relative;
}
.title {
text-align: center;
}
.title h1 {
font-size: 30px;
margin: 0;
color: #111827;
}
.title p {
margin: 6px 0 0;
color: #4b5563;
font-size: 15px;
}
.upload-section {
background: #fff;
border-radius: 18px;
padding: 28px;
box-shadow: 0 20px 60px rgba(79, 70, 229, 0.12);
margin-bottom: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #374151;
margin: 0 0 20px;
padding-bottom: 12px;
border-bottom: 2px solid #f3f4f6;
}
.upload-area {
border: 2px dashed #d1d5db;
border-radius: 12px;
padding: 48px 20px;
text-align: center;
background: #f9fafb;
transition: all 0.3s;
cursor: pointer;
position: relative;
}
.upload-area:hover {
border-color: #6366f1;
background: #f0f9ff;
}
.upload-area.dragover {
border-color: #6366f1;
background: #eef2ff;
transform: scale(1.02);
}
.upload-icon {
font-size: 48px;
margin-bottom: 12px;
color: #6366f1;
}
.upload-text {
font-size: 16px;
color: #4b5563;
margin-bottom: 8px;
}
.upload-hint {
font-size: 14px;
color: #9ca3af;
}
.upload-input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-list {
margin-top: 24px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 10px;
margin-bottom: 12px;
}
.file-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.file-icon {
font-size: 24px;
color: #6366f1;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 600;
color: #374151;
margin-bottom: 4px;
}
.file-size {
font-size: 13px;
color: #9ca3af;
}
.file-status {
font-size: 13px;
padding: 4px 12px;
border-radius: 999px;
background: #dbeafe;
color: #1e40af;
}
.file-status.uploading {
background: #fef3c7;
color: #92400e;
}
.file-status.success {
background: #d1fae5;
color: #065f46;
}
.file-status.error {
background: #fee2e2;
color: #991b1b;
}
.file-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-small {
padding: 6px 12px;
font-size: 13px;
}
.btn-danger {
background: #fee2e2;
color: #991b1b;
}
.btn-danger:hover {
background: #fecaca;
}
.btn-primary {
background: #6366f1;
color: #fff;
}
.btn-primary:hover {
background: #4f46e5;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.form-select, .form-input, .form-textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 10px;
font-size: 14px;
background: #fff;
transition: all 0.2s;
}
.form-select:focus, .form-input:focus, .form-textarea:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-help {
font-size: 13px;
color: #6b7280;
margin-top: 6px;
}
.binding-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-top: 12px;
}
.binding-card {
padding: 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
}
.binding-card:hover {
border-color: #6366f1;
}
.binding-card.selected {
border-color: #6366f1;
background: #eef2ff;
}
.binding-radio {
display: none;
}
.binding-title {
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.binding-desc {
font-size: 13px;
color: #6b7280;
}
.submit-section {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 24px;
border-top: 2px solid #f3f4f6;
}
.loading {
text-align: center;
padding: 40px 20px;
color: #6b7280;
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f4f6;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
background: #fee2e2;
border: 1px solid #fecaca;
color: #991b1b;
padding: 16px;
border-radius: 10px;
margin-bottom: 20px;
}
.success-message {
background: #d1fae5;
border: 1px solid #a7f3d0;
color: #065f46;
padding: 16px;
border-radius: 10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="page">
<!-- 页面头部 -->
<div class="header">
<div class="title">
<h1>许可文件上传</h1>
<p>上传Excel格式的许可文件可选择绑定到特定单位</p>
</div>
</div>
<!-- 上传区域 -->
<div class="upload-section">
<h2 class="section-title">1. 选择文件</h2>
<div id="uploadArea" class="upload-area">
<div class="upload-icon">📤</div>
<div class="upload-text">点击或拖拽文件到此处上传</div>
<div class="upload-hint">支持 .xlsx 格式,建议文件大小不超过 50MB</div>
<input type="file" id="fileInput" class="upload-input" accept=".xlsx,.xls" multiple />
</div>
<div id="fileList" class="file-list" style="display: none;"></div>
</div>
<!-- 绑定设置 -->
<div class="upload-section">
<h2 class="section-title">2. 绑定设置(可选)</h2>
<div class="form-group">
<label class="form-label">绑定方式</label>
<div class="binding-options">
<label class="binding-card">
<input type="radio" name="bindingType" value="none" class="binding-radio" checked />
<div class="binding-title">不绑定</div>
<div class="binding-desc">文件将只有上传者和上级单位可见</div>
</label>
<label class="binding-card" id="municipalBindingCard">
<input type="radio" name="bindingType" value="municipal" class="binding-radio" />
<div class="binding-title">绑定市级单位</div>
<div class="binding-desc">绑定到选定的市级单位,该单位及其下属可见</div>
</label>
<label class="binding-card" id="districtBindingCard">
<input type="radio" name="bindingType" value="district" class="binding-radio" />
<div class="binding-title">绑定区级单位</div>
<div class="binding-desc">绑定到选定的区级单位,只有该单位可见</div>
</label>
</div>
</div>
<div id="municipalSection" class="form-group" style="display: none;">
<label class="form-label">选择市级单位</label>
<select id="municipalDept" class="form-select">
<option value="">请选择市级单位</option>
</select>
<div class="form-help">市级单位可以看到自身和所有下属区级单位的许可文件</div>
</div>
<div id="districtSection" class="form-group" style="display: none;">
<label class="form-label">选择区级单位</label>
<select id="districtDept" class="form-select" disabled>
<option value="">请先选择市级单位</option>
</select>
<div class="form-help">区级单位只能看到自身绑定的许可文件</div>
</div>
<div class="form-group">
<label class="form-label">上传说明(可选)</label>
<textarea id="uploadNote" class="form-textarea" placeholder="输入本次上传的说明信息..."></textarea>
</div>
</div>
<!-- 提交区域 -->
<div class="upload-section">
<div class="submit-section">
<button id="cancelBtn" class="btn" style="background: #e5e7eb; color: #374151;">取消</button>
<button id="submitBtn" class="btn btn-primary" disabled>开始上传</button>
</div>
</div>
<!-- 状态消息 -->
<div id="errorMessage" class="error-message" style="display: none;"></div>
<div id="successMessage" class="success-message" style="display: none;"></div>
<!-- 上传进度 -->
<div id="uploadProgress" class="upload-section" style="display: none;">
<h2 class="section-title">上传进度</h2>
<div id="progressContent"></div>
</div>
</div>
<script>
// 全局变量
let selectedFiles = [];
let municipalTree = [];
// DOM元素
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const submitBtn = document.getElementById('submitBtn');
const cancelBtn = document.getElementById('cancelBtn');
const errorMessage = document.getElementById('errorMessage');
const successMessage = document.getElementById('successMessage');
const municipalDept = document.getElementById('municipalDept');
const districtDept = document.getElementById('districtDept');
const municipalSection = document.getElementById('municipalSection');
const districtSection = document.getElementById('districtSection');
const uploadProgress = document.getElementById('uploadProgress');
const progressContent = document.getElementById('progressContent');
// 页面初始化
document.addEventListener('DOMContentLoaded', function() {
loadMunicipalDepartments();
bindEvents();
});
// 绑定事件
function bindEvents() {
// 文件选择
fileInput.addEventListener('change', handleFileSelect);
// 拖拽上传
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
// 绑定方式选择
document.querySelectorAll('input[name="bindingType"]').forEach(radio => {
radio.addEventListener('change', handleBindingTypeChange);
});
// 市级单位选择变更
municipalDept.addEventListener('change', handleMunicipalChange);
// 提交/取消
submitBtn.addEventListener('click', handleSubmit);
cancelBtn.addEventListener('click', handleCancel);
}
// 加载市级单位
async function loadMunicipalDepartments() {
try {
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/service-departments/tree');
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '加载市级单位失败');
}
municipalTree = data.data.tree || [];
renderMunicipalOptions(municipalTree);
} catch (error) {
console.error('加载市级单位失败:', error);
showError('加载市级单位失败: ' + error.message);
}
}
// 渲染市级单位选项
function renderMunicipalOptions(tree) {
municipalDept.innerHTML = '<option value="">请选择市级单位</option>';
tree.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
option.textContent = `${dept.name} (${dept.code})`;
municipalDept.appendChild(option);
});
}
// 处理文件选择
function handleFileSelect(e) {
const files = Array.from(e.target.files);
addFiles(files);
}
// 处理拖拽悬停
function handleDragOver(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
}
// 处理拖拽离开
function handleDragLeave(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
}
// 处理拖拽放下
function handleDrop(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
addFiles(files);
}
// 添加文件
function addFiles(files) {
// 过滤文件类型
const validFiles = files.filter(file => {
const validTypes = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
const validExt = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
if (!validTypes.includes(file.type) && !validExt) {
showError(`文件 "${file.name}" 不是有效的Excel格式`);
return false;
}
if (file.size > 50 * 1024 * 1024) {
showError(`文件 "${file.name}" 超过50MB大小限制`);
return false;
}
return true;
});
validFiles.forEach(file => {
selectedFiles.push({
file: file,
id: Date.now() + Math.random(),
status: 'pending'
});
});
renderFileList();
updateSubmitButton();
}
// 渲染文件列表
function renderFileList() {
if (selectedFiles.length === 0) {
fileList.style.display = 'none';
return;
}
fileList.style.display = 'block';
fileList.innerHTML = selectedFiles.map(item => `
<div class="file-item" data-id="${item.id}">
<div class="file-info">
<div class="file-icon">📊</div>
<div class="file-details">
<div class="file-name">${escapeHtml(item.file.name)}</div>
<div class="file-size">${formatFileSize(item.file.size)}</div>
</div>
</div>
<div class="file-status ${item.status}">${getStatusText(item.status)}</div>
<div class="file-actions">
<button class="btn btn-small btn-danger" onclick="removeFile('${item.id}')">移除</button>
</div>
</div>
`).join('');
}
// 移除文件
function removeFile(id) {
selectedFiles = selectedFiles.filter(item => item.id !== id);
renderFileList();
updateSubmitButton();
}
// 处理绑定方式变更
function handleBindingTypeChange(e) {
const value = e.target.value;
// 更新卡片选中状态
document.querySelectorAll('.binding-card').forEach(card => {
card.classList.remove('selected');
});
e.target.closest('.binding-card').classList.add('selected');
// 显示/隐藏对应的选择区域
municipalSection.style.display = value === 'municipal' ? 'block' : 'none';
districtSection.style.display = value === 'district' ? 'block' : 'none';
// 重置区级单位选择
districtDept.innerHTML = '<option value="">请先选择市级单位</option>';
districtDept.disabled = value === 'district' && !municipalDept.value;
}
// 处理市级单位选择变更
async function handleMunicipalChange() {
const municipalId = municipalDept.value;
// 重置区级单位
districtDept.innerHTML = '<option value="">请选择区级单位</option>';
districtDept.disabled = !municipalId;
if (!municipalId) {
return;
}
// 加载区级单位
try {
const response = await fetch(
`/fs-ai-asistant/api/workflow/lawrisk/admin/departments/children?parent_id=${municipalId}`
);
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '加载区级单位失败');
}
const children = data.data.units || [];
districtDept.innerHTML = '<option value="">请选择区级单位</option>';
children.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
option.textContent = `${dept.name} (${dept.code})`;
districtDept.appendChild(option);
});
} catch (error) {
console.error('加载区级单位失败:', error);
showError('加载区级单位失败: ' + error.message);
}
}
// 处理提交
async function handleSubmit() {
if (selectedFiles.length === 0) {
showError('请先选择要上传的文件');
return;
}
const bindingType = document.querySelector('input[name="bindingType"]:checked').value;
let boundDepartmentId = null;
if (bindingType === 'municipal' && municipalDept.value) {
boundDepartmentId = municipalDept.value;
} else if (bindingType === 'district' && districtDept.value) {
boundDepartmentId = districtDept.value;
}
// 开始上传第一个文件
await uploadFile(0, boundDepartmentId);
}
// 上传文件
async function uploadFile(index, boundDepartmentId) {
if (index >= selectedFiles.length) {
// 所有文件上传完成
showSuccess(`成功上传 ${selectedFiles.length} 个文件`);
resetForm();
return;
}
const item = selectedFiles[index];
item.status = 'uploading';
renderFileList();
try {
const formData = new FormData();
formData.append('file', item.file);
// 添加绑定信息
if (boundDepartmentId) {
formData.append('bound_department_id', boundDepartmentId);
}
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/admin/permit-import/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || '上传失败');
}
item.status = 'success';
renderFileList();
// 显示上传进度
showProgress(index + 1, selectedFiles.length, item.file.name);
// 继续上传下一个文件
setTimeout(() => uploadFile(index + 1, boundDepartmentId), 1000);
} catch (error) {
console.error('上传失败:', error);
item.status = 'error';
renderFileList();
showError(`文件 "${item.file.name}" 上传失败: ` + error.message);
}
}
// 显示上传进度
function showProgress(current, total, filename) {
uploadProgress.style.display = 'block';
progressContent.innerHTML = `
<div style="margin-bottom: 12px;">正在上传: ${escapeHtml(filename)} (${current}/${total})</div>
<div style="width: 100%; height: 8px; background: #f3f4f6; border-radius: 4px; overflow: hidden;">
<div style="width: ${(current / total) * 100}%; height: 100%; background: #6366f1; transition: width 0.3s;"></div>
</div>
`;
}
// 处理取消
function handleCancel() {
if (confirm('确定要取消上传吗?已选择的文件将被清除。')) {
resetForm();
}
}
// 重置表单
function resetForm() {
selectedFiles = [];
fileInput.value = '';
fileList.style.display = 'none';
uploadProgress.style.display = 'none';
submitBtn.disabled = true;
// 重置绑定设置
document.querySelector('input[name="bindingType"][value="none"]').checked = true;
handleBindingTypeChange({ target: document.querySelector('input[name="bindingType"][value="none"]') });
municipalDept.value = '';
districtDept.value = '';
districtDept.disabled = true;
document.getElementById('uploadNote').value = '';
}
// 更新提交按钮状态
function updateSubmitButton() {
submitBtn.disabled = selectedFiles.length === 0;
}
// 获取状态文本
function getStatusText(status) {
const statusMap = {
'pending': '等待上传',
'uploading': '上传中...',
'success': '上传成功',
'error': '上传失败'
};
return statusMap[status] || status;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// HTML转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
setTimeout(() => errorMessage.style.display = 'none', 5000);
}
// 显示成功信息
function showSuccess(message) {
successMessage.textContent = message;
successMessage.style.display = 'block';
setTimeout(() => successMessage.style.display = 'none', 5000);
}
</script>
</body>
</html>