好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 xyxy1111 于 2026-7-14 22:00 编辑
大神BD7IWD在精品软件区发了一个软件:拍照自动回传工具 v2.0
链接:https://www.52pojie.cn/forum.php?mod=viewthread&tid=2110717
让我想起来2020年给疫情物资清单库存时制作的一个网页版工具,稍微改了一下,给有需要的网友。
系统要求:有自己的域名(公网IP)和存储空间(服务器开启IIS),必须开启SSL(也就是要有https),否则无法启用手机的摄像头。
系统其实就2个页面:index.asp负责拍照,view.asp负责对上传的照片进行管理(请修改第7行的pwd="888888"为自己的密码)。
index.asp会自动生成一个二维码,就是你的网站的地址。可以把这个二维码保存在自己的手机里用浏览器或者微信进行识别。也可以将网站地址直接存在手机浏览器的收藏夹里(https://域名/index.asp?mode=camera )。
拍照时,是不需要确认上传的,只要按下拍照键就自动拍照上传了,可以连拍连传(当然,上传速度取决于你的网络情况,尤其是服务器的带宽。一般来说,1-2秒一张是没问题的)。
index.asp代码如下:
[HTML] 纯文本查看 复制代码 <%@LANGUAGE="VBSCRIPT" CODEPAGE="65001"%>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>手机拍照上传系统</title>
<script src="js/qrcode.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
a { text-decoration:none;cursor:pointer;}
body {
background: #f1f5f9;
margin: 0;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 500px;
width: 100%;
background: white;
border-radius: 2rem;
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.2);
padding: 1.5rem 1.8rem 2rem;
}
.title {
font-size: 24px;
font-weight: 700;
margin: 0 0 1.5rem 0;
color: #0f172a;
text-align: center;
display:flex;
align-items:center;
justify-content:center;
gap:15px;
}
.qrcard {
text-align: center;
margin-bottom: 1.5rem;
}
.qrcode-area {
display: flex;
justify-content: center;
margin: 15px;
}
#qrcode {
width: 220px;
height: 220px;
display: inline-block;
background: white;
margin: 10px;
}
.qrTip {
font-size: 0.75rem;line-height:1.75;
color: #555;
background: #f8fafc;
display: inline-block;
padding: 6px 20px;
border-radius: 60px;border:1px solid #e2e8f0;
word-break: break-all;
}
.footer-note {
text-align: center;
font-size: 12px;
color: #5b6e8c;
margin-top: 28px;
border-top: 1px solid #e2e8f0;
padding-top: 16px;
display:flex;
align-items:center;
justify-content:space-between;
}
.view-link {
border:1px solid #5b6e8c;
color:#5b6e8c;
padding:4px 12px;
border-radius:20px;
font-size:14px;
}
/* 相机全屏样式 */
.camera-fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
display: flex;
flex-direction: column;
z-index: 1000;
}
.camera-header {
background: rgba(0,0,0,0.7);
color: white;
padding: 12px 16px;
text-align: center;
font-weight: 100;
backdrop-filter: blur(12px);
font-size: 1.2rem;
}
.camera-video-container {
flex: 1;
position: relative;
background: #111;
overflow: hidden;
}
.camera-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-controls {
padding: 20px;
background: #0f172a;
display: grid;
grid-template-columns: 1fr 1fr; /* 分为两列 */
justify-content: center;/* 整个网格容器在父元素中水平居中 */
gap: 24px; /* 网格行列之间的间距 */
align-items: center;/* 网格子项垂直居中对齐 */
justify-items: center; /* 网格子项水平居中对齐 */
text-align: center; /* 文本内容居中 */
}
.op-btn {
background: none;
border: none;
width: 40px;
height: 40px;
border: none;
font-size: 1.2rem;
font-weight: bold;
color: white;
cursor: pointer;
}
.toast-msg {
position: fixed;
bottom: 100px;
left: 20px;
right: 20px;
background: rgba(0,0,0,0.8);
color: #ccffcc;
text-align: center;
padding: 8px;
border-radius: 50px;
font-size: 0.8rem;
pointer-events: none;
opacity: 0;
transition: 0.2s;
z-index: 2000;
}
</style>
</head>
<%
' ==================== 经典ASP无组件上传处理 ====================
' 功能: 二维码展示 + 手机拍照上传(二进制解析)
' ========================================================
Response.Expires = 0
Response.CodePage = 65001
Response.Charset = "UTF-8"
' 定义上传文件夹
upload_folder="upload/"
method = Request.ServerVariables("REQUEST_METHOD")
' 判断是否有文件上传 (POST 且 TotalBytes > 0)
If method = "POST" And Request.TotalBytes > 0 Then
Call ProcessPhotoUpload()
Response.End()
End If
Dim mode
mode = Request.QueryString("mode")
If mode = "camera" Then ' 相机模式
%>
<body>
<div class="camera-fullscreen" id="cameraApp">
<div class="camera-header">拍照上传</div>
<div class="camera-video-container">
<video id="video" class="camera-video" autoplay playsinline></video>
<canvas id="canvas" style="display:none;"></canvas>
</div>
<div class="camera-controls">
<button id="captureBtn" class="op-btn"><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 24 24" ><path d="M12 8c-2.17 0-4 1.83-4 4s1.83 4 4 4 4-1.83 4-4-1.83-4-4-4m0 6c-1.07 0-2-.93-2-2s.93-2 2-2 2 .93 2 2-.93 2-2 2"></path><path d="M20 5h-2.59L14.7 2.29a1 1 0 0 0-.71-.29h-4c-.27 0-.52.11-.71.29L6.57 5H3.98c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2Zm0 13H4V7h3c.27 0 .52-.11.71-.29L10.42 4h3.17l2.71 2.71c.19.19.44.29.71.29h3v11Z"></path></svg></button>
<button id="backBtn" class="op-btn"><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 24 24" ><path d="M11 2h2v10h-2z"></path><path d="M15 4.53v2.16c2.36 1.13 4 3.53 4 6.32 0 3.86-3.14 7-7 7s-7-3.14-7-7c0-2.79 1.64-5.19 4-6.32V4.53C5.51 5.77 3 9.1 3 13c0 4.96 4.04 9 9 9s9-4.04 9-9c0-3.91-2.51-7.24-6-8.47"></path></svg></button>
</div>
<div id="toastMsg" class="toast-msg"></div>
</div>
<script>
var uploadUrl = '<%=GetBaseUrl()%>';
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var captureBtn = document.getElementById('captureBtn');
var backBtn = document.getElementById('backBtn');
var toastDiv = document.getElementById('toastMsg');
function showToast(msg, isError) {
toastDiv.style.opacity = '1';
toastDiv.textContent = msg;
toastDiv.style.background = isError ? 'rgba(220,38,38,0.9)' : 'rgba(0,0,0,0.8)';
setTimeout(function() { toastDiv.style.opacity = '0'; }, 1800);
}
function initCamera() {
// 请求高分辨率,从高到低降级尝试
var resolutions = [
{ width: { ideal: 3840 }, height: { ideal: 2160 } }, // 4K
{ width: { ideal: 2560 }, height: { ideal: 1440 } }, // 2K
{ width: { ideal: 1920 }, height: { ideal: 1080 } }, // 1080p
{ width: { ideal: 1280 }, height: { ideal: 720 } }, // 720p
{ width: { ideal: 640 }, height: { ideal: 480 } } // 480p 保底
];
function tryResolution(index) {
if (index >= resolutions.length) {
// 所有分辨率都失败,使用无约束
return navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
}
var constraints = {
video: {
facingMode: 'environment',
width: resolutions[index].width,
height: resolutions[index].height
}
};
return navigator.mediaDevices.getUserMedia(constraints)
.catch(function(err) {
console.warn('分辨率', resolutions[index].width.ideal, 'x', resolutions[index].height.ideal, '失败:', err);
return tryResolution(index + 1);
});
}
tryResolution(0)
.then(function(stream) {
video.srcObject = stream;
return video.play();
})
.then(function() {
showToast('📷 相机就绪,当前分辨率: ' + video.videoWidth + 'x' + video.videoHeight, false);
})
.catch(function(err) {
showToast('无法获取摄像头,请使用HTTPS或授权', true);
alert('摄像头不可用:' + err.message);
});
}
function takePhotoAndUpload() {
if (!video.videoWidth || !video.videoHeight) {
showToast('相机未就绪', true);
return;
}
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(function(blob) {
if (!blob) {
showToast('拍照失败', true);
return;
}
var now = new Date();
var year = now.getFullYear();
var month = String(now.getMonth() + 1).padStart(2, '0');
var day = String(now.getDate()).padStart(2, '0');
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var ss = String(now.getSeconds()).padStart(2, '0');
var ms = String(now.getMilliseconds()).padStart(3, '0');
var filename = year + month + day + '_' + hh + mm + ss + '_' + ms + '.jpg';
var formData = new FormData();
formData.append('photo', blob, filename);
fetch(uploadUrl, { method: 'POST', body: formData })
.then(function(resp) { return resp.text(); })
.then(function(text) {
try {
var json = JSON.parse(text);
if (json.success) {
showToast('✅ 已保存: ' + json.filename, false);
} else {
showToast('失败: ' + json.message, true);
}
} catch(e) {
if (text.indexOf('success') > 0) {
showToast('✅ 已保存', false);
} else {
showToast('服务器响应异常', true);
}
}
})
.catch(function(err) {
showToast('网络错误: ' + err.message, true);
});
}, 'image/jpeg', 0.85);
}
captureBtn.addEventListener('click', takePhotoAndUpload);
backBtn.addEventListener('click', function() {
if (video.srcObject) {
video.srcObject.getTracks().forEach(function(t) { t.stop(); });
}
window.close();
});
initCamera();
window.addEventListener('beforeunload', function() {
if (video.srcObject) {
video.srcObject.getTracks().forEach(function(t) { t.stop(); });
}
});
</script>
</body>
</html>
<%
Else ' 二维码模式
%>
<body>
<div class="container">
<div class="title">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="currentColor" viewBox="0 0 24 24"><path d="M12 8c-2.17 0-4 1.83-4 4s1.83 4 4 4 4-1.83 4-4-1.83-4-4-4m0 6c-1.07 0-2-.93-2-2s.93-2 2-2 2 .93 2 2-.93 2-2 2"></path><path d="M20 5h-2.59L14.7 2.29a1 1 0 0 0-.71-.29h-4c-.27 0-.52.11-.71.29L6.57 5H3.98c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2Zm0 13H4V7h3c.27 0 .52-.11.71-.29L10.42 4h3.17l2.71 2.71c.19.19.44.29.71.29h3v11Z"></path></svg>
<span>随身拍照助手</span>
</div>
<div class="qrcard">
<div class="qrcode-area"><div id="qrcode"></div></div>
<div class="qrTip" id="qrTip">二维码加载中...</div>
</div>
<div class="footer-note"><div>手机扫码进入相机 > 拍照 > 自动上传至服务器</div><a href="view.asp"><div class="view-link">管理图片</div></a></div>
</div>
<script>
var cameraUrl = '<%=GetBaseUrl()%>?mode=camera';
var qrDiv = document.getElementById('qrcode');
new QRCode(qrDiv, {
text: cameraUrl,
width: 220,
height: 220,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
document.getElementById('qrTip').innerHTML = '扫码访问<br>' + cameraUrl;
</script>
</body>
</html>
<%
End If
' ==================== 获取网址函数 ====================
Function GetBaseUrl()
Dim url, protocol
If Request.ServerVariables("HTTPS") = "on" Then
protocol = "https://"
Else
protocol = "http://"
End If
GetBaseUrl = protocol & Request.ServerVariables("HTTP_HOST") & Request.ServerVariables("SCRIPT_NAME")
End Function
' ==================== 照片上传处理 ====================
Sub ProcessPhotoUpload()
On Error Resume Next
totalBytes = Request.TotalBytes
binData = Request.BinaryRead(totalBytes)
Set a = CreateObject("adodb.stream")
a.Type = 1
a.Open
a.Write binData
a.Position = 0
b = a.Read
c = ChrB(13) & ChrB(10)
d = InStrB(b, c)
e = InStrB(d + 1, b, c)
Set f = CreateObject("adodb.stream")
f.Type = 1
f.Open
a.Position = d + 1
a.CopyTo f, e - d - 3
f.Position = 0
f.Type = 2
f.CharSet = "utf-8"
g = f.ReadText
f.Close
' 解析文件名
h = Mid(g, InStrRev(g, "\") + 1, e)
h = Mid(h, InStrRev(h, "=") + 2)
h = Trim(h)
' 解析文件数据位置
i = InStrB(b, c & c) + 4
j = InStrB(i + 1, b, LeftB(b, d - 1)) - i - 2
k = InStrB(i + j + 1, b, c & c) + 4
l = InStrB(k + 1, b, LeftB(b, d - 1)) - k - 2
m = InStrB(k + l + 1, b, c & c) + 4
n = InStrB(m + 1, b, LeftB(b, d - 1)) - m - 2
' 获取文件扩展名
hh = LCase(Mid(h, InStrRev(h, ".") + 1))
If hh = "" Then hh = "jpg"
' 生成时间格式的文件名
currentTime = Now()
formattedTime = Year(currentTime) & Right("0" & Month(currentTime), 2) & Right("0" & Day(currentTime), 2) & "_" & Right("0" & Hour(currentTime), 2) & Right("0" & Minute(currentTime), 2) & Right("0" & Second(currentTime), 2)
' 加上毫秒避免重复(使用Timer)
Dim msPart
msPart = Int((Timer * 1000) Mod 1000)
msPart = Right("00" & msPart, 3)
formattedTime = formattedTime & "_" & msPart
fileName = formattedTime & "." & hh
' 保存文件
f.Type = 1
f.Open
a.Position = i - 1
a.CopyTo f, j
' 确保upload目录存在
savePath = Server.MapPath(upload_folder)
Set fso = CreateObject("Scripting.FileSystemObject")
If Not fso.FolderExists(savePath) Then
fso.CreateFolder(savePath)
End If
Set fso = Nothing
fullPath = savePath & "\" & fileName
f.SaveToFile fullPath, 2
f.Close
' 清理
Set f = Nothing
a.Close
Set a = Nothing
' 返回成功JSON
If Err.Number = 0 Then
Response.ContentType = "application/json"
Response.Write "{""success"":true,""filename"":""" & fileName & """}"
Else
Response.ContentType = "application/json"
Response.Write "{""success"":false,""message"":""保存文件失败: " & Err.Description & """}"
End If
End Sub
%>
</body>
</html>
view.asp代码如下(包含了无组件上传代码):
[HTML] 纯文本查看 复制代码 <%@LANGUAGE="VBSCRIPT" CODEPAGE="65001"%>
<%
' ==================== 图片管理页面 ====================
Response.Expires = 0
Response.CodePage = 65001
Response.Charset = "UTF-8"
pwd="888888" '引号内为管理密码
upload_folder = "upload/"
mode = Request.QueryString("mode")
method = Request.ServerVariables("REQUEST_METHOD")
if request("act")="login" then
if request.form("password")=pwd then
session("login")=1
response.redirect "view.asp"
end if
end if
if request("act")="logout" then
session("login")=0
response.redirect "view.asp"
end if
' 获取图片列表接口
If mode = "listImages" Then
Response.ContentType = "application/json"
Dim folderPath, fso, folder, file, arr(), i, j, temp, jsonFiles
folderPath = Server.MapPath(upload_folder)
Set fso = CreateObject("Scripting.FileSystemObject")
If Not fso.FolderExists(folderPath) Then
Response.Write "{""success"":true,""files"":[]}"
Set fso = Nothing
Response.End()
End If
Set folder = fso.GetFolder(folderPath)
ReDim arr(-1)
For Each file In folder.Files
Dim ext
ext = LCase(fso.GetExtensionName(file.Name))
If ext = "jpg" Or ext = "jpeg" Or ext = "png" Or ext = "gif" Or ext = "webp" Then
ReDim Preserve arr(UBound(arr) + 1)
arr(UBound(arr)) = file.Name
End If
Next
' 按文件名倒序排列(最新的在前)
For i = 0 To UBound(arr) - 1
For j = i + 1 To UBound(arr)
If arr(i) < arr(j) Then
temp = arr(i)
arr(i) = arr(j)
arr(j) = temp
End If
Next
Next
jsonFiles = "["
For i = 0 To UBound(arr)
If i > 0 Then jsonFiles = jsonFiles & ","
jsonFiles = jsonFiles & """" & Replace(arr(i), "\", "/") & """"
Next
jsonFiles = jsonFiles & "]"
Response.Write "{""success"":true,""files"": " & jsonFiles & "}"
Set folder = Nothing
Set fso = Nothing
Response.End()
End If
' 批量删除接口
If mode = "deleteMultiple" Then
Response.ContentType = "application/json"
Dim fso3, folderPath3, filesParam, fileArray, fileItem, count, ff, fullPath3
count = 0
filesParam = Request.Form("files")
If filesParam = "" Then
Response.Write "{""success"":false,""message"":""未指定文件列表""}"
Response.End()
End If
' 简单的逗号分隔解析
fileArray = Split(filesParam, ",")
Set fso3 = CreateObject("Scripting.FileSystemObject")
folderPath3 = Server.MapPath(upload_folder)
For ff = 0 To UBound(fileArray)
fileItem = Trim(fileArray(ff))
' 安全过滤,防止路径遍历
fileItem = Replace(fileItem, "..", "")
fileItem = Replace(fileItem, "/", "")
fileItem = Replace(fileItem, "\", "")
If fileItem <> "" Then
fullPath3 = folderPath3 & "\" & fileItem
If fso3.FileExists(fullPath3) Then
On Error Resume Next
fso3.DeleteFile fullPath3
If Err.Number = 0 Then
count = count + 1
End If
On Error GoTo 0
End If
End If
Next
Response.Write "{""success"":true,""deletedCount"":" & count & "}"
Set fso3 = Nothing
Response.End()
End If
' 单张删除接口
If mode = "delete" Then
Response.ContentType = "application/json"
Dim fileName, fullPath, fso2, folderPath2
fileName = Request.QueryString("file")
If fileName = "" Then
Response.Write "{""success"":false,""message"":""未指定文件名""}"
Response.End()
End If
' 安全过滤
fileName = Replace(fileName, "..", "")
fileName = Replace(fileName, "/", "")
fileName = Replace(fileName, "\", "")
Set fso2 = CreateObject("Scripting.FileSystemObject")
folderPath2 = Server.MapPath(upload_folder)
fullPath = folderPath2 & "\" & fileName
If fso2.FileExists(fullPath) Then
On Error Resume Next
fso2.DeleteFile fullPath
If Err.Number = 0 Then
Response.Write "{""success"":true,""message"":""删除成功""}"
Else
Response.Write "{""success"":false,""message"":""删除失败: " & Err.Description & """}"
End If
On Error GoTo 0
Else
Response.Write "{""success"":false,""message"":""文件不存在""}"
End If
Set fso2 = Nothing
Response.End()
End If
%>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>图片管理 - 手机拍照上传系统</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #f1f5f9;
padding: 20px;
min-height: 100vh;
}
.login-form {
background: white;
border-radius: 20px;
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.1);
width:300px;
position:absolute;
left:50%;top:50%;
transform:translate(-50%, -50%);
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:15px;
text-align:center;
overflow:hidden;
}
.login-form .header {
width:100%;
background: #ff6600;
color:#fff;
font-size:26px;
font-weight:700;
text-align:center;
padding: 20px;
display:flex;
align-items:center;
justify-content:center;
letter-spacing:5px;
}
.login-form input {
width:80%;
padding:14px;
font-size:16px;
text-align:center;
border: 1px solid #555;
border-radius:8px;
}
.login-form button {
width:80%;
text-align:center;
border: none;
background:none;
margin:10px;
cursor:pointer;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.1);
padding: 30px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 18px;
margin-bottom: 20px;
border-bottom: 1px solid #eef2ff;
padding-bottom: 20px;
}
.title {
font-size: 22px;
font-weight: 700;
color: #0f172a;
display: flex;
align-items: center;
gap: 10px;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
border: none;
padding: 8px 16px;
border-radius: 40px;
font-weight: 100;
color:#000;
cursor: pointer;
transition: 0.2s ease;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.btn-index {
background: #fcd4b8;
}
.btn-index:hover {
background: #fcc39a;
}
.btn-refresh {
background: #f1f5f9;
}
.btn-refresh:hover {
background: #bfd5eb;
}
.btn-del {
background: #fee2e2;
}
.btn-del:hover {
background: #fecaca;
}
.btn-camera {
background: #dcfce7;
}
.btn-camera:hover {
background: #bbf7d0;
}
.stats-bar {
background: #f8fafc;
padding: 10px 20px;
border-radius: 60px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
font-size: 14px;
color: #475569;
font-weight:100;
}
.sum {display:flex;align-items:center;gap:4px;}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
margin-top: 20px;
}
.image-card {
background: #f8fafc;
border-radius: 10px;
overflow: hidden;
transition: 0.2s ease;
border: 1px solid #e2e8f0;
position: relative;
}
.image-card:hover {
transform: translateY(-1px);
box-shadow: 0 12px 20px -12px rgba(0,0,0,0.2);
}
.img-checkbox {
position: absolute;
top: 5px;
right: 5px;
z-index: 99;
width: 18px;
height: 18px;
cursor: pointer;
}
.image-card img {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
background: #e2e8f0;
cursor: pointer;
}
.image-info {
padding: 10px 8px;
font-size: 0.7rem;
color: #334155;
text-align: center;
background: white;
border-top: 1px solid #e2e8f0;
word-break: break-all;
}
.filename {
font-weight: 500;
margin-bottom: 6px;
color: #0f172a;
font-size: 12px;
}
.file-time {
font-size: 10px;
color: #5b6e8c;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #64748b;
font-weight: 100;
}
.empty-state svg {
margin-bottom: 15px;
opacity: 0.5;
}
.footer-bar {
margin-top: 28px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding-top: 18px;
border-top: 1px solid #e2e8f0;
}
.select-all {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
cursor: pointer;
}
.tip {font-size: 14px;}
.toast-msg {
position: fixed;
bottom: 30px;
left: 20px;
right: 20px;
background: #0f172a;
color: white;
text-align: center;
padding: 12px 20px;
border-radius: 60px;
font-size: 12px;
opacity: 0;
transition: 0.2s;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.loading {
text-align: center;
padding: 40px;
color: #64748b;
}
.preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 2000;
justify-content: center;
align-items: center;
cursor: pointer;
}
.preview-modal img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
.close-preview {
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
/* 分页样式 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 20px;
flex-wrap: wrap;
}
.page-btn {
background: #f1f5f9;
border: none;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: 0.2s;
}
.page-btn:hover {
background: #e2e8f0;
}
.page-btn.active {
background: #0f172a;
color: white;
}
.page-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #64748b;
margin: 0 10px;
}
@media (max-width: 640px) {
.container { padding: 1rem; }
.image-grid { gap: 12px; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.title { font-size: 18px; }
.page-btn { padding: 4px 10px; font-size: 12px; }
}
</style>
</head>
<body>
<%
if session("login")<>1 then
%>
<form method="post" action="?act=login" class="login-form">
<div class="header">通行证</div>
<input type="password" name="password" placeholder="请输入密码">
<button type="submit"><svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" fill="currentColor" viewBox="0 0 24 24" ><path d="m9 17 6-5-6-5v4H2v2h7z"></path><path d="M13 3v2c3.86 0 7 3.14 7 7s-3.14 7-7 7v2c4.96 0 9-4.04 9-9s-4.04-9-9-9"></path></svg></button>
<div class="actions" style="margin-bottom:20px;width:100%;justify-content:space-around;">
<a href="index.asp?mode=scanQR" class="btn btn-index">返回首页</a>
<a href="index.asp?mode=camera" class="btn btn-camera">拍照上传</a>
</div>
</form>
<%
response.end
else
%>
<div class="container">
<div class="header">
<div class="title">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 24 24" ><path d="M4 21h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2M4 5h16v14H4z"></path><path d="M7.5 8a1.5 1.5 0 1 0 0 3 1.5 1.5 0 1 0 0-3m3 6L9 12l-3 4h12l-4.5-6z"></path></svg>
图片管理
</div>
<div class="actions">
<a href="index.asp?mode=scanQR" class="btn btn-index">返回首页</a>
<a href="index.asp?mode=camera" class="btn btn-camera">拍照上传</a>
<button id="refreshBtn" class="btn btn-refresh">刷新列表</button>
<button id="deleteSelectedBtn" class="btn btn-del">删除选中</button>
<a href="?act=logout" class="btn btn-index">退出登录</a>
</div>
</div>
<div class="stats-bar" id="statsBar">
<span class="sum"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24" ><path d="M4 2H2v19c0 .55.45 1 1 1h19v-2H4z"></path><path d="M17 12h2v6h-2zm-5-8h2v14h-2zM7 9h2v9H7z"></path></svg>总计<span id="totalCount">0</span>张图片</span>
<span class="sum"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24" ><path d="M10.5 15.5c-.26 0-.51-.1-.71-.29l-2.5-2.5L8.7 11.3l1.79 1.79 4.79-4.79 1.41 1.41-5.5 5.5c-.2.2-.45.29-.71.29Z"></path><path d="M19 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2M5 5v14h14V5z"></path></svg>已选择<span id="selectedCount">0</span> 张</span>
</div>
<div id="imageListContainer" class="image-grid">
<div class="loading">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" stroke-dasharray="30" stroke-dashoffset="10">
<animate attributeName="stroke-dashoffset" from="30" to="0" dur="1s" repeatCount="indefinite"/>
</circle>
</svg>
<p>加载中...</p>
</div>
</div>
<!-- 分页控件 -->
<div id="pagination" class="pagination"></div>
<div class="footer-bar">
<label class="select-all">
<input type="checkbox" id="selectAllCheckbox"> 全选 / 取消
</label>
<span class="tip">点击图片可预览大图</span>
</div>
</div>
<div id="toastMsg" class="toast-msg"></div>
<div id="previewModal" class="preview-modal">
<span class="close-preview">×</span>
<img id="previewImage" src="" alt="预览">
</div>
<script>
var scriptName = 'view.asp';
var IMAGE_FOLDER = '<%=upload_folder%>';
var currentImages = [];
var currentPage = 1;
var pageSize = 8;
var totalPages = 1;
function showToast(msg, isError) {
var toast = document.getElementById('toastMsg');
toast.textContent = msg;
toast.style.background = isError ? '#b91c1c' : '#0f172a';
toast.style.opacity = '1';
setTimeout(function() { toast.style.opacity = '0'; }, 2000);
}
// 获取当前页的数据
function getCurrentPageImages() {
var start = (currentPage - 1) * pageSize;
var end = start + pageSize;
return currentImages.slice(start, end);
}
// 渲染分页控件
function renderPagination() {
if (totalPages <= 1) {
document.getElementById('pagination').innerHTML = '';
return;
}
var html = '';
// 上一页
if (currentPage > 1) {
html += '<button class="page-btn">上一页</button>';
} else {
html += '<button class="page-btn disabled" disabled>上一页</button>';
}
// 页码
var startPage = Math.max(1, currentPage - 2);
var endPage = Math.min(totalPages, currentPage + 2);
if (startPage > 1) {
html += '<button class="page-btn">1</button>';
if (startPage > 2) html += '<span class="page-info">...</span>';
}
for (var i = startPage; i <= endPage; i++) {
if (i === currentPage) {
html += '<button class="page-btn active" disabled>' + i + '</button>';
} else {
html += '<button class="page-btn">' + i + '</button>';
}
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += '<span class="page-info">...</span>';
html += '<button class="page-btn">' + totalPages + '</button>';
}
// 下一页
if (currentPage < totalPages) {
html += '<button class="page-btn">下一页</button>';
} else {
html += '<button class="page-btn disabled" disabled>下一页</button>';
}
//html += '<span class="page-info">第 ' + currentPage + ' / ' + totalPages + ' 页</span>';
document.getElementById('pagination').innerHTML = html;
}
// 跳转到指定页
function goToPage(page) {
if (page < 1 || page > totalPages) return;
currentPage = page;
renderImageList(getCurrentPageImages());
renderPagination();
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// 图片列表
function renderImageList(images) {
var container = document.getElementById('imageListContainer');
var totalSpan = document.getElementById('totalCount');
if (!currentImages.length) {
container.innerHTML = `
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 24 24" ><path d="M5 21h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2m0-2v-1.59l3-3 1.29 1.29c.39.39 1.02.39 1.41 0l5.29-5.29 3 3V19h-14ZM19 5v5.59L16.71 8.3a.996.996 0 0 0-1.41 0l-5.29 5.29-1.29-1.29a.996.996 0 0 0-1.41 0l-2.29 2.29V5h14Z"></path><path d="M8.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 1 0 0-3"></path></svg>
<p>暂无图片</p>
</div>
`;
totalSpan.innerText = '0';
document.getElementById('selectedCount').innerText = '0';
return;
}
totalSpan.innerText = currentImages.length;
totalPages = Math.ceil(currentImages.length / pageSize);
if (!images.length) {
container.innerHTML = '<div class="empty-state"><p>暂无图片</p></div>';
} else {
var html = '';
for (var i = 0; i < images.length; i++) {
var img = images[i];
var displayName = img.length > 28 ? img.substring(0,25) + '...' : img;
html += '<div class="image-card" data-filename="' + img.replace(/"/g, '"') + '">'+
'<input type="checkbox" class="img-checkbox" data-filename="' + img.replace(/"/g, '"') + '" id="chk_' + i + '">'+
'<img src="' + IMAGE_FOLDER + img + '" alt="' + img + '" loading="lazy"\\'") + '\')">'+
'<div class="image-info">'+
'<div class="filename" title="' + img + '">' + displayName + '</div>'+
'</div>'+
'</div>';
}
container.innerHTML = html;
}
attachCheckboxEvents();
updateSelectedCount();
renderPagination();
}
function attachCheckboxEvents() {
var checkboxes = document.querySelectorAll('.img-checkbox');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].onchange = updateSelectedCount;
}
var selectAllChk = document.getElementById('selectAllCheckbox');
if (selectAllChk) {
selectAllChk.onchange = handleSelectAll;
}
}
function handleSelectAll(e) {
var isChecked = e.target.checked;
var allCheckboxes = document.querySelectorAll('.img-checkbox');
for (var i = 0; i < allCheckboxes.length; i++) {
allCheckboxes[i].checked = isChecked;
}
updateSelectedCount();
}
function updateSelectedCount() {
var selected = document.querySelectorAll('.img-checkbox:checked').length;
document.getElementById('selectedCount').innerText = selected;
var selectAllChk = document.getElementById('selectAllCheckbox');
var total = document.querySelectorAll('.img-checkbox').length;
if (selectAllChk) {
selectAllChk.checked = (total > 0 && selected === total);
selectAllChk.indeterminate = (selected > 0 && selected < total);
}
}
function fetchImageList() {
var container = document.getElementById('imageListContainer');
container.innerHTML = '<div class="loading"><p>加载中...</p></div>';
var xhr = new XMLHttpRequest();
xhr.open('GET', scriptName + '?mode=listImages', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.success && Array.isArray(data.files)) {
currentImages = data.files;
currentPage = 1;
renderImageList(getCurrentPageImages());
} else {
container.innerHTML = '<div class="empty-state">⚠️ 获取列表失败</div>';
}
} catch(e) {
container.innerHTML = '<div class="empty-state">❌ 解析数据失败</div>';
console.error('解析错误:', e);
}
} else {
container.innerHTML = '<div class="empty-state">❌ HTTP错误: ' + xhr.status + '</div>';
}
}
};
xhr.send();
}
function deleteSelectedImages() {
var selectedCheckboxes = document.querySelectorAll('.img-checkbox:checked');
if (selectedCheckboxes.length === 0) {
showToast('请先选择要删除的图片', true);
return;
}
if (!confirm('确定要删除 ' + selectedCheckboxes.length + ' 张图片吗?\n操作不可恢复!')) return;
var filesToDelete = [];
for (var i = 0; i < selectedCheckboxes.length; i++) {
filesToDelete.push(selectedCheckboxes[i].getAttribute('data-filename'));
}
var filesParam = filesToDelete.join(',');
var xhr = new XMLHttpRequest();
xhr.open('POST', scriptName + '?mode=deleteMultiple', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var result = JSON.parse(xhr.responseText);
if (result.success) {
showToast('✅ 成功删除 ' + result.deletedCount + ' 张图片');
fetchImageList();
} else {
showToast('删除失败: ' + (result.message || '未知错误'), true);
}
} catch(e) {
showToast('删除失败: 响应解析错误', true);
}
} else {
showToast('删除失败: HTTP ' + xhr.status, true);
}
}
};
xhr.send('files=' + encodeURIComponent(filesParam));
}
// 预览图片
function previewImage(src) {
var modal = document.getElementById('previewModal');
var previewImg = document.getElementById('previewImage');
previewImg.src = src;
modal.style.display = 'flex';
}
// 关闭预览
document.querySelector('.close-preview').onclick = function() {
document.getElementById('previewModal').style.display = 'none';
};
document.getElementById('previewModal').onclick = function(e) {
if (e.target === this) {
this.style.display = 'none';
}
};
// ESC键关闭预览
document.onkeydown = function(e) {
if (e.key === 'Escape') {
document.getElementById('previewModal').style.display = 'none';
}
};
// 刷新按钮
document.getElementById('refreshBtn').onclick = function() {
fetchImageList();
showToast('刷新中...');
};
// 删除按钮
document.getElementById('deleteSelectedBtn').onclick = deleteSelectedImages;
// 页面加载
fetchImageList();
</script>
<%end if%>
</body>
</html>
|
免费评分
-
查看全部评分
|