2026-09-22 22:03:08 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
$isEdit = $isEdit ?? false;
|
|
|
|
|
|
$n = $node; // null=新建;数组=编辑
|
|
|
|
|
|
$curStatus = $n['status'] ?? ($category === 'plan' ? 'planned' : 'completed');
|
|
|
|
|
|
$statusLabels = [
|
|
|
|
|
|
'planned' => '待执行',
|
|
|
|
|
|
'ongoing' => '进行中',
|
|
|
|
|
|
'completed' => '已完成',
|
|
|
|
|
|
'cancelled' => '已取消',
|
|
|
|
|
|
];
|
|
|
|
|
|
// 计划节点不会有"进行中",执行节点默认就是已完成
|
|
|
|
|
|
$statusOptions = $category === 'plan'
|
|
|
|
|
|
? ['planned', 'completed', 'cancelled']
|
|
|
|
|
|
: ['completed', 'ongoing', 'planned', 'cancelled'];
|
|
|
|
|
|
|
|
|
|
|
|
$dtLocal = static function ($value): string {
|
|
|
|
|
|
if (empty($value)) return '';
|
|
|
|
|
|
$ts = strtotime((string)$value);
|
|
|
|
|
|
return $ts ? date('Y-m-d\TH:i', $ts) : '';
|
|
|
|
|
|
};
|
|
|
|
|
|
$sLoc = json_decode_field($n['start_location'] ?? null);
|
|
|
|
|
|
$eLoc = json_decode_field($n['end_location'] ?? null);
|
2026-09-22 23:09:37 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 交通信息只留一个输入框:飞机只要航班号(航空公司是隐含的),火车只要车次。
|
|
|
|
|
|
* 座位号/登机口这类「一趟车多人同行」时根本没法记,所以不再收集。
|
|
|
|
|
|
*/
|
|
|
|
|
|
$tripField = [
|
|
|
|
|
|
'plane' => ['label' => '航班号', 'ph' => '例如:CA1234'],
|
|
|
|
|
|
'train' => ['label' => '车次', 'ph' => '例如:G1234'],
|
|
|
|
|
|
'bus' => ['label' => '班次', 'ph' => '例如:T108、8 路'],
|
|
|
|
|
|
'subway' => ['label' => '线路', 'ph' => '例如:地铁 10 号线'],
|
|
|
|
|
|
'ship' => ['label' => '航次', 'ph' => '例如:渤海轮渡 3 号'],
|
|
|
|
|
|
'taxi' => ['label' => '叫车备注', 'ph' => '例如:尾号 1234'],
|
|
|
|
|
|
'other' => ['label' => '车次 / 班次', 'ph' => '例如:G1234、CA1234'],
|
|
|
|
|
|
];
|
2026-09-22 22:03:08 +08:00
|
|
|
|
?>
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1><?= $isEdit ? '编辑' : '新建' ?><?= $category === 'plan' ? '计划' : '执行' ?>节点</h1>
|
|
|
|
|
|
<p class="muted">
|
|
|
|
|
|
活动:
|
|
|
|
|
|
<a href="<?= url('activities/show', ['id' => $activity['id']]) ?>"><?= e($activity['title']) ?></a>
|
|
|
|
|
|
<?php if ($isEdit && !empty($n['original_plan_node_id'])): ?>
|
|
|
|
|
|
· <span class="tag-default">由计划节点生成</span>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a href="<?= url('activities/show', ['id' => $activity['id']]) ?>" class="btn">返回活动</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-23 07:53:20 +08:00
|
|
|
|
<form method="post" action="<?= url($isEdit ? 'nodes/update' : 'nodes/store') ?>"
|
|
|
|
|
|
enctype="multipart/form-data" class="form" id="node-form">
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
|
|
|
|
|
|
<input type="hidden" name="activity_id" value="<?= (int)$activity['id'] ?>">
|
|
|
|
|
|
<input type="hidden" name="node_category" value="<?= e($category) ?>">
|
|
|
|
|
|
<?php if ($isEdit): ?>
|
|
|
|
|
|
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php if (!$isEdit && $category === 'reality' && !empty($planNodes)): ?>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>关联计划节点(可选)</label>
|
|
|
|
|
|
<select name="original_plan_node_id">
|
|
|
|
|
|
<option value="">— 不关联,此为临时新增 —</option>
|
|
|
|
|
|
<?php foreach ($planNodes as $p): ?>
|
|
|
|
|
|
<option value="<?= (int)$p['id'] ?>">
|
|
|
|
|
|
<?= e($p['title'] ?: '(无标题)') ?> · <?= date('m-d H:i', strtotime($p['start_time'])) ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
2026-09-22 23:09:37 +08:00
|
|
|
|
<div class="muted">关联后可以在计划卡片上看到「已成行」。</div>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>节点类型 *</label>
|
|
|
|
|
|
<select name="node_type_id" required>
|
|
|
|
|
|
<?php foreach ($nodeTypes as $t): ?>
|
|
|
|
|
|
<option value="<?= (int)$t['id'] ?>"
|
|
|
|
|
|
<?= (int)($n['node_type_id'] ?? 0) === (int)$t['id'] ? ' selected' : '' ?>>
|
|
|
|
|
|
<?= e($t['icon'] . ' ' . $t['name']) ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row two-col">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label>开始时间 *</label>
|
|
|
|
|
|
<input type="datetime-local" name="start_time" required value="<?= e($dtLocal($n['start_time'] ?? null)) ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label>结束时间 *</label>
|
|
|
|
|
|
<input type="datetime-local" name="end_time" required value="<?= e($dtLocal($n['end_time'] ?? null)) ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>标题</label>
|
|
|
|
|
|
<input type="text" name="title" maxlength="200" placeholder="例如:出发去奥森"
|
|
|
|
|
|
value="<?= e($n['title'] ?? '') ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<fieldset class="fieldset">
|
2026-09-22 23:09:37 +08:00
|
|
|
|
<legend>出行方式</legend>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label for="travel-mode-select">选择方式</label>
|
|
|
|
|
|
<select name="travel_mode_id" id="travel-mode-select">
|
|
|
|
|
|
<option value="" data-mode-type="">— 不使用 —</option>
|
|
|
|
|
|
<?php foreach ($travelModes as $tm): ?>
|
|
|
|
|
|
<option value="<?= (int)$tm['id'] ?>" data-mode-type="<?= e($tm['mode_type']) ?>"
|
|
|
|
|
|
<?= (int)($n['travel_mode_id'] ?? 0) === (int)$tm['id'] ? ' selected' : '' ?>>
|
|
|
|
|
|
<?= e(($modeLabels[$tm['mode_type']] ?? $tm['mode_type']) . (!empty($tm['plate_number']) ? ' · ' . $tm['plate_number'] : '')) ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<div class="muted">
|
|
|
|
|
|
没有合适的方式?在右上角菜单的「出行方式」里添加。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* 只有一个交通字段:飞机=航班号、火车=车次,标签随方式切换 */ ?>
|
|
|
|
|
|
<div class="form-row" id="field-trip-number" style="display:none;">
|
|
|
|
|
|
<label id="trip-number-label" for="trip-number-input">车次 / 航班号</label>
|
|
|
|
|
|
<input type="text" name="trip_number" id="trip-number-input" maxlength="50"
|
|
|
|
|
|
placeholder="例如:G1234、CA1234"
|
|
|
|
|
|
value="<?= e($n['trip_number'] ?? '') ?>">
|
|
|
|
|
|
<div class="muted" id="trip-number-hint"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row" id="field-no-trip" style="display:none;">
|
|
|
|
|
|
<div class="muted" id="no-trip-hint"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
|
|
|
|
|
<fieldset class="fieldset">
|
|
|
|
|
|
<legend>起点(可选)</legend>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>起点名称(输入地址自动补全)</label>
|
|
|
|
|
|
<input type="text" name="start_location_name" id="start-location-input"
|
|
|
|
|
|
placeholder="输入关键词,如「北京南站」「奥森公园」"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
value="<?= e($sLoc['name'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="start_location_address" id="start-location-address" value="<?= e($sLoc['address'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="start_location_lat" id="start-location-lat" value="<?= e($sLoc['lat'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="start_location_lng" id="start-location-lng" value="<?= e($sLoc['lng'] ?? '') ?>">
|
2026-09-23 07:53:20 +08:00
|
|
|
|
<?php /* 选中下拉项后由 JS 补上 href;服务端先按已存坐标渲染一次,编辑时不至于空白 */ ?>
|
|
|
|
|
|
<?php $sNav = amap_nav_url($sLoc); ?>
|
|
|
|
|
|
<div class="loc-nav" id="start-location-nav"<?= $sNav === '' ? ' style="display:none;"' : '' ?>>
|
|
|
|
|
|
<a class="loc-nav-link" id="start-location-nav-link" href="<?= e($sNav ?: '#') ?>"
|
|
|
|
|
|
target="_blank" rel="noopener">🧭 导航到这里</a>
|
|
|
|
|
|
<span class="muted">保存后节点详情页也会带这个链接</span>
|
|
|
|
|
|
</div>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
|
|
|
|
|
<fieldset class="fieldset">
|
2026-09-22 23:09:37 +08:00
|
|
|
|
<legend>终点(可选)</legend>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>终点名称(输入地址自动补全)</label>
|
|
|
|
|
|
<input type="text" name="end_location_name" id="end-location-input"
|
|
|
|
|
|
placeholder="输入关键词,如「国家大剧院」「三里屯」"
|
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
value="<?= e($eLoc['name'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="end_location_address" id="end-location-address" value="<?= e($eLoc['address'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="end_location_lat" id="end-location-lat" value="<?= e($eLoc['lat'] ?? '') ?>">
|
|
|
|
|
|
<input type="hidden" name="end_location_lng" id="end-location-lng" value="<?= e($eLoc['lng'] ?? '') ?>">
|
2026-09-23 07:53:20 +08:00
|
|
|
|
<?php $eNav = amap_nav_url($eLoc); ?>
|
|
|
|
|
|
<div class="loc-nav" id="end-location-nav"<?= $eNav === '' ? ' style="display:none;"' : '' ?>>
|
|
|
|
|
|
<a class="loc-nav-link" id="end-location-nav-link" href="<?= e($eNav ?: '#') ?>"
|
|
|
|
|
|
target="_blank" rel="noopener">🧭 导航到这里</a>
|
|
|
|
|
|
<span class="muted">保存后节点详情页也会带这个链接</span>
|
|
|
|
|
|
</div>
|
2026-09-22 22:03:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>详细描述</label>
|
|
|
|
|
|
<textarea name="description" rows="4" placeholder="记录一些细节..."><?= e($n['description'] ?? '') ?></textarea>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-23 07:53:20 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/* 照片附件:随节点表单一起提交。
|
|
|
|
|
|
新建时节点还没有 id,附件挂不上去,所以只提示「保存后一起挂上」;
|
|
|
|
|
|
编辑时把已有照片带出来,可勾选删除。 */
|
|
|
|
|
|
$existingAtts = $attachments ?? [];
|
|
|
|
|
|
?>
|
|
|
|
|
|
<fieldset class="fieldset" id="photo-field">
|
|
|
|
|
|
<legend>照片附件(可多张)</legend>
|
|
|
|
|
|
|
|
|
|
|
|
<?php if ($existingAtts !== []): ?>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>已有附件(勾选后保存即删除)</label>
|
|
|
|
|
|
<div class="photo-grid photo-grid-sm" id="node-photo-existing">
|
|
|
|
|
|
<?php foreach ($existingAtts as $att): ?>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
$attId = (int)$att['id'];
|
|
|
|
|
|
$attIsImg = \App\Attachments::isImageRow($att);
|
|
|
|
|
|
$attDesc = \App\Attachments::describe($att);
|
|
|
|
|
|
?>
|
|
|
|
|
|
<label class="photo-pick" title="<?= e($attDesc['name']) ?>">
|
|
|
|
|
|
<?php /* checkbox 排在最前:CSS 用的是 input:checked ~ img 这种兄弟选择器 */ ?>
|
|
|
|
|
|
<input type="checkbox" name="delete_attachments[]" value="<?= $attId ?>">
|
|
|
|
|
|
<?php if ($attIsImg): ?>
|
|
|
|
|
|
<img src="<?= e(media_att($attId)) ?>" alt="" loading="lazy">
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
<span class="photo-pick-ext"><?= e(strtoupper(pathinfo((string)($att['file_name'] ?? ''), PATHINFO_EXTENSION) ?: 'FILE')) ?></span>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<span class="photo-pick-mark" aria-hidden="true">✓</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted">勾上的会在保存时删掉(文件也会一起清理)。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>添加照片 / 附件</label>
|
|
|
|
|
|
<?php /* 用 div + role=button 而不是 <label>:label 包着隐藏 input 时,
|
|
|
|
|
|
点击会在部分浏览器里触发两次文件对话框。这里统一由 JS 打开。 */ ?>
|
|
|
|
|
|
<div class="drop-zone" id="node-photo-drop" tabindex="0" role="button"
|
|
|
|
|
|
aria-label="选择或拖入照片">
|
|
|
|
|
|
<input type="file" id="node-photo-input" name="files[]" multiple
|
|
|
|
|
|
accept="image/*,application/pdf,.heic,.heif" style="display:none;">
|
|
|
|
|
|
<div class="drop-zone-inner">
|
|
|
|
|
|
<div class="drop-zone-icon">📷</div>
|
|
|
|
|
|
<p class="drop-zone-title">点击选择,或把照片拖到这里</p>
|
|
|
|
|
|
<p class="muted drop-zone-hint">
|
|
|
|
|
|
可一次选多张;登机牌、车票、门票随手拍下来即可。
|
|
|
|
|
|
单张上限 <?= (int)($maxPhotoMb ?? 12) ?>MB,一次最多 <?= (int)\App\Uploads::MAX_FILES ?> 个。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-hint" id="node-photo-status" role="status"></div>
|
|
|
|
|
|
<div class="photo-grid photo-grid-sm" id="node-photo-preview"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<?php if ($isEdit): ?>
|
|
|
|
|
|
<fieldset class="fieldset">
|
|
|
|
|
|
<legend>执行状态</legend>
|
|
|
|
|
|
<div class="form-row">
|
2026-09-22 23:09:37 +08:00
|
|
|
|
<label for="status-select">状态</label>
|
|
|
|
|
|
<select name="status" id="status-select">
|
2026-09-22 22:03:08 +08:00
|
|
|
|
<?php foreach ($statusOptions as $st): ?>
|
|
|
|
|
|
<option value="<?= e($st) ?>" <?= $curStatus === $st ? ' selected' : '' ?>>
|
|
|
|
|
|
<?= e($statusLabels[$st]) ?>
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<label>取消原因(状态选「已取消」时填写)</label>
|
|
|
|
|
|
<input type="text" name="cancel_reason" maxlength="500" placeholder="例如:下雨,临时改行程"
|
|
|
|
|
|
value="<?= e($n['cancel_reason'] ?? '') ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-actions">
|
|
|
|
|
|
<a href="<?= url('activities/show', ['id' => $activity['id']]) ?>" class="btn">取消</a>
|
|
|
|
|
|
<button type="submit" class="btn-primary">保存</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
2026-09-22 23:09:37 +08:00
|
|
|
|
// ===== 交通字段:只一个输入框,标签随出行方式变 =====
|
|
|
|
|
|
var TRIP = <?= json_encode($tripField, JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
|
|
// 这些方式不需要车次/航班号(自驾、步行、骑行)
|
|
|
|
|
|
var NO_TRIP = ['drive_own', 'drive_rental', 'walk', 'bike'];
|
|
|
|
|
|
|
2026-09-22 22:03:08 +08:00
|
|
|
|
var modeSelect = document.getElementById('travel-mode-select');
|
2026-09-22 23:09:37 +08:00
|
|
|
|
var tripRow = document.getElementById('field-trip-number');
|
|
|
|
|
|
var tripLabel = document.getElementById('trip-number-label');
|
|
|
|
|
|
var tripInput = document.getElementById('trip-number-input');
|
|
|
|
|
|
var tripHint = document.getElementById('trip-number-hint');
|
|
|
|
|
|
var noTripRow = document.getElementById('field-no-trip');
|
|
|
|
|
|
var noTripHint = document.getElementById('no-trip-hint');
|
|
|
|
|
|
|
|
|
|
|
|
function refreshTransport() {
|
|
|
|
|
|
if (!modeSelect || !tripRow) return;
|
|
|
|
|
|
var opt = modeSelect.options[modeSelect.selectedIndex];
|
|
|
|
|
|
var mode = opt ? (opt.getAttribute('data-mode-type') || '') : '';
|
|
|
|
|
|
|
|
|
|
|
|
if (mode === '') {
|
|
|
|
|
|
tripRow.style.display = 'none';
|
|
|
|
|
|
noTripRow.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (NO_TRIP.indexOf(mode) !== -1) {
|
|
|
|
|
|
tripRow.style.display = 'none';
|
|
|
|
|
|
noTripRow.style.display = '';
|
|
|
|
|
|
noTripHint.textContent = (mode === 'walk' || mode === 'bike')
|
|
|
|
|
|
? '步行 / 骑行不用填车次。'
|
|
|
|
|
|
: '自驾不用填车次,车牌已在出行方式里。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cfg = TRIP[mode] || TRIP['other'];
|
|
|
|
|
|
noTripRow.style.display = 'none';
|
|
|
|
|
|
tripRow.style.display = '';
|
|
|
|
|
|
tripLabel.textContent = cfg.label;
|
|
|
|
|
|
tripInput.placeholder = cfg.ph;
|
|
|
|
|
|
tripHint.textContent = '只填这一个就够了' +
|
|
|
|
|
|
(mode === 'plane' ? '——航班号本身就包含了航空公司。'
|
|
|
|
|
|
: mode === 'train' ? '——车次就够,座位号对多人同行没意义。'
|
|
|
|
|
|
: '。');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 22:03:08 +08:00
|
|
|
|
if (modeSelect) {
|
2026-09-22 23:09:37 +08:00
|
|
|
|
modeSelect.addEventListener('change', refreshTransport);
|
|
|
|
|
|
refreshTransport();
|
2026-09-22 22:03:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 07:53:20 +08:00
|
|
|
|
// ===== 起点 / 终点的「🧭 导航到这里」 =====
|
|
|
|
|
|
// 规则必须与服务端 amap_nav_url() 保持一致(改一边就得改另一边):
|
|
|
|
|
|
// 有经纬度 → navigation;只填了名字 → search。
|
|
|
|
|
|
// ⚠️ navigation 的坐标顺序是「经度,纬度」(lng 在前),写反了会导到地球另一边。
|
|
|
|
|
|
var NAV_SRC = 'trip.yangyq.net';
|
|
|
|
|
|
|
|
|
|
|
|
function navUrl(name, address, lng, lat) {
|
|
|
|
|
|
name = String(name || '').trim();
|
|
|
|
|
|
address = String(address || '').trim();
|
|
|
|
|
|
lng = String(lng || '').trim();
|
|
|
|
|
|
lat = String(lat || '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (lng !== '' && lat !== '' && isFinite(Number(lng)) && isFinite(Number(lat))) {
|
|
|
|
|
|
// 只编码名称,逗号留字面量 —— 高德要的是 lng,lat,name 三段式
|
|
|
|
|
|
return 'https://uri.amap.com/navigation?to=' + lng + ',' + lat
|
|
|
|
|
|
+ (name !== '' ? ',' + encodeURIComponent(name) : '')
|
|
|
|
|
|
+ '&mode=car&policy=1&src=' + NAV_SRC + '&coordinate=gaode&callnative=1';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var keyword = name !== '' ? name : address;
|
|
|
|
|
|
if (keyword === '') return '';
|
|
|
|
|
|
return 'https://uri.amap.com/search?keyword=' + encodeURIComponent(keyword) + '&src=' + NAV_SRC;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function refreshLocNav(prefix) {
|
|
|
|
|
|
var box = document.getElementById(prefix + '-location-nav');
|
|
|
|
|
|
var link = document.getElementById(prefix + '-location-nav-link');
|
|
|
|
|
|
if (!box || !link) return;
|
|
|
|
|
|
|
|
|
|
|
|
var nameEl = document.getElementById(prefix + '-location-input');
|
|
|
|
|
|
var addrEl = document.getElementById(prefix + '-location-address');
|
|
|
|
|
|
var latEl = document.getElementById(prefix + '-location-lat');
|
|
|
|
|
|
var lngEl = document.getElementById(prefix + '-location-lng');
|
|
|
|
|
|
|
|
|
|
|
|
var lng = lngEl ? lngEl.value : '';
|
|
|
|
|
|
var lat = latEl ? latEl.value : '';
|
|
|
|
|
|
var href = navUrl(nameEl ? nameEl.value : '', addrEl ? addrEl.value : '', lng, lat);
|
|
|
|
|
|
|
|
|
|
|
|
if (href === '') {
|
|
|
|
|
|
box.style.display = 'none';
|
|
|
|
|
|
link.setAttribute('href', '#');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
link.setAttribute('href', href);
|
|
|
|
|
|
box.style.display = '';
|
|
|
|
|
|
var hint = box.querySelector('.muted');
|
|
|
|
|
|
if (hint) {
|
|
|
|
|
|
hint.textContent = (lng !== '' && lat !== '')
|
|
|
|
|
|
? '点开就是到这里的驾车导航(手机端会尝试唤起高德 App)'
|
|
|
|
|
|
: '还没选中具体坐标,点开是高德搜索这个地名';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
refreshLocNav('start');
|
|
|
|
|
|
refreshLocNav('end');
|
|
|
|
|
|
|
|
|
|
|
|
// 手动敲字(没从下拉里选)也要能刷出链接,所以这里给两侧输入框各挂一个 blur。
|
|
|
|
|
|
// 故意挂在 AMap 的加载回调之外:高德的脚本要是没加载出来(Key 失效 / 域名没绑),
|
|
|
|
|
|
// 用户仍然可以自己敲地名,这时的「搜索这个地名」链接不该跟着一起消失。
|
|
|
|
|
|
['start', 'end'].forEach(function (prefix) {
|
|
|
|
|
|
var el = document.getElementById(prefix + '-location-input');
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
el.addEventListener('blur', function () { refreshLocNav(prefix); });
|
|
|
|
|
|
el.addEventListener('input', function () {
|
|
|
|
|
|
// 手改名字后原先选中的坐标就作废了,否则会拿旧坐标去导航新地名
|
|
|
|
|
|
var latEl = document.getElementById(prefix + '-location-lat');
|
|
|
|
|
|
var lngEl = document.getElementById(prefix + '-location-lng');
|
|
|
|
|
|
if (latEl) latEl.value = '';
|
|
|
|
|
|
if (lngEl) lngEl.value = '';
|
|
|
|
|
|
refreshLocNav(prefix);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-22 22:03:08 +08:00
|
|
|
|
// ===== 高德地址自动补全(自定义下拉) =====
|
|
|
|
|
|
var maxWait = 5000; // 最多等 5 秒
|
|
|
|
|
|
var waited = 0;
|
|
|
|
|
|
|
|
|
|
|
|
function waitForAmap(callback) {
|
|
|
|
|
|
if (window.__AMAP_LOADED && typeof AMap !== 'undefined' && AMap.AutoComplete) {
|
|
|
|
|
|
callback();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (window.__AMAP_ERROR) {
|
|
|
|
|
|
console.error('[AMap] 脚本加载失败:', window.__AMAP_ERROR);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (waited >= maxWait) {
|
2026-09-22 23:09:37 +08:00
|
|
|
|
console.error('[AMap] 等待超时。可能原因:Key 无效 / 未绑定域名 / 无法访问 webapi.amap.com');
|
2026-09-22 22:03:08 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
waited += 200;
|
|
|
|
|
|
setTimeout(function () { waitForAmap(callback); }, 200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
waitForAmap(function () {
|
|
|
|
|
|
setupAutoComplete('start-location-input', 'start');
|
|
|
|
|
|
setupAutoComplete('end-location-input', 'end');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function setupAutoComplete(inputId, prefix) {
|
|
|
|
|
|
var input = document.getElementById(inputId);
|
|
|
|
|
|
if (!input) return;
|
|
|
|
|
|
|
|
|
|
|
|
var addrField = document.getElementById(prefix + '-location-address');
|
|
|
|
|
|
var latField = document.getElementById(prefix + '-location-lat');
|
|
|
|
|
|
var lngField = document.getElementById(prefix + '-location-lng');
|
|
|
|
|
|
|
|
|
|
|
|
var dropdown = document.createElement('div');
|
|
|
|
|
|
dropdown.className = 'amap-dropdown';
|
|
|
|
|
|
dropdown.style.display = 'none';
|
|
|
|
|
|
var wrapper = input.closest('.form-row') || input.parentElement;
|
|
|
|
|
|
wrapper.style.position = 'relative';
|
|
|
|
|
|
wrapper.appendChild(dropdown);
|
|
|
|
|
|
|
|
|
|
|
|
var autoComplete = new AMap.AutoComplete({});
|
|
|
|
|
|
var currentIndex = -1;
|
|
|
|
|
|
var currentResults = [];
|
|
|
|
|
|
|
2026-09-22 23:09:37 +08:00
|
|
|
|
function escapeHtml(s) {
|
|
|
|
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
|
|
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 22:03:08 +08:00
|
|
|
|
function renderDropdown(results) {
|
|
|
|
|
|
currentResults = results || [];
|
|
|
|
|
|
currentIndex = -1;
|
|
|
|
|
|
if (!currentResults.length) {
|
|
|
|
|
|
dropdown.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
dropdown.innerHTML = currentResults.map(function (item, i) {
|
2026-09-22 23:09:37 +08:00
|
|
|
|
var sub = [item.district || '', item.address || ''].filter(Boolean).join(' ');
|
|
|
|
|
|
return '<div class="amap-dropdown-item" data-index="' + i + '">'
|
|
|
|
|
|
+ '<div class="amap-dropdown-name">' + escapeHtml(item.name || '') + '</div>'
|
|
|
|
|
|
+ (sub ? '<div class="amap-dropdown-sub">' + escapeHtml(sub) + '</div>' : '')
|
|
|
|
|
|
+ '</div>';
|
2026-09-22 22:03:08 +08:00
|
|
|
|
}).join('');
|
|
|
|
|
|
dropdown.style.display = 'block';
|
|
|
|
|
|
dropdown.querySelectorAll('.amap-dropdown-item').forEach(function (el) {
|
|
|
|
|
|
el.addEventListener('mousedown', function (ev) {
|
|
|
|
|
|
ev.preventDefault(); // 防止 input 失焦
|
2026-09-22 23:09:37 +08:00
|
|
|
|
selectItem(currentResults[parseInt(el.getAttribute('data-index'), 10)]);
|
2026-09-22 22:03:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectItem(item) {
|
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
input.value = item.name || '';
|
2026-09-22 23:09:37 +08:00
|
|
|
|
if (addrField) addrField.value = item.address || item.district || '';
|
2026-09-22 22:03:08 +08:00
|
|
|
|
if (item.location) {
|
|
|
|
|
|
if (latField) latField.value = item.location.lat;
|
|
|
|
|
|
if (lngField) lngField.value = item.location.lng;
|
|
|
|
|
|
}
|
|
|
|
|
|
dropdown.style.display = 'none';
|
2026-09-23 07:53:20 +08:00
|
|
|
|
// 坐标刚变,导航链接要跟着变(有坐标才是真导航,没坐标只是搜索)
|
|
|
|
|
|
refreshLocNav(prefix);
|
2026-09-22 22:03:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var searchTimer = null;
|
|
|
|
|
|
input.addEventListener('input', function () {
|
|
|
|
|
|
var keyword = input.value.trim();
|
|
|
|
|
|
clearTimeout(searchTimer);
|
|
|
|
|
|
if (keyword.length < 2) {
|
|
|
|
|
|
dropdown.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
searchTimer = setTimeout(function () {
|
|
|
|
|
|
autoComplete.search(keyword, function (status, result) {
|
|
|
|
|
|
if (status === 'complete' && result && result.tips) {
|
|
|
|
|
|
renderDropdown(result.tips);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dropdown.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
input.addEventListener('blur', function () {
|
|
|
|
|
|
setTimeout(function () { dropdown.style.display = 'none'; }, 200);
|
|
|
|
|
|
});
|
|
|
|
|
|
input.addEventListener('focus', function () {
|
|
|
|
|
|
if (currentResults.length) dropdown.style.display = 'block';
|
|
|
|
|
|
});
|
2026-09-22 23:09:37 +08:00
|
|
|
|
input.addEventListener('keydown', function (ev) {
|
2026-09-22 22:03:08 +08:00
|
|
|
|
if (dropdown.style.display === 'none') return;
|
|
|
|
|
|
var items = dropdown.querySelectorAll('.amap-dropdown-item');
|
|
|
|
|
|
if (!items.length) return;
|
|
|
|
|
|
|
2026-09-22 23:09:37 +08:00
|
|
|
|
function highlight() {
|
|
|
|
|
|
items.forEach(function (el, i) { el.classList.toggle('active', i === currentIndex); });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ev.key === 'ArrowDown') {
|
|
|
|
|
|
ev.preventDefault();
|
2026-09-22 22:03:08 +08:00
|
|
|
|
currentIndex = (currentIndex + 1) % items.length;
|
2026-09-22 23:09:37 +08:00
|
|
|
|
highlight();
|
|
|
|
|
|
} else if (ev.key === 'ArrowUp') {
|
|
|
|
|
|
ev.preventDefault();
|
2026-09-22 22:03:08 +08:00
|
|
|
|
currentIndex = (currentIndex - 1 + items.length) % items.length;
|
2026-09-22 23:09:37 +08:00
|
|
|
|
highlight();
|
|
|
|
|
|
} else if (ev.key === 'Enter') {
|
2026-09-22 22:03:08 +08:00
|
|
|
|
if (currentIndex >= 0 && currentResults[currentIndex]) {
|
2026-09-22 23:09:37 +08:00
|
|
|
|
ev.preventDefault();
|
2026-09-22 22:03:08 +08:00
|
|
|
|
selectItem(currentResults[currentIndex]);
|
|
|
|
|
|
}
|
2026-09-22 23:09:37 +08:00
|
|
|
|
} else if (ev.key === 'Escape') {
|
2026-09-22 22:03:08 +08:00
|
|
|
|
dropdown.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-09-23 07:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
// ===== 节点表单里的照片附件 =====
|
|
|
|
|
|
// 与「照片与附件」页不同:这里不做裁剪,选好就跟着表单一起提交。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 必须在这一层挡住「总量超 post_max_size」:PHP 遇到超限请求会把 $_POST 与
|
|
|
|
|
|
// $_FILES **整个丢掉**,控制器只能走到 csrf_check() 失败、报「请求已过期」,
|
|
|
|
|
|
// 用户完全看不懂发生了什么。提前拦住才能给一句能行动的话。
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var form = document.getElementById('node-form');
|
|
|
|
|
|
var dropZone = document.getElementById('node-photo-drop');
|
|
|
|
|
|
var input = document.getElementById('node-photo-input');
|
|
|
|
|
|
var preview = document.getElementById('node-photo-preview');
|
|
|
|
|
|
var status = document.getElementById('node-photo-status');
|
|
|
|
|
|
if (!form || !dropZone || !input || !preview) return;
|
|
|
|
|
|
|
|
|
|
|
|
var MAX_FILES = <?= (int)\App\Uploads::MAX_FILES ?>;
|
|
|
|
|
|
var MAX_ONE = <?= (int)($maxPhotoMb ?? 12) ?> * 1024 * 1024;
|
|
|
|
|
|
var POST_LIMIT = <?= (int)ini_bytes((string)ini_get('post_max_size')) ?>;
|
|
|
|
|
|
|
|
|
|
|
|
var IMG_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'];
|
|
|
|
|
|
var picked = []; // [{file, key, url}]
|
|
|
|
|
|
|
|
|
|
|
|
function extOf(name) {
|
|
|
|
|
|
var i = String(name || '').lastIndexOf('.');
|
|
|
|
|
|
return i < 0 ? '' : String(name).slice(i + 1).toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
function isImageFile(f) {
|
|
|
|
|
|
if (f.type && f.type.indexOf('image/') === 0) return true;
|
|
|
|
|
|
return IMG_EXT.indexOf(extOf(f.name)) >= 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
function fmtSize(n) {
|
|
|
|
|
|
n = Number(n) || 0;
|
|
|
|
|
|
if (n < 1024) return n + ' B';
|
|
|
|
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
|
|
|
|
return (n / 1024 / 1024).toFixed(2) + ' MB';
|
|
|
|
|
|
}
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
|
return String(s == null ? '' : s)
|
|
|
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 能不能改写 input.files —— 决定「移除」按钮是否有意义。
|
|
|
|
|
|
// 只判断 DataTransfer 存在是不够的:必须真塞一个文件、再读回来验证赋值生效。
|
|
|
|
|
|
// 否则界面上显示「已移除」、请求里却还带着,用户永远查不出为什么多传了一张。
|
|
|
|
|
|
var canSync = (function () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var probe = new DataTransfer();
|
|
|
|
|
|
probe.items.add(new File([], 'probe.txt'));
|
|
|
|
|
|
input.files = probe.files;
|
|
|
|
|
|
var ok = !!(input.files && input.files.length === 1);
|
|
|
|
|
|
input.files = new DataTransfer().files;
|
|
|
|
|
|
return ok;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
function syncInput() {
|
|
|
|
|
|
if (!canSync) return;
|
|
|
|
|
|
var dt = new DataTransfer();
|
|
|
|
|
|
picked.forEach(function (it) { dt.items.add(it.file); });
|
|
|
|
|
|
input.files = dt.files;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function totalBytes() {
|
|
|
|
|
|
return picked.reduce(function (sum, it) { return sum + it.file.size; }, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setStatus(text, isError) {
|
|
|
|
|
|
if (!status) return;
|
|
|
|
|
|
status.textContent = text || '';
|
|
|
|
|
|
status.className = 'form-hint' + (text && isError ? ' form-hint-error' : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
|
preview.innerHTML = '';
|
|
|
|
|
|
picked.forEach(function (it, idx) {
|
|
|
|
|
|
var card = document.createElement('div');
|
|
|
|
|
|
card.className = 'photo-card';
|
|
|
|
|
|
card.innerHTML =
|
|
|
|
|
|
(it.url
|
|
|
|
|
|
? '<img src="' + it.url + '" alt="">'
|
|
|
|
|
|
: '<div class="attachment-icon">' + esc(extOf(it.file.name).toUpperCase() || 'FILE') + '</div>')
|
|
|
|
|
|
+ (canSync
|
|
|
|
|
|
? '<button type="button" class="photo-del" data-idx="' + idx + '" title="移除">×</button>'
|
|
|
|
|
|
: '')
|
|
|
|
|
|
+ '<div class="photo-cap">'
|
|
|
|
|
|
+ '<span class="photo-cap-name" title="' + esc(it.file.name) + '">' + esc(it.file.name) + '</span>'
|
|
|
|
|
|
+ '<span class="photo-cap-sub">' + esc(fmtSize(it.file.size)) + '</span>'
|
|
|
|
|
|
+ '</div>';
|
|
|
|
|
|
preview.appendChild(card);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!picked.length) {
|
|
|
|
|
|
setStatus('', false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var total = totalBytes();
|
|
|
|
|
|
var over = POST_LIMIT > 0 && total > POST_LIMIT;
|
|
|
|
|
|
setStatus(
|
|
|
|
|
|
picked.length + ' 个待上传 · 合计约 ' + fmtSize(total)
|
|
|
|
|
|
+ (POST_LIMIT > 0 ? '(服务器单次上限 ' + fmtSize(POST_LIMIT) + ')' : '')
|
|
|
|
|
|
+ (over ? ' —— 超了。请分几次保存,或先保存节点、再进「📷 照片」页上传。' : ''),
|
|
|
|
|
|
over
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addFiles(list) {
|
|
|
|
|
|
var arr = Array.prototype.slice.call(list || []);
|
|
|
|
|
|
if (!arr.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
var errors = [];
|
|
|
|
|
|
arr.forEach(function (f) {
|
|
|
|
|
|
if (picked.length >= MAX_FILES) {
|
|
|
|
|
|
errors.push(f.name + ':一次最多 ' + MAX_FILES + ' 个');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var key = f.name + '|' + f.size + '|' + (f.lastModified || 0);
|
|
|
|
|
|
if (canSync && picked.some(function (it) { return it.key === key; })) {
|
|
|
|
|
|
return; // 同一个文件选两次,静默忽略
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isImageFile(f) && MAX_ONE > 0 && f.size > MAX_ONE) {
|
|
|
|
|
|
errors.push(f.name + ':超过单张 ' + (MAX_ONE / 1024 / 1024) + 'MB');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
picked.push({
|
|
|
|
|
|
file: f,
|
|
|
|
|
|
key: key,
|
|
|
|
|
|
url: isImageFile(f) ? URL.createObjectURL(f) : ''
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
syncInput();
|
|
|
|
|
|
render();
|
|
|
|
|
|
if (errors.length) setStatus(errors.join(';'), true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeAt(idx) {
|
|
|
|
|
|
var it = picked[idx];
|
|
|
|
|
|
if (!it) return;
|
|
|
|
|
|
if (it.url) { try { URL.revokeObjectURL(it.url); } catch (e) {} }
|
|
|
|
|
|
picked.splice(idx, 1);
|
|
|
|
|
|
syncInput();
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
preview.addEventListener('click', function (ev) {
|
|
|
|
|
|
var btn = ev.target.closest ? ev.target.closest('button[data-idx]') : null;
|
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
removeAt(parseInt(btn.getAttribute('data-idx'), 10));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 用 div + role=button + 由 JS 打开文件框:label 包隐藏 input 时,
|
|
|
|
|
|
// 部分浏览器一次点击会弹两次文件对话框
|
|
|
|
|
|
dropZone.addEventListener('click', function () { input.click(); });
|
|
|
|
|
|
dropZone.addEventListener('keydown', function (ev) {
|
|
|
|
|
|
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); input.click(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
// 不清 input.value:能改写 files 时下一次选择本来就会替换掉旧的
|
|
|
|
|
|
input.addEventListener('change', function () { addFiles(input.files); });
|
|
|
|
|
|
|
|
|
|
|
|
['dragenter', 'dragover'].forEach(function (t) {
|
|
|
|
|
|
dropZone.addEventListener(t, function (ev) {
|
|
|
|
|
|
ev.preventDefault(); ev.stopPropagation();
|
|
|
|
|
|
dropZone.classList.add('dragging');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
['dragleave', 'dragend'].forEach(function (t) {
|
|
|
|
|
|
dropZone.addEventListener(t, function (ev) {
|
|
|
|
|
|
ev.preventDefault(); ev.stopPropagation();
|
|
|
|
|
|
dropZone.classList.remove('dragging');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
dropZone.addEventListener('drop', function (ev) {
|
|
|
|
|
|
ev.preventDefault(); ev.stopPropagation();
|
|
|
|
|
|
dropZone.classList.remove('dragging');
|
|
|
|
|
|
if (ev.dataTransfer && ev.dataTransfer.files) addFiles(ev.dataTransfer.files);
|
|
|
|
|
|
});
|
|
|
|
|
|
// 别让文件掉在页面别处被浏览器直接打开
|
|
|
|
|
|
window.addEventListener('dragover', function (ev) { ev.preventDefault(); });
|
|
|
|
|
|
window.addEventListener('drop', function (ev) { ev.preventDefault(); });
|
|
|
|
|
|
|
|
|
|
|
|
form.addEventListener('submit', function (ev) {
|
|
|
|
|
|
var total = totalBytes();
|
|
|
|
|
|
if (POST_LIMIT > 0 && total > POST_LIMIT) {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
setStatus('这次要传约 ' + fmtSize(total) + ',超过服务器单次请求上限 '
|
|
|
|
|
|
+ fmtSize(POST_LIMIT) + '。请分几次保存,或先保存节点、再进「📷 照片」页上传。', true);
|
|
|
|
|
|
dropZone.scrollIntoView({ block: 'center' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|
2026-09-22 22:03:08 +08:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|