Files
Achievement_Inputing/elastic/templates/elastic/upload.html
2025-11-17 19:23:51 +08:00

384 lines
13 KiB
HTML
Raw 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" />
<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;}
.result-box {flex: 1;}
.result-box h3 { margin-top: 0; color: #334155;}
.form-controls { display: flex;gap: 8px;margin-bottom: 12px;flex-wrap: wrap;}
#kvForm {border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; max-height: 300px; overflow: auto;margin-bottom: 12px;background: white;}
.form-row {display: grid;grid-template-columns: 1fr 1fr auto;gap: 8px; margin-bottom: 6px; }
.form-row input {padding: 8px;border: 1px solid #cbd5e1;border-radius: 4px;}
#resultBox { width: 100%;min-height: 200px;font-family: ui-monospace, SFMono-Regular, Menlo, monospace;font-size: 14px; padding: 12px; 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; }
</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>图片上传与识别</h2>
<p>选择图片后上传,服务端调用大模型解析为可编辑的 JSON再确认入库。</p>
</div>
</div>
<div class="upload-section" id="dropArea">
<h3>上传图片</h3>
<p>点击下方按钮选择图片,或拖拽图片到此区域</p>
<form id="uploadForm" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" id="fileInput" name="file" accept="image/*" required />
<br>
<button type="submit" class="btn btn-primary">上传并识别</button>
</form>
<div class="status-message" id="uploadMsg"></div>
</div>
<div class="preview-container">
<div class="preview-box">
<h3>图片预览</h3>
<img id="preview" alt="预览" />
</div>
<div class="result-box">
<h3>识别结果(可编辑)</h3>
<div class="form-controls">
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
</div>
<div id="kvForm"></div>
<textarea id="resultBox" placeholder="识别结果JSON将显示在这里"></textarea>
</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 preview = document.getElementById('preview');
const resultBox = document.getElementById('resultBox');
const uploadMsg = document.getElementById('uploadMsg');
const confirmBtn = document.getElementById('confirmBtn');
const clearBtn = document.getElementById('clearBtn');
const confirmMsg = document.getElementById('confirmMsg');
const kvForm = document.getElementById('kvForm');
const addFieldBtn = document.getElementById('addFieldBtn');
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
const dropArea = document.getElementById('dropArea');
let currentImageRel = '';
// 拖拽上传功能
['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) {
fileInput.files = files;
const event = new Event('change', { bubbles: true });
fileInput.dispatchEvent(event);
}
}
// 文件选择后预览
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
function createRow(k = '', v = '') {
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 = () => {
if (kvForm.children.length > 1) {
kvForm.removeChild(row);
} else {
keyInput.value = '';
valInput.value = '';
}
syncTextarea();
};
keyInput.oninput = syncTextarea;
valInput.oninput = syncTextarea;
row.appendChild(keyInput);
row.appendChild(valInput);
row.appendChild(delBtn);
return row;
}
function renderFormFromObject(obj) {
kvForm.innerHTML = '';
Object.keys(obj || {}).forEach(k => {
kvForm.appendChild(createRow(k, obj[k]));
});
if (!kvForm.children.length) kvForm.appendChild(createRow());
syncTextarea();
}
function objectFromForm() {
const obj = {};
Array.from(kvForm.children).forEach(row => {
const [kInput, vInput] = row.querySelectorAll('input');
const k = (kInput.value || '').trim();
if (!k) return;
const raw = vInput.value;
try {
obj[k] = JSON.parse(raw);
} catch (_) {
obj[k] = raw;
}
});
return obj;
}
function syncTextarea() {
const obj = objectFromForm();
resultBox.value = JSON.stringify(obj, null, 2);
}
addFieldBtn.addEventListener('click', () => {
kvForm.appendChild(createRow());
syncTextarea();
});
syncFromTextBtn.addEventListener('click', () => {
try {
const obj = JSON.parse(resultBox.value || '{}');
renderFormFromObject(obj);
uploadMsg.textContent = '已从文本区刷新表单';
uploadMsg.className = 'status-message success';
uploadMsg.style.display = 'block';
setTimeout(() => {
uploadMsg.style.display = 'none';
}, 2000);
} catch (e) {
uploadMsg.textContent = '文本区不是有效JSON';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
}
});
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
uploadMsg.textContent = '';
confirmMsg.textContent = '';
confirmBtn.disabled = true;
resultBox.value = '';
currentImageRel = '';
const file = fileInput.files[0];
if (!file) {
uploadMsg.textContent = '请选择图片文件';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const resp = await fetch('/elastic/upload/', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': getCookie('csrftoken') || '' },
body: formData,
});
const data = await resp.json();
if (!resp.ok || data.status !== 'success') {
throw new Error(data.message || '上传识别失败');
}
uploadMsg.textContent = data.message || '识别成功';
uploadMsg.className = 'status-message success';
uploadMsg.style.display = 'block';
preview.src = data.image_url;
renderFormFromObject(data.data || {});
currentImageRel = data.image;
confirmBtn.disabled = false;
} catch (e) {
uploadMsg.textContent = e.message || '发生错误';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
}
});
confirmBtn.addEventListener('click', async () => {
confirmMsg.textContent = '';
try {
const edited = objectFromForm();
const resp = await fetch('/elastic/confirm/', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken') || ''
},
body: JSON.stringify({ data: edited, image: currentImageRel })
});
const data = await resp.json();
if (!resp.ok || data.status !== 'success') {
throw new Error(data.message || '录入失败');
}
confirmMsg.textContent = data.message || '录入成功';
confirmMsg.style.color = '#179957';
} catch (e) {
confirmMsg.textContent = e.message || '发生错误';
confirmMsg.style.color = '#d14343';
}
});
clearBtn.addEventListener('click', () => {
fileInput.value = '';
preview.src = '';
resultBox.value = '';
kvForm.innerHTML = '';
kvForm.appendChild(createRow()); // 保留一个空行
uploadMsg.textContent = '';
confirmMsg.textContent = '';
confirmBtn.disabled = true;
});
// 退出登录处理
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>