@@ -10,7 +10,7 @@
background : #fafafa ;
}
/* 导航栏样式 */
/* 导航栏样式 - 保持原有样式 */
. sidebar {
position : fixed ;
top : 0 ;
@@ -35,7 +35,7 @@
. sidebar h3 {
margin-top : 0 ;
font-size : 18 px ;
color : #ff79c 6 ;
color : #add8e 6 ;
text-align : center ;
margin-bottom : 20 px ;
}
@@ -68,42 +68,207 @@
background-color : rgba ( 139 , 233 , 253 , 0.2 ) ;
}
/* 主内容区 */
/* 主内容区 - 改进后的样式 */
. main-content {
margin-left : 200 px ;
padding : 20 px ;
color : #333 ;
}
/* 原有样式保持不变 */
. container {
max-width : 9 00px ;
margin : 6 vh auto ;
max-width : 12 00px ;
margin : 0 auto ;
background : #fff ;
border-radius : 10 px ;
box-shadow : 0 6 px 18 px rgba ( 0 , 0 , 0 , 0.06 ) ;
border-radius : 14 px ;
box-shadow : 0 10 px 24 px rgba ( 31 , 35 , 4 0, 0.08 ) ;
padding : 24 px ;
}
. row { display : flex ; gap : 16 px ; }
. col { flex : 1 ; }
textarea {
width : 100 % ;
min-height : 260 px ;
font-family : ui-monospace , SFMono-Regular , Menlo , monospace ;
. header {
display : flex ;
align-items : center ;
justify-content : space-between ;
margin-bottom : 12 px ;
}
. header h2 {
margin : 0 ;
color : #1e293b ;
}
. header p {
margin : 5 px 0 0 0 ;
color : #64748b ;
font-size : 14 px ;
}
img { max-width : 100 % ; border : 1 px solid #eee ; border-radius : 6 px ; }
. bt n {
padding : 8 px 12 px ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
. upload-sectio n {
background : #f8fafc ;
border : 2 px dashed #cbd5e1 ;
border-radius : 12 px ;
padding : 32 px ;
text-align : center ;
transition : all 0.3 s ease ;
margin-bottom : 24 px ;
}
. upload-section : hover {
border-color : #4f46e5 ;
background : #f1f5f9 ;
}
. upload-section . drag-over {
border-color : #4f46e5 ;
background : #e0e7ff ;
}
. upload-section input [ type = "file" ] {
margin : 15 px 0 ;
}
. btn {
padding : 10 px 16 px ;
border : none ;
border-radius : 8 px ;
cursor : pointer ;
margin : 0 4 px ;
font-size : 14 px ;
transition : all 0.2 s 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 : 24 px ;
margin : 24 px 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 : 300 px ;
border : 1 px solid #e2e8f0 ;
border-radius : 8 px ;
object-fit : contain ;
}
. result-box {
flex : 1 ;
}
. result-box h3 {
margin-top : 0 ;
color : #334155 ;
}
. form-controls {
display : flex ;
gap : 8 px ;
margin-bottom : 12 px ;
flex-wrap : wrap ;
}
# kvForm {
border : 1 px solid #e2e8f0 ;
border-radius : 8 px ;
padding : 12 px ;
max-height : 300 px ;
overflow : auto ;
margin-bottom : 12 px ;
background : white ;
}
. form-row {
display : grid ;
grid-template-columns : 1 fr 1 fr auto ;
gap : 8 px ;
margin-bottom : 6 px ;
}
. form-row input {
padding : 8 px ;
border : 1 px solid #cbd5e1 ;
border-radius : 4 px ;
}
# resultBox {
width : 100 % ;
min-height : 200 px ;
font-family : ui-monospace , SFMono-Regular , Menlo , monospace ;
font-size : 14 px ;
padding : 12 px ;
border : 1 px solid #e2e8f0 ;
border-radius : 8 px ;
resize : vertical ;
box-sizing : border-box ;
}
. status-message {
padding : 10 px ;
margin : 10 px 0 ;
border-radius : 6 px ;
display : none ;
}
. status-message . success {
background-color : #d4edda ;
color : #155724 ;
border : 1 px solid #c3e6cb ;
}
. status-message . error {
background-color : #f8d7da ;
color : #721c24 ;
border : 1 px solid #f5c6cb ;
}
. action-buttons {
margin-top : 16 px ;
display : flex ;
gap : 8 px ;
flex-wrap : wrap ;
}
. btn-primary { background : #1677ff ; color : #fff ; }
. btn-secondary { background : #f0f0f0 ; }
. muted { color : #666 ; font-size : 12 px ; }
. error { color : #d14343 ; }
. success { color : #179957 ; }
< / style >
< / head >
< body >
@@ -123,38 +288,46 @@
<!-- 主内容区域 -->
< div class = "main-content" >
< 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 class = "header" >
< div >
< h2 > 图片上传与识别< / h2 >
< p > 选择图片后上传,服务端调用大模型解析为可编辑的 JSON, 再确认入库。< / p >
< / 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 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 >
@@ -177,15 +350,63 @@ 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 . style . display = 'grid ';
row . style . gridTemplateColumns = '1fr 1fr auto' ;
row . style . gap = '8px' ;
row . style . marginBottom = '6px' ;
row . className = 'form-row ';
const keyInput = document . createElement ( 'input' ) ;
keyInput . type = 'text' ;
keyInput . placeholder = '字段名' ;
@@ -196,9 +417,17 @@ function createRow(k = '', v = '') {
valInput . value = typeof v === 'object' ? JSON . stringify ( v ) : ( v ? ? '' ) ;
const delBtn = document . createElement ( 'button' ) ;
delBtn . type = 'button' ;
delBtn . className = 'btn btn-secondary ' ;
delBtn . className = 'btn btn-danger ' ;
delBtn . textContent = '删除' ;
delBtn . onclick = ( ) => { kvForm . removeChild ( row ) ; syncTextarea ( ) ; } ;
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 ) ;
@@ -246,9 +475,16 @@ 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 = 'error' ;
uploadMsg . className = 'status-message error' ;
uploadMsg . style . display = 'block' ;
}
} ) ;
@@ -263,7 +499,8 @@ uploadForm.addEventListener('submit', async (e) => {
const file = fileInput . files [ 0 ] ;
if ( ! file ) {
uploadMsg . textContent = '请选择图片文件' ;
uploadMsg . className = 'error' ;
uploadMsg . className = 'status-message error' ;
uploadMsg . style . display = 'block' ;
return ;
}
@@ -282,14 +519,16 @@ uploadForm.addEventListener('submit', async (e) => {
throw new Error ( data . message || '上传识别失败' ) ;
}
uploadMsg . textContent = data . message || '识别成功' ;
uploadMsg . className = 'success' ;
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 = 'error' ;
uploadMsg . className = 'status-message error' ;
uploadMsg . style . display = 'block' ;
}
} ) ;
@@ -311,10 +550,10 @@ confirmBtn.addEventListener('click', async () => {
throw new Error ( data . message || '录入失败' ) ;
}
confirmMsg . textContent = data . message || '录入成功' ;
confirmMsg . className = 'success ';
confirmMsg . style . color = '#179957 ';
} catch ( e ) {
confirmMsg . textContent = e . message || '发生错误' ;
confirmMsg . className = 'error ';
confirmMsg . style . color = '#d14343 ';
}
} ) ;
@@ -323,6 +562,7 @@ clearBtn.addEventListener('click', () => {
preview . src = '' ;
resultBox . value = '' ;
kvForm . innerHTML = '' ;
kvForm . appendChild ( createRow ( ) ) ; // 保留一个空行
uploadMsg . textContent = '' ;
confirmMsg . textContent = '' ;
confirmBtn . disabled = true ;