229 lines
7.5 KiB
HTML
229 lines
7.5 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>图片上传与识别</title>
|
||
<style>
|
||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #fafafa; }
|
||
.container { max-width: 900px; margin: 6vh auto; background: #fff; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); padding: 24px; }
|
||
.row { display: flex; gap: 16px; }
|
||
.col { flex: 1; }
|
||
textarea { width: 100%; min-height: 260px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px; }
|
||
img { max-width: 100%; border: 1px solid #eee; border-radius: 6px; }
|
||
.btn { padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer; }
|
||
.btn-primary { background: #1677ff; color: #fff; }
|
||
.btn-secondary { background: #f0f0f0; }
|
||
.muted { color: #666; font-size: 12px; }
|
||
.error { color: #d14343; }
|
||
.success { color: #179957; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h2>图片上传与识别</h2>
|
||
<p class="muted">选择图片后上传,服务端调用大模型解析为可编辑的 JSON,再确认入库。</p>
|
||
|
||
<form id="uploadForm" enctype="multipart/form-data">
|
||
{% csrf_token %}
|
||
<input type="file" id="fileInput" name="file" accept="image/*" />
|
||
<button type="submit" class="btn btn-primary">上传并识别</button>
|
||
<span id="uploadMsg" class="muted"></span>
|
||
</form>
|
||
|
||
<div class="row" style="margin-top:16px;">
|
||
<div class="col">
|
||
<h4>图片预览</h4>
|
||
<img id="preview" alt="预览" />
|
||
</div>
|
||
<div class="col">
|
||
<h4>识别结果(可编辑)</h4>
|
||
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
|
||
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
|
||
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
|
||
</div>
|
||
<div id="kvForm" style="border:1px solid #eee; border-radius:6px; padding:8px; max-height:300px; overflow:auto;"></div>
|
||
<div style="margin-top:8px;">
|
||
<textarea id="resultBox" placeholder="识别结果JSON将显示在这里"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:16px;">
|
||
<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>
|
||
|
||
<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');
|
||
|
||
let currentImageRel = '';
|
||
|
||
function createRow(k = '', v = '') {
|
||
const row = document.createElement('div');
|
||
row.style.display = 'grid';
|
||
row.style.gridTemplateColumns = '1fr 1fr auto';
|
||
row.style.gap = '8px';
|
||
row.style.marginBottom = '6px';
|
||
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-secondary';
|
||
delBtn.textContent = '删除';
|
||
delBtn.onclick = () => { kvForm.removeChild(row); 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);
|
||
} catch (e) {
|
||
uploadMsg.textContent = '文本区不是有效JSON';
|
||
uploadMsg.className = 'error';
|
||
}
|
||
});
|
||
|
||
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 = 'error';
|
||
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 = 'success';
|
||
preview.src = data.image_url;
|
||
renderFormFromObject(data.data || {});
|
||
currentImageRel = data.image;
|
||
confirmBtn.disabled = false;
|
||
} catch (e) {
|
||
uploadMsg.textContent = e.message || '发生错误';
|
||
uploadMsg.className = 'error';
|
||
}
|
||
});
|
||
|
||
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.className = 'success';
|
||
} catch (e) {
|
||
confirmMsg.textContent = e.message || '发生错误';
|
||
confirmMsg.className = 'error';
|
||
}
|
||
});
|
||
|
||
clearBtn.addEventListener('click', () => {
|
||
fileInput.value = '';
|
||
preview.src = '';
|
||
resultBox.value = '';
|
||
kvForm.innerHTML = '';
|
||
uploadMsg.textContent = '';
|
||
confirmMsg.textContent = '';
|
||
confirmBtn.disabled = true;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|