Files
laoyang 331fadfb48 地图与正文同宽 / 详情页封面改列表行样式 / 封面拖拽上传(批 21)
① 带地图的页面(首页旅行地图、详情页行程地图)原本铺满整个视口
  (批 18 的负 margin 出血),用户反馈「上面正文固定宽、下面地图铺满屏」
  看着奇怪 → 改回与正文版心同宽,bleed() 已删、CSS 去边框覆盖已删。

② 活动详情页顶部整幅 16:9 大封面太大 → 改用活动列表同款「封面行」
  (.act-row-static:小封面 + 参与人 + 统计),数据复用 collectStats
  同一聚合口径;.activity-cover 样式删除。

③ 创建/编辑活动封面支持直接把图片拖进上传区:drop→DataTransfer 塞给
  原有 file input 并触发 change,类型/大小校验与裁剪流程原样复用;
  拖入悬停给高亮反馈。

验证:本地全链(含 _map12/_map16 同宽与封面行断言、5 档视口)全绿;
部署哨兵同步改向(批 17/18 锚定旧实现的断言全部反向或改口径)。
2026-09-24 22:13:29 +08:00

218 lines
9.0 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$isEdit = $activity !== null;
$cover = $isEdit ? (string)($activity['cover_image'] ?? '') : '';
// 预览也走媒体代理:页面里不出现 /uploads/... 真实路径
$coverPreview = ($isEdit && $cover !== '') ? media_cover((int)$activity['id']) : '';
?>
<div class="page-header">
<div>
<h1><?= $isEdit ? '编辑活动' : '新建活动' ?></h1>
<?php if ($isEdit): ?>
<p class="muted">
创建于 <?= e(date('Y-m-d', strtotime($activity['created_at']))) ?>
· 最近更新 <?= e(date('Y-m-d H:i', strtotime($activity['updated_at'] ?? $activity['created_at']))) ?>
</p>
<?php endif; ?>
</div>
<?php if ($isEdit): ?>
<a href="<?= url('activities/show', ['id' => $activity['id']]) ?>" class="btn">返回活动</a>
<?php else: ?>
<a href="<?= url('activities/index') ?>" class="btn">返回列表</a>
<?php endif; ?>
</div>
<form method="post" action="<?= url($isEdit ? 'activities/update' : 'activities/store') ?>" class="form">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<?php if ($isEdit): ?>
<input type="hidden" name="id" value="<?= (int)$activity['id'] ?>">
<?php endif; ?>
<div class="form-row">
<label>封面图</label>
<div class="cover-upload" id="cover-upload-box">
<div class="cover-preview" id="cover-preview">
<img id="cover-preview-img" src="<?= e($coverPreview) ?>" alt=""
style="<?= $coverPreview === '' ? 'display:none;' : '' ?>">
<div class="cover-placeholder" id="cover-placeholder"
style="<?= $cover === '' ? '' : 'display:none;' ?>">
<div style="font-size:32px;">🖼️</div>
<div style="font-size:12px; margin-top:6px;">16:9 封面</div>
</div>
</div>
<div class="cover-upload-actions">
<button type="button" class="btn-sm" id="btn-upload-cover"><?= $cover === '' ? '选择图片' : '更换图片' ?></button>
<button type="button" class="btn-sm btn-danger" id="btn-remove-cover"
style="<?= $cover === '' ? 'display:none;' : '' ?>">移除</button>
<span class="muted" style="font-size:12px;">支持 JPG/PNG/WebP,可直接拖进来或点选,裁剪为 800×450</span>
</div>
<input type="file" id="cover-file-input" accept="image/*" style="display:none;">
<input type="hidden" name="cover_image" id="cover-url-input" value="<?= e($cover) ?>">
</div>
</div>
<div class="form-row">
<label>活动标题 *</label>
<input type="text" name="title" required maxlength="200" placeholder="例如:2026 盛夏去奥森"
value="<?= e($activity['title'] ?? '') ?>">
</div>
<div class="form-row">
<label>活动简介</label>
<textarea name="description" rows="4" placeholder="简单描述一下这次活动..."><?= e($activity['description'] ?? '') ?></textarea>
</div>
<div class="form-actions">
<a href="<?= url($isEdit ? 'activities/show' : 'activities/index', $isEdit ? ['id' => $activity['id']] : []) ?>" class="btn">取消</a>
<button type="submit" class="btn-primary"><?= $isEdit ? '保存' : '创建' ?></button>
</div>
</form>
<!-- 裁剪模态框 -->
<div class="modal" id="cover-crop-modal" style="display:none;">
<div class="modal-backdrop" id="cover-crop-backdrop"></div>
<div class="modal-content modal-content-wide">
<h2>裁剪封面图</h2>
<div class="crop-container crop-container-wide">
<img id="cover-crop-image" src="" alt="">
</div>
<div class="modal-actions">
<button type="button" class="btn" id="btn-cancel-cover-crop">取消</button>
<button type="button" class="btn-primary" id="btn-confirm-cover-crop">确认裁剪</button>
</div>
</div>
</div>
<script>
(function () {
var fileInput = document.getElementById('cover-file-input');
var btnUpload = document.getElementById('btn-upload-cover');
var btnRemove = document.getElementById('btn-remove-cover');
var previewImg = document.getElementById('cover-preview-img');
var placeholder = document.getElementById('cover-placeholder');
var urlInput = document.getElementById('cover-url-input');
var modal = document.getElementById('cover-crop-modal');
var cropImage = document.getElementById('cover-crop-image');
var backdrop = document.getElementById('cover-crop-backdrop');
var btnCancel = document.getElementById('btn-cancel-cover-crop');
var btnConfirm = document.getElementById('btn-confirm-cover-crop');
var csrfToken = '<?= e(csrf_token()) ?>';
if (!btnUpload) return;
var cropper = null;
btnUpload.addEventListener('click', function () { fileInput.click(); });
/* 批 21:直接把图片拖进上传区 —— 走 DataTransfer 塞给原有 file input,
后面的「校验类型/大小 → 打开裁剪」流程原样复用,不另写一套。
dragenter/dragover 必须 preventDefault,否则浏览器会直接打开图片文件。 */
var dropBox = document.getElementById('cover-upload-box');
['dragenter', 'dragover'].forEach(function (t) {
dropBox.addEventListener(t, function (e) {
e.preventDefault();
dropBox.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(function (t) {
dropBox.addEventListener(t, function (e) {
e.preventDefault();
dropBox.classList.remove('dragover');
});
});
dropBox.addEventListener('drop', function (e) {
var files = e.dataTransfer && e.dataTransfer.files;
if (!files || !files.length) return;
var dt = new DataTransfer();
dt.items.add(files[0]); // 封面只有一张:一次拖多张也只取第一张
fileInput.files = dt.files;
fileInput.dispatchEvent(new Event('change'));
});
fileInput.addEventListener('change', function (e) {
var file = e.target.files && e.target.files[0];
if (!file) return;
if (!/^image\//.test(file.type)) { alert('请选择图片文件'); return; }
if (file.size > 15 * 1024 * 1024) { alert('原始图片不能超过 15MB'); return; }
var reader = new FileReader();
reader.onload = function (ev) {
cropImage.src = ev.target.result;
modal.style.display = 'flex';
if (cropper) cropper.destroy();
cropper = new Cropper(cropImage, {
aspectRatio: 16 / 9,
viewMode: 1,
autoCropArea: 0.9,
background: false,
movable: true,
zoomable: true,
rotatable: false,
scalable: false,
});
};
reader.readAsDataURL(file);
});
function closeCrop() {
modal.style.display = 'none';
if (cropper) { cropper.destroy(); cropper = null; }
fileInput.value = '';
}
btnCancel.addEventListener('click', closeCrop);
backdrop.addEventListener('click', closeCrop);
btnConfirm.addEventListener('click', function () {
if (!cropper) return;
btnConfirm.disabled = true;
btnConfirm.textContent = '上传中...';
var canvas = cropper.getCroppedCanvas({
width: 800,
height: 450,
imageSmoothingQuality: 'high',
});
var dataUrl = canvas.toDataURL('image/jpeg', 0.9);
fetch('index.php?r=activities/uploadCover', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ image: dataUrl, old: urlInput.value }),
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
.then(function (res) {
btnConfirm.disabled = false;
btnConfirm.textContent = '确认裁剪';
if (!res.ok || res.data.error) {
alert(res.data.error || '上传失败');
return;
}
urlInput.value = res.data.url;
previewImg.src = res.data.url;
previewImg.style.display = 'block';
placeholder.style.display = 'none';
btnRemove.style.display = 'inline-block';
btnUpload.textContent = '更换图片';
closeCrop();
})
.catch(function (err) {
btnConfirm.disabled = false;
btnConfirm.textContent = '确认裁剪';
alert('网络错误:' + err.message);
});
});
btnRemove.addEventListener('click', function () {
urlInput.value = '';
previewImg.src = '';
previewImg.style.display = 'none';
placeholder.style.display = 'flex';
btnRemove.style.display = 'none';
btnUpload.textContent = '选择图片';
});
})();
</script>