fs-lawrisk/static/permit_upload.html

746 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
.hint-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-left: 6px;
border-radius: 50%;
border: 1px solid #9ca3af;
color: #374151;
font-size: 12px;
font-weight: 700;
background: #f9fafb;
cursor: default;
}
.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">
绑定单位
<span class="hint-icon" title="自动绑定到当前账号所属单位,该单位及下级可见,上级可下钻查看,横向同级不可见。">i</span>
</label>
<div class="binding-card selected"
style="display:flex; align-items:center; justify-content:space-between;">
<div>
<div class="binding-title" id="boundDeptLabel">加载中...</div>
<div class="binding-desc">绑定到本单位,权限范围由组织层级决定</div>
</div>
<div class="pill" id="boundDeptHint">自动绑定</div>
</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 currentDepartment = null;
// 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 boundDeptLabel = document.getElementById('boundDeptLabel');
const uploadProgress = document.getElementById('uploadProgress');
const progressContent = document.getElementById('progressContent');
// 页面初始化
document.addEventListener('DOMContentLoaded', function () {
loadCurrentUserDept();
bindEvents();
});
// 绑定事件
function bindEvents() {
// 文件选择
fileInput.addEventListener('change', handleFileSelect);
// 拖拽上传
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
// 提交/取消
submitBtn.addEventListener('click', handleSubmit);
cancelBtn.addEventListener('click', handleCancel);
}
// 处理文件选择
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();
}
async function loadCurrentUserDept() {
try {
const resp = await fetch('/fs-ai-asistant/api/workflow/lawrisk/auth/me');
const data = await resp.json();
if (!data.authenticated) {
throw new Error('未登录');
}
const dept = data.user && data.user.department;
currentDepartment = dept;
if (dept && dept.name) {
boundDeptLabel.textContent = `${dept.name}${dept.code ? ' · ' + dept.code : ''}`;
boundDeptHint.textContent = '自动绑定';
submitBtn.disabled = false;
} else {
boundDeptLabel.textContent = '未绑定单位';
boundDeptHint.textContent = '请先绑定';
submitBtn.disabled = true;
showError('当前账号未绑定单位,无法上传文件。');
}
} catch (error) {
console.error('加载当前用户失败:', error);
boundDeptLabel.textContent = '加载失败';
boundDeptHint.textContent = '请重新登录';
submitBtn.disabled = true;
showError('无法获取当前用户信息,请重新登录。');
}
}
// 处理提交
async function handleSubmit() {
if (selectedFiles.length === 0) {
showError('请先选择要上传的文件');
return;
}
const boundDepartmentId = currentDepartment ? currentDepartment.id : null;
const bindingMode = 'department';
if (!boundDepartmentId) {
showError('当前账号未绑定单位,请先绑定后再上传。');
return;
}
// 开始上传第一个文件
await uploadFile(0, boundDepartmentId, bindingMode);
}
// 上传文件
async function uploadFile(index, boundDepartmentId, bindingMode) {
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);
}
formData.append('binding_mode', bindingMode || 'auto');
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, bindingMode), 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.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>