616 lines
24 KiB
HTML
616 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>图片上传与识别</title>
|
||
<style>
|
||
body {margin: 0;font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;background: #fafafa;}
|
||
/* 导航栏样式 */
|
||
.sidebar {position: fixed;top: 0;left: 0;width: 180px;height: 100vh;background: #1e1e2e;color: white;padding: 20px;box-shadow: 2px 0 5px rgba(0,0,0,0.1);z-index: 1000;display: flex;
|
||
flex-direction: column;align-items: center;}
|
||
.user-id {text-align: center;margin-bottom: 0px;}
|
||
.sidebar h3 {margin-top: 0;font-size: 18px;color: #add8e6;text-align: center; margin-bottom: 20px;}
|
||
.navigation-links {width: 100%;margin-top: 60px;}
|
||
.sidebar a,
|
||
.sidebar button {display: block;color: #8be9fd;text-decoration: none;margin: 10px 0;font-size: 16px;padding: 15px;border-radius: 4px;background: transparent;
|
||
border: none;cursor: pointer; width: calc(100% - 40px);text-align: left;transition: all 0.2s ease;}
|
||
.sidebar a:hover,
|
||
.sidebar button:hover {color: #ff79c6;background-color: rgba(139, 233, 253, 0.2);}
|
||
/* 主内容区 - 改进后的样式 */
|
||
.main-content {margin-left: 200px;padding: 20px;color: #333;}
|
||
.container { max-width: 1200px;margin: 0 auto;background: #fff;border-radius: 14px;box-shadow: 0 10px 24px rgba(31,35,40,0.08);
|
||
padding: 24px;}
|
||
.header {display: flex;align-items: center;justify-content: space-between;margin-bottom: 12px;}
|
||
.header h2 {margin: 0; color: #1e293b;}
|
||
.header p {margin: 5px 0 0 0;color: #64748b;font-size: 14px;}
|
||
.upload-section { background: #f8fafc; border: 2px dashed #cbd5e1; border-radius: 12px;padding: 32px; text-align: center;transition: all 0.3s ease;
|
||
margin-bottom: 24px;}
|
||
.upload-section:hover {border-color: #4f46e5; background: #f1f5f9; }
|
||
.upload-section.drag-over {border-color: #4f46e5; background: #e0e7ff; }
|
||
.upload-section input[type="file"] {margin: 15px 0;}
|
||
.btn {padding: 10px 16px;border: none;border-radius: 8px;cursor: pointer;margin: 0 4px;font-size: 14px;transition: all 0.2s ease; }
|
||
.btn-primary { background: #4f46e5; color: #fff; }
|
||
.btn-primary:hover { background: #4338ca;}
|
||
.btn-secondary {background: #e2e8f0;color: #334155; }
|
||
.btn-secondary:hover { background: #cbd5e1;}
|
||
.btn-danger { background: #ef4444;color: #fff;}
|
||
.btn-danger:hover { background: #dc2626;}
|
||
.preview-container {display: flex; gap: 24px; margin: 24px 0;}
|
||
@media (max-width: 768px) {
|
||
.preview-container {flex-direction: column;}
|
||
}
|
||
.preview-box {flex: 1; text-align: center; }
|
||
.preview-box h3 {margin-top: 0;color: #334155; }
|
||
.preview-box img { max-width: 100%;max-height: 300px;border: 1px solid #e2e8f0;border-radius: 8px;object-fit: contain;}
|
||
.preview-list {display: grid;grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));gap: 12px; margin-top: 20px;}
|
||
.preview-item {position: relative;}
|
||
.preview-item img {width: 100%;max-height: 220px;border: 1px solid #e2e8f0;border-radius: 8px;object-fit: contain;}
|
||
.preview-remove {position: absolute;top: 6px;right: 6px;border: none;border-radius: 999px;background: rgba(15,23,42,0.8);color: #fff;width: 24px;height: 24px;cursor: pointer;display: flex;align-items: center;justify-content: center;font-size: 14px;line-height: 1;}
|
||
.result-box {flex: 1;}
|
||
.result-box h3 { margin-top: 0; color: #334155;}
|
||
.form-controls { display: flex;gap: 8px;margin-bottom: 12px;flex-wrap: wrap;}
|
||
.pending-item { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
|
||
.pending-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid #f1f5f9; padding-bottom: 12px; }
|
||
.pending-item-title { font-weight: 600; color: #1e293b; font-size: 16px; }
|
||
.pending-item-body { display: flex; gap: 20px; }
|
||
.pending-item-preview { flex: 0 0 240px; }
|
||
.pending-item-preview img { width: 100%; border-radius: 8px; border: 1px solid #f1f5f9; }
|
||
.pending-item-edit { flex: 1; }
|
||
.pending-item-footer { margin-top: 16px; text-align: right; }
|
||
@media (max-width: 992px) {
|
||
.pending-item-body { flex-direction: column; }
|
||
.pending-item-preview { flex: 0 0 auto; }
|
||
}
|
||
.form-row {display: grid;grid-template-columns: 1fr 1fr auto;gap: 8px; margin-bottom: 6px; align-items: center;}
|
||
.form-row input {padding: 8px;border: 1px solid #cbd5e1;border-radius: 4px; width: 100%; box-sizing: border-box;}
|
||
.kv-form-container {border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; max-height: 400px; overflow: auto; margin-bottom: 12px; background: #f8fafc;}
|
||
.form-header { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; margin-bottom: 8px; padding: 0 4px; font-weight: 600; color: #475569; font-size: 14px;}
|
||
.result-textarea { width: 100%; min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; resize: vertical; box-sizing: border-box; }
|
||
.status-message { padding: 10px; margin: 10px 0; border-radius: 6px; display: none; }
|
||
.status-message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||
.status-message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||
.action-buttons { margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.progress {position: relative; height: 12px; background: #e2e8f0; border-radius: 8px; overflow: hidden;}
|
||
.progress-bar {height: 100%; width: 0; background: linear-gradient(90deg, #4f46e5 0%, #60a5fa 100%); transition: width .2s ease;}
|
||
.progress-wrap {display:none; margin-top: 8px;}
|
||
.progress-text {margin-top: 6px; font-size: 12px; color: #334155;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 左侧固定栏目 -->
|
||
<div class="sidebar">
|
||
<div class="user-id">
|
||
<h3>你好,{{ username|default:"访客" }}</h3>
|
||
</div>
|
||
<div class="navigation-links">
|
||
<a href="{% url 'main:home' %}">返回主页</a>
|
||
<a id="logoutBtn">退出登录</a>
|
||
<div id="logoutMsg"></div>
|
||
{% csrf_token %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="main-content">
|
||
<div class="container">
|
||
<div class="header">
|
||
<div>
|
||
<h2>图片与PDF上传识别</h2>
|
||
<p>选择图片或PDF文件后上传,服务端调用大模型解析为可编辑的 JSON,再确认入库。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-section" id="dropArea">
|
||
<h3>上传文件</h3>
|
||
<p>点击下方按钮选择图片或PDF文件,或拖拽文件到此区域</p>
|
||
<p style="margin: 8px 0 0; font-size: 13px; color: #64748b;">单次最多上传 {{ max_single_upload_count|default:"3" }} 个文件。</p>
|
||
<form id="uploadForm" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<input type="file" id="fileInput" name="file" accept="image/*,.pdf" multiple />
|
||
<span id="fileHint" class="muted"></span>
|
||
<div id="previewList" class="preview-list"></div>
|
||
<br>
|
||
<button type="submit" class="btn btn-primary">上传并识别</button>
|
||
</form>
|
||
<div class="status-message" id="uploadMsg"></div>
|
||
<div class="progress-wrap" id="progressWrap">
|
||
<div class="progress"><div class="progress-bar" id="progressBar"></div></div>
|
||
<div class="progress-text" id="progressText"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="preview-container">
|
||
<div class="result-box">
|
||
<h3>待处理文件列表</h3>
|
||
<div id="pendingItems" class="pending-list">
|
||
<!-- 这里将动态生成每个文件的预览和编辑区域 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<button id="confirmBtn" class="btn btn-primary" disabled>确认并入库</button>
|
||
<button id="clearBtn" class="btn btn-secondary" type="button">清空</button>
|
||
<span id="confirmMsg" class="muted"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function getCookie(name) {
|
||
const value = `; ${document.cookie}`;
|
||
const parts = value.split(`; ${name}=`);
|
||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||
}
|
||
|
||
const uploadForm = document.getElementById('uploadForm');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const fileHint = document.getElementById('fileHint');
|
||
const previewList = document.getElementById('previewList');
|
||
const pendingItems = document.getElementById('pendingItems');
|
||
const uploadMsg = document.getElementById('uploadMsg');
|
||
const confirmBtn = document.getElementById('confirmBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const confirmMsg = document.getElementById('confirmMsg');
|
||
const dropArea = document.getElementById('dropArea');
|
||
const progressWrap = document.getElementById('progressWrap');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressText = document.getElementById('progressText');
|
||
const MAX_SINGLE_UPLOAD_COUNT = Number('{{ max_single_upload_count|default:"3" }}');
|
||
|
||
let currentItems = []; // 存储当前待处理的所有文件结果
|
||
let selectedFiles = [];
|
||
|
||
function setProgress(p, text){
|
||
const v = Math.max(0, Math.min(100, Math.round(p||0)));
|
||
progressBar.style.width = v + '%';
|
||
progressText.textContent = (text||'') + (text? ' ' : '') + v + '%';
|
||
}
|
||
function showProgress(){
|
||
progressWrap.style.display = 'block';
|
||
}
|
||
function hideProgress(){
|
||
progressWrap.style.display = 'none';
|
||
setProgress(0, '');
|
||
}
|
||
|
||
async function convertToJpeg(file){
|
||
const url = URL.createObjectURL(file);
|
||
let img;
|
||
try{
|
||
const blob = await fetch(url).then(r=>r.blob());
|
||
img = await createImageBitmap(blob);
|
||
}catch(e){
|
||
img = await new Promise((resolve,reject)=>{const i=new Image();i.onload=()=>resolve(i);i.onerror=reject;i.src=url;});
|
||
}
|
||
URL.revokeObjectURL(url);
|
||
const maxDim = 2000;
|
||
const w = img.width;
|
||
const h = img.height;
|
||
const scale = Math.min(1, maxDim/Math.max(w,h));
|
||
const nw = Math.round(w*scale);
|
||
const nh = Math.round(h*scale);
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = nw;
|
||
canvas.height = nh;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0, nw, nh);
|
||
const blob = await new Promise(resolve=>canvas.toBlob(resolve,'image/jpeg',0.82));
|
||
const name = (file.name||'image').replace(/\.[^/.]+$/, '') + '.jpg';
|
||
return new File([blob], name, {type:'image/jpeg'});
|
||
}
|
||
|
||
// 拖拽上传功能
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
dropArea.addEventListener(eventName, preventDefaults, false);
|
||
});
|
||
|
||
function preventDefaults(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
|
||
['dragenter', 'dragover'].forEach(eventName => {
|
||
dropArea.addEventListener(eventName, highlight, false);
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(eventName => {
|
||
dropArea.addEventListener(eventName, unhighlight, false);
|
||
});
|
||
|
||
function highlight() {
|
||
dropArea.classList.add('drag-over');
|
||
}
|
||
|
||
function unhighlight() {
|
||
dropArea.classList.remove('drag-over');
|
||
}
|
||
|
||
dropArea.addEventListener('drop', handleDrop, false);
|
||
|
||
function handleDrop(e) {
|
||
const dt = e.dataTransfer;
|
||
const files = dt.files;
|
||
if (files.length) {
|
||
addFiles(files);
|
||
}
|
||
}
|
||
|
||
function setPreviewList(urls) {
|
||
previewList.innerHTML = '';
|
||
(urls || []).forEach((url, index) => {
|
||
if (!url) return;
|
||
const item = document.createElement('div');
|
||
item.className = 'preview-item';
|
||
item.dataset.index = String(index);
|
||
const img = document.createElement('img');
|
||
img.src = url;
|
||
img.alt = '预览';
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'preview-remove';
|
||
btn.textContent = '×';
|
||
btn.onclick = () => {
|
||
const idx = Number(item.dataset.index);
|
||
if (!Number.isNaN(idx)) {
|
||
selectedFiles.splice(idx, 1);
|
||
const urls = selectedFiles.map(f => {
|
||
if (f.name.toLowerCase().endsWith('.pdf')) {
|
||
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNlZjQ0NDQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTQgMmgyYTIgMiAwIDAgMSAyIDJ2MTZhMiAyIDAgMCAxLTIgMmgtMTJhMiAyIDAgMCAxLTItMlY0YTIgMiAwIDAgMSAyLTJoMiIvPjxwYXRoIGQ9Ik0xNCAydjRjMCAxLjEgLjkgMiAyIDJoNCIvPjxwYXRoIGQ9Ik03IDloNSIvPjxwYXRoIGQ9Ik03IDEzaDUiLz48cGF0aCBkPSJNNyAxN2g4Ii8+PC9zdmc+';
|
||
}
|
||
return URL.createObjectURL(f);
|
||
});
|
||
setPreviewList(urls);
|
||
updateFileHint();
|
||
setTimeout(() => urls.forEach(u => { if (u.startsWith('blob:')) URL.revokeObjectURL(u); }), 0);
|
||
}
|
||
};
|
||
item.appendChild(img);
|
||
item.appendChild(btn);
|
||
previewList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
function updateFileHint() {
|
||
const count = selectedFiles.length;
|
||
fileHint.textContent = count ? `已选择 ${count} 个文件` : '未选择文件';
|
||
}
|
||
|
||
function addFiles(files) {
|
||
const incoming = Array.from(files || []).filter(f => f && (f.type.startsWith('image/') || f.name.toLowerCase().endsWith('.pdf')));
|
||
const existingKeys = new Set(selectedFiles.map(f => `${f.name}|${f.size}|${f.lastModified}`));
|
||
const rejected = [];
|
||
incoming.forEach(f => {
|
||
const key = `${f.name}|${f.size}|${f.lastModified}`;
|
||
if (!existingKeys.has(key) && selectedFiles.length < MAX_SINGLE_UPLOAD_COUNT) {
|
||
existingKeys.add(key);
|
||
selectedFiles.push(f);
|
||
} else if (!existingKeys.has(key) && selectedFiles.length >= MAX_SINGLE_UPLOAD_COUNT) {
|
||
rejected.push(f.name);
|
||
}
|
||
});
|
||
if (rejected.length) {
|
||
uploadMsg.textContent = `单次最多上传 ${MAX_SINGLE_UPLOAD_COUNT} 个文件,以下文件未加入:${rejected.join('、')}`;
|
||
uploadMsg.className = 'status-message error';
|
||
uploadMsg.style.display = 'block';
|
||
}
|
||
const urls = selectedFiles.map(f => {
|
||
if (f.name.toLowerCase().endsWith('.pdf')) {
|
||
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNlZjQ0NDQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTQgMmgyYTIgMiAwIDAgMSAyIDJ2MTZhMiAyIDAgMCAxLTIgMmgtMTJhMiAyIDAgMCAxLTItMlY0YTIgMiAwIDAgMSAyLTJoMiIvPjxwYXRoIGQ9Ik0xNCAydjRjMCAxLjEgLjkgMiAyIDJoNCIvPjxwYXRoIGQ9Ik03IDloNSIvPjxwYXRoIGQ9Ik03IDEzaDUiLz48cGF0aCBkPSJNNyAxN2g4Ii8+PC9zdmc+';
|
||
}
|
||
return URL.createObjectURL(f);
|
||
});
|
||
setPreviewList(urls);
|
||
updateFileHint();
|
||
setTimeout(() => urls.forEach(u => { if (u.startsWith('blob:')) URL.revokeObjectURL(u); }), 0);
|
||
}
|
||
|
||
fileInput.addEventListener('change', function(e) {
|
||
addFiles(e.target.files || []);
|
||
fileInput.value = '';
|
||
});
|
||
|
||
function createKvRow(k = '', v = '', onInput) {
|
||
const row = document.createElement('div');
|
||
row.className = 'form-row';
|
||
const keyInput = document.createElement('input');
|
||
keyInput.type = 'text';
|
||
keyInput.placeholder = '字段名';
|
||
keyInput.value = k;
|
||
const valInput = document.createElement('input');
|
||
valInput.type = 'text';
|
||
valInput.placeholder = '字段值';
|
||
valInput.value = typeof v === 'object' ? JSON.stringify(v) : (v ?? '');
|
||
const delBtn = document.createElement('button');
|
||
delBtn.type = 'button';
|
||
delBtn.className = 'btn btn-danger';
|
||
delBtn.textContent = '删除';
|
||
|
||
delBtn.onclick = () => {
|
||
const container = row.parentElement;
|
||
if (container.querySelectorAll('.form-row').length > 1) {
|
||
container.removeChild(row);
|
||
} else {
|
||
keyInput.value = '';
|
||
valInput.value = '';
|
||
}
|
||
if (onInput) onInput();
|
||
};
|
||
|
||
keyInput.oninput = onInput;
|
||
valInput.oninput = onInput;
|
||
|
||
row.appendChild(keyInput);
|
||
row.appendChild(valInput);
|
||
row.appendChild(delBtn);
|
||
return row;
|
||
}
|
||
|
||
function renderPendingItems(items) {
|
||
pendingItems.innerHTML = '';
|
||
currentItems = items;
|
||
|
||
items.forEach((item, index) => {
|
||
const itemEl = document.createElement('div');
|
||
itemEl.className = 'pending-item';
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'pending-item-header';
|
||
header.innerHTML = `<span class="pending-item-title">${index + 1}. ${item.name}</span>`;
|
||
|
||
const removeBtn = document.createElement('button');
|
||
removeBtn.className = 'btn btn-danger';
|
||
removeBtn.textContent = '忽略此项';
|
||
removeBtn.onclick = () => {
|
||
currentItems.splice(index, 1);
|
||
renderPendingItems(currentItems);
|
||
};
|
||
header.appendChild(removeBtn);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'pending-item-body';
|
||
|
||
const preview = document.createElement('div');
|
||
preview.className = 'pending-item-preview';
|
||
const mainImg = document.createElement('img');
|
||
mainImg.src = item.image_urls[0];
|
||
preview.appendChild(mainImg);
|
||
if (item.image_urls.length > 1) {
|
||
const hint = document.createElement('p');
|
||
hint.className = 'muted';
|
||
hint.style.textAlign = 'center';
|
||
hint.textContent = `共 ${item.image_urls.length} 页`;
|
||
preview.appendChild(hint);
|
||
}
|
||
|
||
const edit = document.createElement('div');
|
||
edit.className = 'pending-item-edit';
|
||
|
||
const controls = document.createElement('div');
|
||
controls.className = 'form-controls';
|
||
const addBtn = document.createElement('button');
|
||
addBtn.className = 'btn btn-secondary';
|
||
addBtn.textContent = '添加字段';
|
||
const syncBtn = document.createElement('button');
|
||
syncBtn.className = 'btn btn-secondary';
|
||
syncBtn.textContent = '刷新表单';
|
||
controls.appendChild(addBtn);
|
||
controls.appendChild(syncBtn);
|
||
|
||
const kvForm = document.createElement('div');
|
||
kvForm.className = 'kv-form-container';
|
||
kvForm.innerHTML = '<div class="form-header"><div>字段名</div><div>字段值</div><div>操作</div></div>';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'result-textarea';
|
||
|
||
const syncData = () => {
|
||
const obj = {};
|
||
kvForm.querySelectorAll('.form-row').forEach(row => {
|
||
const inputs = row.querySelectorAll('input');
|
||
const k = inputs[0].value.trim();
|
||
if (!k) return;
|
||
try { obj[k] = JSON.parse(inputs[1].value); } catch(e) { obj[k] = inputs[1].value; }
|
||
});
|
||
item.data = obj;
|
||
textarea.value = JSON.stringify(obj, null, 2);
|
||
};
|
||
|
||
Object.entries(item.data).forEach(([k, v]) => {
|
||
kvForm.appendChild(createKvRow(k, v, syncData));
|
||
});
|
||
if (kvForm.querySelectorAll('.form-row').length === 0) {
|
||
kvForm.appendChild(createKvRow('', '', syncData));
|
||
}
|
||
|
||
addBtn.onclick = () => {
|
||
kvForm.appendChild(createKvRow('', '', syncData));
|
||
syncData();
|
||
};
|
||
|
||
syncBtn.onclick = () => {
|
||
try {
|
||
const obj = JSON.parse(textarea.value);
|
||
kvForm.innerHTML = '<div class="form-header"><div>字段名</div><div>字段值</div><div>操作</div></div>';
|
||
Object.entries(obj).forEach(([k, v]) => kvForm.appendChild(createKvRow(k, v, syncData)));
|
||
item.data = obj;
|
||
} catch(e) { alert('JSON格式错误'); }
|
||
};
|
||
|
||
textarea.value = JSON.stringify(item.data, null, 2);
|
||
textarea.oninput = () => { item.data = JSON.parse(textarea.value); };
|
||
|
||
edit.appendChild(controls);
|
||
edit.appendChild(kvForm);
|
||
edit.appendChild(textarea);
|
||
|
||
body.appendChild(preview);
|
||
body.appendChild(edit);
|
||
|
||
itemEl.appendChild(header);
|
||
itemEl.appendChild(body);
|
||
pendingItems.appendChild(itemEl);
|
||
});
|
||
|
||
confirmBtn.disabled = items.length === 0;
|
||
}
|
||
|
||
uploadForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
uploadMsg.textContent = '';
|
||
confirmMsg.textContent = '';
|
||
confirmBtn.disabled = true;
|
||
previewList.innerHTML = '';
|
||
pendingItems.innerHTML = '';
|
||
currentItems = [];
|
||
|
||
if (!selectedFiles.length) {
|
||
uploadMsg.textContent = '请选择文件';
|
||
uploadMsg.className = 'status-message error';
|
||
uploadMsg.style.display = 'block';
|
||
return;
|
||
}
|
||
if (selectedFiles.length > MAX_SINGLE_UPLOAD_COUNT) {
|
||
uploadMsg.textContent = `单次最多上传 ${MAX_SINGLE_UPLOAD_COUNT} 个文件,请分批上传`;
|
||
uploadMsg.className = 'status-message error';
|
||
uploadMsg.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
showProgress();
|
||
setProgress(5, '预处理中');
|
||
const formData = new FormData();
|
||
for (let i = 0; i < selectedFiles.length; i++) {
|
||
const file = selectedFiles[i];
|
||
if (file.type.startsWith('image/')) {
|
||
setProgress(5 + Math.round((i/selectedFiles.length)*45), '转换图片');
|
||
try {
|
||
const jpegFile = await convertToJpeg(file);
|
||
formData.append('file', jpegFile);
|
||
} catch (_) {
|
||
formData.append('file', file);
|
||
}
|
||
} else {
|
||
formData.append('file', file);
|
||
}
|
||
}
|
||
|
||
try {
|
||
let prog = 50;
|
||
setProgress(prog, '识别中');
|
||
const timer = setInterval(() => {
|
||
prog = Math.min(95, prog + 1);
|
||
setProgress(prog, '识别中');
|
||
}, 200);
|
||
|
||
const resp = await fetch('/elastic/upload/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'X-CSRFToken': getCookie('csrftoken') || '' },
|
||
body: formData,
|
||
});
|
||
clearInterval(timer);
|
||
const ct = (resp.headers.get('content-type') || '').toLowerCase();
|
||
if (!ct.includes('application/json')) {
|
||
const text = await resp.text();
|
||
throw new Error(text ? String(text).slice(0, 200) : `HTTP ${resp.status}`);
|
||
}
|
||
const data = await resp.json();
|
||
if (!resp.ok || data.status !== 'success') {
|
||
throw new Error(data.message || '上传识别失败');
|
||
}
|
||
setProgress(100, '识别完成');
|
||
uploadMsg.textContent = data.message || '识别成功';
|
||
uploadMsg.className = 'status-message success';
|
||
uploadMsg.style.display = 'block';
|
||
|
||
renderPendingItems(data.items || []);
|
||
setTimeout(hideProgress, 800);
|
||
} catch (e) {
|
||
uploadMsg.textContent = e.message || '发生错误';
|
||
uploadMsg.className = 'status-message error';
|
||
uploadMsg.style.display = 'block';
|
||
progressText.textContent = '识别失败';
|
||
}
|
||
});
|
||
|
||
confirmBtn.addEventListener('click', async () => {
|
||
confirmMsg.textContent = '正在录入...';
|
||
try {
|
||
const payload = {
|
||
items: currentItems.map(it => ({
|
||
data: it.data,
|
||
image: it.images
|
||
}))
|
||
};
|
||
const resp = await fetch('/elastic/confirm/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': getCookie('csrftoken') || ''
|
||
},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || data.status !== 'success') {
|
||
throw new Error(data.message || '录入失败');
|
||
}
|
||
confirmMsg.textContent = data.message || '录入成功';
|
||
confirmMsg.style.color = '#179957';
|
||
// 录入成功后清空待处理列表
|
||
pendingItems.innerHTML = '';
|
||
currentItems = [];
|
||
selectedFiles = [];
|
||
updateFileHint();
|
||
confirmBtn.disabled = true;
|
||
} catch (e) {
|
||
confirmMsg.textContent = e.message || '发生错误';
|
||
confirmMsg.style.color = '#d14343';
|
||
}
|
||
});
|
||
|
||
clearBtn.addEventListener('click', () => {
|
||
fileInput.value = '';
|
||
previewList.innerHTML = '';
|
||
pendingItems.innerHTML = '';
|
||
uploadMsg.textContent = '';
|
||
confirmMsg.textContent = '';
|
||
confirmBtn.disabled = true;
|
||
currentItems = [];
|
||
selectedFiles = [];
|
||
updateFileHint();
|
||
});
|
||
|
||
updateFileHint();
|
||
|
||
// 退出登录处理
|
||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||
const msg = document.getElementById('logoutMsg');
|
||
msg.textContent = '';
|
||
const csrftoken = getCookie('csrftoken');
|
||
try {
|
||
const resp = await fetch('/accounts/logout/', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken || ''
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.ok) {
|
||
throw new Error('登出失败');
|
||
}
|
||
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
document.cookie = 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||
window.location.href = data.redirect_url;
|
||
} catch (e) {
|
||
msg.textContent = e.message || '发生错误';
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|