- measure.py: MeasureResult 增 hairline_discarded 判定(顶庭<0.7cm),弃用时 顶/上庭字段置null、face_total只算中下庭、hairline_source=discarded - annotation.py: 弃用时保留头顶线、去掉发际线、只标中/下庭 - app.py: 接口1/6 弃用分支 + eye1/7竖向范围改用眉心 + 接口1调试页(measure-debug) - 新增 static/test_interface1_debug.html 分步可视化页(9步原理)
367 lines
19 KiB
HTML
367 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>接口1 调试页(分步可视化)— 四庭七眼测量</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
||
h1 { font-size: 22px; margin-bottom: 8px; }
|
||
h2 { font-size: 17px; margin: 24px 0 12px; }
|
||
.subtitle { color: #888; font-size: 13px; margin-bottom: 16px; line-height: 1.7; }
|
||
.subtitle code { background:#f0f0f0; padding:1px 6px; border-radius:4px; font-size:12px; }
|
||
|
||
/* 上传区 */
|
||
.upload-card { background: #fff; border-radius: 12px; padding: 20px 24px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
|
||
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||
.file-input { flex: 1; min-width: 200px; }
|
||
.file-input input[type=file] { width: 100%; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
|
||
.file-input input[type=file].dragover { border-color: #2563eb; background: #eff6ff; }
|
||
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: .2s; }
|
||
.btn-primary { background: #2563eb; color: #fff; }
|
||
.btn-primary:hover { background: #1d4ed8; }
|
||
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
|
||
.btn-sm { padding: 6px 14px; font-size: 13px; }
|
||
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
|
||
.btn-outline:hover { background: #f9fafb; }
|
||
.upload-hint { font-size: 12px; color: #9ca3af; margin-top: 8px; line-height: 1.6; }
|
||
|
||
/* 状态 */
|
||
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-top: 12px; display: none; }
|
||
.status.info { background: #dbeafe; color: #1e40af; display: block; }
|
||
.status.error { background: #fee2e2; color: #991b1b; display: block; }
|
||
.status.success { background: #d1fae5; color: #065f46; display: block; }
|
||
|
||
/* 指标卡片 */
|
||
.metrics { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
||
.metric { background: #f9fafb; border-radius: 8px; padding: 12px 14px; }
|
||
.metric .label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: .5px; }
|
||
.metric .value { font-size: 18px; font-weight: 700; color: #111827; margin-top: 2px; }
|
||
.metric .unit { font-size: 12px; color: #6b7280; font-weight: 400; }
|
||
|
||
/* debug 数值面板 */
|
||
.debug-info { background: #fff; border-radius: 12px; padding: 16px 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-top: 16px; }
|
||
.debug-info .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px 24px; font-size: 13px; }
|
||
.debug-info .grid div { color: #4b5563; }
|
||
.debug-info .grid b { color: #111827; }
|
||
|
||
/* 分步卡片 */
|
||
.steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
||
.step { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||
.step .cap { font-size: 13px; font-weight: 600; padding: 10px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; display:flex; align-items:center; gap:8px; }
|
||
.step .cap .badge { flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; min-width:26px; height:26px; padding:0 6px; border-radius:6px; background:#2563eb; color:#fff; font-size:14px; font-weight:700; }
|
||
.step .cap .ttext { flex:1; }
|
||
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; font-size:11px; }
|
||
.step .desc { font-size: 12.5px; line-height: 1.7; color: #4b5563; padding: 10px 12px; background: #f9fafb; border-bottom: 1px solid #f0f0f0; }
|
||
.step .desc b { color:#1f2937; }
|
||
.step img { width: 100%; display: block; background: #222; cursor: zoom-in; }
|
||
.step .noimg { padding: 30px; text-align: center; color: #ccc; font-size: 13px; }
|
||
|
||
/* JSON */
|
||
.panel { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; margin-top:16px; }
|
||
.panel-header { font-weight: 700; font-size: 14px; padding: 12px 18px; border-bottom: 1px solid #f0f0f0; background: #fafafa; display: flex; justify-content: space-between; align-items: center; }
|
||
.json-panel { max-height: 500px; overflow: auto; }
|
||
.json-content { padding: 14px 18px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
||
|
||
/* lightbox */
|
||
#lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.9); display: none; align-items: center; justify-content: center; z-index: 999; cursor: zoom-out; padding: 30px; }
|
||
#lightbox img { max-width: 95%; max-height: 95%; }
|
||
|
||
.hidden { display: none !important; }
|
||
</style>
|
||
<script src="/static/img_downscale.js?v=2"></script>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📐 接口1 调试页 <span style="font-size:13px;color:#888">(分步可视化 + 原理)</span></h1>
|
||
<div class="subtitle">
|
||
独立调试页,把「四庭七眼测量」算法的每一步中间产物都可视化出来,并标注原理。<br>
|
||
调试接口:<code>POST /api/v1/face/measure-debug</code> | 与正式接口 <code>/api/v1/face/measure</code> 同算法链路,额外返回 9 张分步图。<br>
|
||
算法链路:取图 → 关键点检测 → 姿态校验 → 头发/耳朵分割 → 发际线定位 → 四庭纵向 → 七眼横向 → 尺度校准 → 最终标注。
|
||
</div>
|
||
|
||
<!-- 上传区 -->
|
||
<div class="upload-card">
|
||
<div class="upload-row">
|
||
<div class="file-input">
|
||
<input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||
</div>
|
||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交测试</button>
|
||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除结果</button>
|
||
</div>
|
||
<div class="upload-hint">支持 JPG / PNG 正面照 | 也可拖拽图片到文件选择框</div>
|
||
<div id="statusBar" class="status hidden"></div>
|
||
</div>
|
||
|
||
<!-- 指标速览 -->
|
||
<h2 class="hidden" id="metricsTitle">📊 指标速览</h2>
|
||
<div id="metricsBar" class="metrics hidden"></div>
|
||
|
||
<!-- debug 数值 -->
|
||
<div id="debugPanel" class="debug-info hidden">
|
||
<div style="font-weight:700;font-size:13px;margin-bottom:8px">🔍 中间数值(data.debug)</div>
|
||
<div class="grid" id="debugGrid"></div>
|
||
</div>
|
||
|
||
<!-- 分步可视化 -->
|
||
<h2 class="hidden" id="stepsTitle">🪜 分步可视化(每一步中间产物 + 原理)</h2>
|
||
<div class="steps" id="stepsGrid"></div>
|
||
|
||
<!-- JSON -->
|
||
<div class="panel hidden" id="jsonPanel">
|
||
<div class="panel-header">
|
||
<span>📋 JSON 响应</span>
|
||
<button class="btn btn-outline btn-sm" onclick="copyJson()">📋 复制</button>
|
||
</div>
|
||
<div class="json-panel"><pre class="json-content" id="jsonContent"></pre></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- lightbox -->
|
||
<div id="lightbox" onclick="this.style.display='none'"><img id="lightboxImg" alt=""></div>
|
||
|
||
<script>
|
||
const API_BASE = window.location.origin;
|
||
const ENDPOINT = '/api/v1/face/measure-debug';
|
||
|
||
// 9 个步骤的配置:no=序号, key=后端 steps 字段名, title=标题, sub=子标题, desc=原理说明
|
||
const STEPS = [
|
||
{ no: '①', key: 'input', title: '输入原图', sub: '用户上传的正面照',
|
||
desc: '算法入口:接收 JPG/PNG 图片,解码为 OpenCV BGR 数组。后续所有关键点、分割、测量都基于这张图,坐标原点在图片左上角,x 向右、y 向下。'
|
||
},
|
||
{ no: '②', key: 'landmarks', title: '人脸关键点检测', sub: '白=全部478点,青=虹膜点,红+标号=关键测量点',
|
||
desc: '用 <b>MediaPipe Face Mesh</b>(468 点拓扑,开启 refine_landmarks 再加 10 个虹膜点,共 478 点)定位人脸。'
|
||
+ '中/下庭(眉心→鼻翼下缘→下巴尖)能直接从关键点读出;上/顶庭因发际线被头发遮挡、关键点不稳定,需后续分割补齐。'
|
||
+ '虹膜点(468~477)是尺度校准的天然标尺。图中红点为七眼 6 点 + 鼻翼/下巴/眉间等纵向基准点。'
|
||
},
|
||
{ no: '③', key: 'pose', title: '头部姿态校验', sub: '黄=PnP 6点,红/绿/青=三轴,文字=yaw/pitch/roll',
|
||
desc: '用 6 个关键点(鼻尖/下巴/双眼外角/双嘴角)配通用 3D 头模,<b>cv2.solvePnP</b> 解出欧拉角。'
|
||
+ 'yaw=左右扭头、pitch=上下点头、roll=面内倾斜。三轴绝对值均 ≤ 30° 才算正面照,否则报「角度问题」(1003)。'
|
||
+ 'ITERATIVE 解偶尔收敛到相机后方(翻转解),检测到 tz<0 时用 SQPNP 重解正深度解。'
|
||
},
|
||
{ no: '④', key: 'segmentation', title: '头发/耳朵分割', sub: '绿=头发(hair=17),蓝=耳朵(ear=7/8)',
|
||
desc: '<b>BiSeNet</b>(CelebAMask-HQ 19 类语义分割,hair=17/l_ear=7/r_ear=8)单次推理同时出头发和耳朵 mask。'
|
||
+ '整张大场景图(脸只占一小块)会欠分割丢耳朵,故先按人脸框放大裁剪再分割,让脸接近训练分布。'
|
||
+ '头发 mask 用于下一步定位发际线;耳朵 mask 用于七眼取「人头最左/最右」端线。'
|
||
},
|
||
{ no: '⑤', key: 'hairline', title: '发际线/头顶定位', sub: '黄列带=中轴线扫描,红横线=hairline,黄横线=hair_top',
|
||
desc: '<b>方案 B(分割,优先)</b>:在面部中轴线 ±3px 列带上扫头发 mask,最靠下的头发行 = 发际线 hairline_y,'
|
||
+ '全图头发 mask 最高点 = 头顶 hair_top_y;合理性校验(头顶<发际线<眉心、各庭为正)不过则回退。'
|
||
+ '<b>方案 A(兜底)</b>:实测中/下庭,按比例常量(顶:上:中:下=0.22:0.25:0.28:0.25)推算上/顶庭。'
|
||
+ 'source=segmentation 表示用了真实分割,=estimated 表示回退估算。'
|
||
},
|
||
{ no: '⑥', key: 'vertical', title: '四庭纵向点', sub: '红点=5个纵向基准,绿竖线=各庭段高(cm)',
|
||
desc: '把 5 个纵向点(头顶/发际线/眉心/鼻翼下缘/下巴尖)连成 4 段:'
|
||
+ '<b>顶庭</b>(头顶→发际线)、<b>上庭</b>(发际线→眉心)、<b>中庭</b>(眉心→鼻翼下缘)、<b>下庭</b>(鼻翼下缘→下巴尖)。'
|
||
+ '各段像素高 ÷ px_per_cm = 厘米值。这是「四庭」比例分析的物理依据。'
|
||
},
|
||
{ no: '⑦', key: 'seven_eyes', title: '七眼横向点', sub: '红点=6个横向基准,紫线=人头最左/最右端线',
|
||
desc: '取左右脸颊/眼外角/眼内角 6 个关键点的横坐标,把头宽切成 5 段(左脸颊/左眼/两眼间距/右眼/右脸颊)。'
|
||
+ '再用耳朵分割外缘补「人头最左/最右」两条端线(紫色),共 7 段 = <b>七眼</b>。'
|
||
+ '耳朵被头发遮挡时该侧端线省略(看不到耳朵就不画),对应 eye1/eye7 返回 null。'
|
||
},
|
||
{ no: '⑧', key: 'scale', title: '尺度校准', sub: '青=虹膜直径线,黄=眼宽线(降级),文字=px_per_cm',
|
||
desc: '把像素换算成厘米。<b>虹膜直径法(优先)</b>:成人虹膜直径高度稳定(平均 11.7mm=1.17cm),'
|
||
+ '左右虹膜直径像素均值 ÷ 1.17 = px_per_cm。虹膜点缺失时<b>降级用眼宽法</b>(外→内眼角均值 2.85cm)。'
|
||
+ '这一步决定了所有 cm 数值的准确度——标尺错了,后面全错。'
|
||
},
|
||
{ no: '⑨', key: 'final', title: '最终标注图', sub: '原图 + 标注层叠加 = 接口1 正式输出',
|
||
desc: '把前面算出的四庭七眼数据,用 <b>create_annotated_image()</b> 渲染成透明底 RGBA 标注层'
|
||
+ '(白线/白字、渐变消失横竖线、虚线双箭头、各段 cm+百分比),再叠加回原图。'
|
||
+ '这就是正式接口 <code>/api/v1/face/measure</code> 返回的 annotated_image_base64。'
|
||
},
|
||
];
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
function setStatus(text, type) {
|
||
const bar = $('statusBar');
|
||
bar.textContent = text;
|
||
bar.className = 'status ' + type;
|
||
}
|
||
|
||
function pick(obj, name) {
|
||
if (!obj) return null;
|
||
return obj[name + '_url'] || obj[name + '_base64'] || null;
|
||
}
|
||
|
||
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
|
||
|
||
function stepCard(st, src) {
|
||
const div = document.createElement('div');
|
||
div.className = 'step';
|
||
const hasImg = src && src.length > 50;
|
||
const img = hasImg ? '<img src="' + src + '" onclick="zoom(this.src)">'
|
||
: '<div class="noimg">无图(后端未返回此步)</div>';
|
||
const badge = '<span class="badge">' + st.no + '</span>';
|
||
const lenTxt = hasImg ? ' (' + src.length + '字符)' : '';
|
||
const ttext = '<span class="ttext">' + st.title + '<small>' + st.sub + lenTxt + '</small></span>';
|
||
const desc = '<div class="desc">' + st.desc + '</div>';
|
||
div.innerHTML = '<div class="cap">' + badge + ttext + '</div>' + desc + img;
|
||
return div;
|
||
}
|
||
|
||
function renderResult(d) {
|
||
const s = d.steps || {};
|
||
const grid = $('stepsGrid');
|
||
grid.innerHTML = '';
|
||
STEPS.forEach(st => {
|
||
grid.appendChild(stepCard(st, pick(s, st.key)));
|
||
});
|
||
}
|
||
|
||
function renderMetrics(data) {
|
||
const fc = data.four_courts || {};
|
||
const se = data.seven_eyes || {};
|
||
const items = [
|
||
{ label: '全脸总高', value: data.face_total_height_cm, unit: 'cm' },
|
||
{ label: '顶庭', value: fc.top_court_cm, unit: 'cm' },
|
||
{ label: '上庭', value: fc.upper_court_cm, unit: 'cm' },
|
||
{ label: '中庭', value: fc.middle_court_cm, unit: 'cm' },
|
||
{ label: '下庭', value: fc.lower_court_cm, unit: 'cm' },
|
||
{ label: '单眼宽度', value: se.eye_width_cm, unit: 'cm' },
|
||
{ label: '脸宽', value: se.face_width_cm, unit: 'cm' },
|
||
{ label: '两眼间距', value: se.inter_eye_distance_cm, unit: 'cm' },
|
||
];
|
||
let html = '';
|
||
items.forEach(m => {
|
||
// null 值(发际线弃用时顶/上庭)显示「已弃用」,灰色弱化
|
||
if (m.value === null || m.value === undefined) {
|
||
html += '<div class="metric" style="opacity:.5"><div class="label">' + m.label + '</div>'
|
||
+ '<div class="value" style="font-size:13px;color:#9ca3af">已弃用</div></div>';
|
||
} else {
|
||
html += '<div class="metric"><div class="label">' + m.label + '</div><div class="value">'
|
||
+ m.value + ' <span class="unit">' + m.unit + '</span></div></div>';
|
||
}
|
||
});
|
||
$('metricsBar').innerHTML = html;
|
||
}
|
||
|
||
function renderDebug(dbg) {
|
||
if (!dbg) { $('debugPanel').classList.add('hidden'); return; }
|
||
const rows = [];
|
||
const push = (k, v) => rows.push('<div>' + k + ': <b>' + v + '</b></div>');
|
||
push('图片尺寸', (dbg.image_width||'?') + ' × ' + (dbg.image_height||'?') + ' px');
|
||
push('关键点数', dbg.num_landmarks || 0);
|
||
if (dbg.head_pose) {
|
||
push('姿态 yaw/pitch/roll',
|
||
dbg.head_pose.yaw + '° / ' + dbg.head_pose.pitch + '° / ' + dbg.head_pose.roll + '°');
|
||
push('是否正面', dbg.head_pose.frontal ? 'YES' : 'NO');
|
||
}
|
||
push('头发像素', dbg.hair_pixels ?? '—');
|
||
push('耳朵像素', dbg.ear_pixels ?? '—');
|
||
const srcMap = { segmentation: '分割(方案B)', estimated: '估算(方案A兜底)',
|
||
discarded: '⚠️ 已弃用(离头顶<0.7cm)' };
|
||
push('发际线来源', srcMap[dbg.hairline_source] || dbg.hairline_source || '—');
|
||
push('尺度方法', dbg.scale_method === 'iris' ? '虹膜直径法' :
|
||
(dbg.scale_method === 'eye_width' ? '眼宽法(降级)' : dbg.scale_method || '—'));
|
||
push('px_per_cm', dbg.px_per_cm ?? '—');
|
||
if (dbg.head_left_x != null || dbg.head_right_x != null) {
|
||
push('人头最左/最右 x', (dbg.head_left_x ?? 'null') + ' / ' + (dbg.head_right_x ?? 'null'));
|
||
}
|
||
$('debugGrid').innerHTML = rows.join('');
|
||
$('debugPanel').classList.remove('hidden');
|
||
}
|
||
|
||
async function submitTest() {
|
||
const fileInput = $('imageFile');
|
||
let file = fileInput.files[0];
|
||
if (!file) { setStatus('请先选择一张图片', 'error'); return; }
|
||
file = await window.downscaleImageFile(file);
|
||
|
||
const _reqStart = performance.now();
|
||
const btn = $('submitBtn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ 请求中...';
|
||
setStatus('正在请求 ' + ENDPOINT + ' ...', 'info');
|
||
// 隐藏旧结果
|
||
['metricsTitle','metricsBar','stepsTitle','jsonPanel'].forEach(id => $(id).classList.add('hidden'));
|
||
$('debugPanel').classList.add('hidden');
|
||
$('stepsGrid').innerHTML = '';
|
||
|
||
const form = new FormData();
|
||
form.append('image_file', file);
|
||
|
||
try {
|
||
const resp = await fetch(API_BASE + ENDPOINT, {
|
||
method: 'POST',
|
||
headers: { 'X-Internal-Token': 'dev-shared-secret-2026' },
|
||
body: form
|
||
});
|
||
const json = await resp.json();
|
||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||
|
||
$('jsonContent').textContent = JSON.stringify(json, null, 2);
|
||
$('jsonPanel').classList.remove('hidden');
|
||
$('stepsTitle').classList.remove('hidden');
|
||
|
||
const d = json.data || {};
|
||
if (json.code === 0) {
|
||
setStatus('✅ 请求成功 (' + _elapsed + 's) — 完整 9 步可视化', 'success');
|
||
renderResult(d);
|
||
renderMetrics(d);
|
||
renderDebug(d.debug);
|
||
$('metricsTitle').classList.remove('hidden');
|
||
$('metricsBar').classList.remove('hidden');
|
||
} else {
|
||
// 业务错误(如 1001 未检出人脸 / 1003 非正面):仍展示已完成的步骤图
|
||
setStatus('⚠️ 业务错误 (' + _elapsed + 's) — code: ' + json.code + ' ' + json.message
|
||
+ '(下方展示算法走到哪一步)', 'error');
|
||
if (d.steps) renderResult(d);
|
||
if (d.debug) renderDebug(d.debug);
|
||
}
|
||
} catch (err) {
|
||
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
|
||
setStatus('❌ 网络错误 (' + _elapsed + 's): ' + err.message, 'error');
|
||
$('jsonContent').textContent = 'Error: ' + err.message;
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '🚀 提交测试';
|
||
}
|
||
}
|
||
|
||
function clearResults() {
|
||
$('metricsTitle').classList.add('hidden');
|
||
$('metricsBar').classList.add('hidden');
|
||
$('debugPanel').classList.add('hidden');
|
||
$('stepsTitle').classList.add('hidden');
|
||
$('stepsGrid').innerHTML = '';
|
||
$('jsonPanel').classList.add('hidden');
|
||
$('statusBar').className = 'status hidden';
|
||
$('imageFile').value = '';
|
||
$('jsonContent').textContent = '';
|
||
}
|
||
|
||
function copyJson() {
|
||
const text = $('jsonContent').textContent;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const btn = event.target;
|
||
const orig = btn.textContent;
|
||
btn.textContent = '✅ 已复制';
|
||
setTimeout(() => btn.textContent = orig, 1500);
|
||
});
|
||
}
|
||
|
||
// 拖拽上传
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const dropZone = $('imageFile');
|
||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
||
dropZone.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('dragover');
|
||
if (e.dataTransfer.files.length) {
|
||
dropZone.files = e.dataTransfer.files;
|
||
setStatus('已选择: ' + e.dataTransfer.files[0].name, 'info');
|
||
}
|
||
});
|
||
dropZone.addEventListener('change', () => {
|
||
if (dropZone.files.length) setStatus('已选择: ' + dropZone.files[0].name, 'info');
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|