384 lines
13 KiB
HTML
384 lines
13 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;}
|
|
.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> |