附件:支持视频 / 文档上传,可预览的浏览器直接看,预览不了的给下载
之前节点附件只能传图片和 PDF(accept 过滤 + 20MB 上限),视频传不上来。 本批放开:MP4 / MOV / PDF / Word / Excel / ZIP 都能作为附件上传, 仍走同一份 Uploads::handleFiles(MIME 白名单三层识别不变),存进已配置的 OSS。 大小分级:照片仍走 photo_max_mb(管理员可调),视频与文档放宽到 40MB (Uploads::MAX_FILE_BYTES,给线上 post_max_size=50M 留余量),先 detect 再按 is_image 选上限。前端 form.php / attachments.php 同步拦截并按类型细分暂存区图标。 预览走灯箱四类型视图:图片(img)/ 视频(<video>,配合 media/show 新增的 HTTP Range 单区间 206/416,进度条可拖、Safari 也能播)/ PDF(iframe 内嵌 阅读器)/ 其它(Word / ZIP 等浏览器预览不了 → 灯箱里给下载按钮)。 dl=1 下载不接 Range;多区间回退 200 全量。 已验证:本地全链 11 步全绿(渲染检查 308 项、表单交互 73 项); 远端 e2e 0 失败(新增 mp4 上传落库 video/mp4、Range 206/416/后缀区间/ 多区间回退/dl 不接 Range 专项断言);部署后双 PHP 渲染逐字节一致。
This commit is contained in:
@@ -22,6 +22,8 @@ foreach ([$postMaxBytes, $fileMaxBytes] as $b) {
|
||||
|
||||
$iconByMime = [
|
||||
'application/pdf' => '📄',
|
||||
'video/mp4' => '🎬',
|
||||
'video/quicktime' => '🎬',
|
||||
'image/gif' => '🖼️',
|
||||
'video/mp4' => '🎬',
|
||||
'video/quicktime' => '🎬',
|
||||
@@ -60,12 +62,14 @@ $iconByMime = [
|
||||
<label>添加照片 / 附件</label>
|
||||
|
||||
<div class="drop-zone" id="drop-zone" tabindex="0" role="button" aria-label="选择或拖入文件">
|
||||
<input type="file" name="files[]" id="file-input" multiple style="display:none;">
|
||||
<input type="file" name="files[]" id="file-input" multiple style="display:none;"
|
||||
accept="image/*,.heic,.heif,.pdf,.mp4,.mov,.doc,.docx,.xls,.xlsx,.zip">
|
||||
<div class="drop-zone-inner">
|
||||
<div class="drop-zone-icon">📷</div>
|
||||
<p class="drop-zone-title">点击选择,或把照片拖到这里</p>
|
||||
<p class="drop-zone-title">点击选择,或把照片 / 视频拖到这里</p>
|
||||
<p class="muted drop-zone-hint">
|
||||
可一次选多张。支持 JPG / PNG / GIF / WebP / HEIC、PDF、MP4 等;单张上限 <?= $maxMb ?>MB。
|
||||
照片(JPG / PNG / GIF / WebP / HEIC)、视频(MP4 / MOV)、文档(PDF / Word / Excel)、ZIP 都可以。
|
||||
照片单张上限 <?= $maxMb ?>MB,视频与文档单个上限 <?= (int)(\App\Uploads::MAX_FILE_BYTES / 1048576) ?>MB。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +108,7 @@ $iconByMime = [
|
||||
|
||||
<?php if (empty($attachments)): ?>
|
||||
<div class="empty">
|
||||
<p>还没有照片 —— 把登机牌、车票、门票拍下来传上去吧</p>
|
||||
<p>还没有附件 —— 把登机牌、车票、门票拍下来,或者把视频、PDF 传上去吧</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="photo-grid">
|
||||
@@ -121,6 +125,18 @@ $iconByMime = [
|
||||
$dl = media_att($attId, true);
|
||||
$sizeTxt = $desc['size_text'];
|
||||
|
||||
/* 灯箱预览方式:图片 / 视频直接播,PDF 内嵌阅读器,
|
||||
其它(Word / ZIP 等)浏览器预览不了 → 灯箱里给下载按钮 */
|
||||
if ($isImg) {
|
||||
$kind = 'image';
|
||||
} elseif (strpos($mime, 'video/') === 0) {
|
||||
$kind = 'video';
|
||||
} elseif ($mime === 'application/pdf') {
|
||||
$kind = 'pdf';
|
||||
} else {
|
||||
$kind = 'file';
|
||||
}
|
||||
|
||||
$dims = '';
|
||||
if (!empty($att['width']) && !empty($att['height'])) {
|
||||
$dims = (int)$att['width'] . '×' . (int)$att['height'];
|
||||
@@ -134,7 +150,8 @@ $iconByMime = [
|
||||
?>
|
||||
<div class="photo-card">
|
||||
<a href="<?= e($url) ?>" title="<?= e($name) ?>"
|
||||
<?= $isImg ? 'class="js-photo" data-cap="' . e($name) . '"' : 'target="_blank" rel="noopener"' ?>>
|
||||
<?= $kind === 'image' ? 'class="js-photo" data-cap="' . e($name) . '"'
|
||||
: 'class="js-media" data-kind="' . $kind . '" data-cap="' . e($name) . '" data-dl="' . e($dl) . '"' ?>>
|
||||
<?php if ($isImg): ?>
|
||||
<img src="<?= e($url) ?>" alt="<?= e($name) ?>" loading="lazy">
|
||||
<?php else: ?>
|
||||
@@ -194,10 +211,21 @@ $iconByMime = [
|
||||
|
||||
var csrf = <?= json_encode(csrf_token()) ?>;
|
||||
var maxBytes = <?= (int)$maxMb ?> * 1024 * 1024;
|
||||
var fileMax = <?= (int)\App\Uploads::MAX_FILE_BYTES ?>;
|
||||
var postLimit = <?= (int)$effBytes ?>;
|
||||
var uploadUrl = <?= json_encode(url('attachments/upload')) ?>;
|
||||
var nodeId = <?= (int)$node['id'] ?>;
|
||||
|
||||
var VIDEO_EXT = ['mp4', 'mov', 'm4v'];
|
||||
var PDF_EXT = ['pdf'];
|
||||
|
||||
function iconFor(f) {
|
||||
var ext = extOf(f.name);
|
||||
if (VIDEO_EXT.indexOf(ext) >= 0) return '🎬';
|
||||
if (PDF_EXT.indexOf(ext) >= 0) return '📄';
|
||||
return '📎';
|
||||
}
|
||||
|
||||
/** 服务端也可以拒,但 30 个的上限在这里先挡一道 */
|
||||
var MAX_FILES = 30;
|
||||
|
||||
@@ -263,9 +291,13 @@ $iconByMime = [
|
||||
|
||||
arr.forEach(function (f) {
|
||||
if (room <= 0) { rejected.push(f.name + ':一次最多 ' + MAX_FILES + ' 个'); return; }
|
||||
if (!isImageFile(f) && f.type === 'image/heic') { /* 仍允许直传 */ }
|
||||
/* 大小分级:照片走 photo_max_mb;视频 / 文档走更宽的 fileMax(后端同款逻辑) */
|
||||
if (isImageFile(f) && maxBytes > 0 && f.size > maxBytes) {
|
||||
rejected.push(f.name + ':超过 ' + (maxBytes / 1024 / 1024) + 'MB');
|
||||
rejected.push(f.name + ':照片超过 ' + (maxBytes / 1048576) + 'MB');
|
||||
return;
|
||||
}
|
||||
if (!isImageFile(f) && f.size > fileMax) {
|
||||
rejected.push(f.name + ':文件超过 ' + (fileMax / 1048576) + 'MB');
|
||||
return;
|
||||
}
|
||||
var url = isImageFile(f) ? URL.createObjectURL(f) : '';
|
||||
@@ -327,7 +359,7 @@ $iconByMime = [
|
||||
if (it.image && it.thumb) {
|
||||
inner += '<img class="photo-thumb" src="' + esc(it.thumb) + '" alt="">';
|
||||
} else {
|
||||
inner += '<div class="photo-thumb photo-thumb-icon">📎</div>';
|
||||
inner += '<div class="photo-thumb photo-thumb-icon">' + iconFor(it.file) + '</div>';
|
||||
}
|
||||
inner += '<div class="photo-info">'
|
||||
+ '<span class="photo-name" title="' + esc(it.name) + '">' + esc(it.name) + '</span>'
|
||||
|
||||
@@ -222,7 +222,7 @@ $tripField = [
|
||||
$checkedDel = old_array('delete_attachments'); // 保存失败时恢复勾选状态
|
||||
?>
|
||||
<fieldset class="fieldset" id="photo-field">
|
||||
<legend>照片附件(可多张)</legend>
|
||||
<legend>附件(照片 / 视频 / 文档)</legend>
|
||||
|
||||
<?php if ($existingAtts !== []): ?>
|
||||
<div class="form-row">
|
||||
@@ -252,19 +252,21 @@ $tripField = [
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-row">
|
||||
<label>添加照片 / 附件</label>
|
||||
<label>添加附件(照片 / 视频 / 文档)</label>
|
||||
<?php /* 用 div + role=button 而不是 <label>:label 包着隐藏 input 时,
|
||||
点击会在部分浏览器里触发两次文件对话框。这里统一由 JS 打开。 */ ?>
|
||||
<div class="drop-zone" id="node-photo-drop" tabindex="0" role="button"
|
||||
aria-label="选择或拖入照片">
|
||||
aria-label="选择或拖入附件">
|
||||
<input type="file" id="node-photo-input" name="files[]" multiple
|
||||
accept="image/*,application/pdf,.heic,.heif" style="display:none;">
|
||||
accept="image/*,.heic,.heif,.pdf,.mp4,.mov,.doc,.docx,.xls,.xlsx,.zip"
|
||||
style="display:none;">
|
||||
<div class="drop-zone-inner">
|
||||
<div class="drop-zone-icon">📷</div>
|
||||
<p class="drop-zone-title">点击选择,或把照片拖到这里</p>
|
||||
<p class="drop-zone-title">点击选择,或把照片 / 视频 / 文件拖到这里</p>
|
||||
<p class="muted drop-zone-hint">
|
||||
可一次选多张;登机牌、车票、门票随手拍下来即可。
|
||||
单张上限 <?= (int)($maxPhotoMb ?? 12) ?>MB,一次最多 <?= (int)\App\Uploads::MAX_FILES ?> 个。
|
||||
照片、视频(MP4 / MOV)、文档(PDF / Word / Excel)、ZIP 都可以。
|
||||
照片单张上限 <?= (int)($maxPhotoMb ?? 12) ?>MB,视频与文档单个上限 <?= (int)(\App\Uploads::MAX_FILE_BYTES / 1048576) ?>MB,
|
||||
一次最多 <?= (int)\App\Uploads::MAX_FILES ?> 个。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,9 +599,12 @@ $tripField = [
|
||||
|
||||
var MAX_FILES = <?= (int)\App\Uploads::MAX_FILES ?>;
|
||||
var MAX_ONE = <?= (int)($maxPhotoMb ?? 12) ?> * 1024 * 1024;
|
||||
var FILE_MAX = <?= (int)\App\Uploads::MAX_FILE_BYTES ?>;
|
||||
var POST_LIMIT = <?= (int)ini_bytes((string)ini_get('post_max_size')) ?>;
|
||||
|
||||
var IMG_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'];
|
||||
var VIDEO_EXT = ['mp4', 'mov', 'm4v'];
|
||||
var PDF_EXT = ['pdf'];
|
||||
var picked = []; // {uid, file, key, name, origName, type, origUrl, thumb, image, croppable, cropped, reviewed}
|
||||
var seq = 0;
|
||||
|
||||
@@ -619,6 +624,12 @@ $tripField = [
|
||||
if (f.type && f.type.indexOf('image/') === 0) return true;
|
||||
return IMG_EXT.indexOf(extOf(f.name)) >= 0;
|
||||
}
|
||||
function iconFor(f) {
|
||||
var ext = extOf(f.name);
|
||||
if (VIDEO_EXT.indexOf(ext) >= 0) return '🎬';
|
||||
if (PDF_EXT.indexOf(ext) >= 0) return '📄';
|
||||
return '📎';
|
||||
}
|
||||
// 浏览器画布解不开的格式(典型是 HEIC),裁不了,只能原图直传
|
||||
function canvasSafe(f) {
|
||||
var t = String(f.type || '');
|
||||
@@ -695,7 +706,7 @@ $tripField = [
|
||||
card.innerHTML =
|
||||
(it.thumb
|
||||
? '<img src="' + esc(it.thumb) + '" alt="">'
|
||||
: '<div class="attachment-icon">' + esc((extOf(it.name).toUpperCase()) || 'FILE') + '</div>')
|
||||
: '<div class="attachment-icon">' + iconFor(it) + '</div>')
|
||||
+ (canSync
|
||||
? '<button type="button" class="photo-del" data-idx="' + idx + '" title="移除">×</button>'
|
||||
: '')
|
||||
@@ -760,6 +771,11 @@ $tripField = [
|
||||
errors.push(f.name + ':超过单张 ' + (MAX_ONE / 1024 / 1024) + 'MB');
|
||||
return;
|
||||
}
|
||||
/* 大小分级:照片走 photo_max_mb;视频 / 文档走更宽的 FILE_MAX(后端同款逻辑) */
|
||||
if (!isImageFile(f) && f.size > FILE_MAX) {
|
||||
errors.push(f.name + ':文件超过 ' + (FILE_MAX / 1024 / 1024) + 'MB');
|
||||
return;
|
||||
}
|
||||
var image = isImageFile(f);
|
||||
var url = image ? URL.createObjectURL(f) : '';
|
||||
var croppable = image && canvasSafe(f);
|
||||
|
||||
Reference in New Issue
Block a user