三条都是「静态检查与 e2e 恒绿、只有真机才现形」的,每条先写真机诊断探针 拿到真根因再改,并逐条反向验证(改回旧行为必须被抓住)。 1. 日程 ICS 导入:文件已上传仍提示「请选择一个文本」 真根因:_stage_drop.php 是"拖进来即刻上传到暂存区"的唯一形态—— 拖放时文件走 upload_stage.php 落到 uploads/_staging/,而 <input>.files 始终是空的;两个导入框(ics/通讯录)都带 required → 浏览器原生校验把提交 拦下,弹它自己的文案。就算绕过校验,路由只读 $_FILES['ics']、从不看 stage_token[],暂存文件永远没人认领。 改:片段新增 $sdStage(默认 true)。导入型传 false → 框上不带 data-drop-stage → app.js 走"写回 input.files、随表单提交"的老路,文件走 PHP 临时文件,路由读完即弃;不落存储、不建附件行、不进 OSS。 同时把 app.js 找框的选择器从 [data-drop-stage][data-sd-for=…] 放宽成 [data-sd-for=…]——否则这些框点不开也接不住拖动。 2. 导航联想词"还是不好用" 真根因:nav_show.php 给出的 suggest 地址已带 ?p=<短语>,而 nav-show.js 又 硬拼 '?slug=…' → 实际是 …?p=abc?slug=bing&q=…;PHP 认为第一个 ? 之后整串 都是查询值 → p 匹配不上短语 → 403 → 下拉永远空。服务端一直是对的(直连 200 + 10 条词),e2e 探针自己拼对了参数,所以 HTTP 层恒绿、只有真页面能抓。 改:nav-show.js 新增 withQuery(),按 base 里有没有 ? 决定分隔符; nav_suggest.php 加短语容错(切掉第一个 ?/& 之后的内容,救缓存里的旧 JS)。 3. 导航设置按钮改纯图标 ☰ v29 只改成了下拉菜单,summary 仍是 btn-ghost btn-sm + 文字「设置」+ 一个 向下箭头。改:换成与其他七个模块逐字节相同的 btn-icon + 三条横线。 验证:e2e 1464/0(新增 2 条)、升级 153/0、vCard 40 项、城市 32/0、 桌面几何全过、移动 680/0、交互 92/0、窄屏 30/0、b24_nav 41/0、b24_ui 26/0、 b28 39/0、b29 38/0、b27 34/0;新真机探针 _b30 22/0、反向验证 11/0; 部署静态断言新增 8 条守卫并逐条证伪通过(本轮抓到 2 条恒真守卫 + 1 处 _code_only 的 locale 差异)。线上 7 个文件 SHA256 与本地一致。
101 lines
4.5 KiB
PHP
101 lines
4.5 KiB
PHP
<?php
|
||
/**
|
||
* 名册导入(v24 用户 2026-10-03 第 1 条)。
|
||
*
|
||
* 页面就两件事:挑文件 → 看结果。所以做成**一页**(提交后同页出报告),
|
||
* 不跳到"导入历史"之类的第二页 —— 一次导入看完就该结束。
|
||
*
|
||
* @param array $groups 我能用的分组(导入进来的人可以先归到一组)
|
||
* @param array|null $report ContactImporter::run 的结果(null = 还没导入)
|
||
*/
|
||
$report = $report ?? null;
|
||
$canMove = \App\Auth::canWrite();
|
||
?>
|
||
<div class="page-head">
|
||
<h1>导入联系人</h1>
|
||
<span class="sub">从手机 / 邮箱导出的通讯录批量加进名册</span>
|
||
<div class="actions">
|
||
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card card-pad">
|
||
<h2 class="cinema-h2">选一份文件</h2>
|
||
<p class="small muted" style="margin:0 0 14px">
|
||
支持 <b>.vcf</b>(vCard,iPhone / 安卓 / 谷歌联系人 / Outlook 都能导出)与
|
||
<b>.csv / .txt</b>(表头里有「姓名 / 手机 / 邮箱」之类就能认,中英文表头都行)。
|
||
<br>
|
||
导入规则:<b>手机号或姓名已经在名册里的,不会重复建人</b>,
|
||
只把「他还没有的信息」补上去(已有的一律不动);其余才新建。
|
||
</p>
|
||
|
||
<form method="post" action="<?= e(url('people/import')) ?>" enctype="multipart/form-data">
|
||
<?= csrf_field() ?>
|
||
<div class="form-grid">
|
||
<div class="field full">
|
||
<label for="imp-file">联系人文件 *</label>
|
||
<?php /* 🔴 v30:与 ics 导入同一条(用户第 1 条)—— 导入型上传**不走暂存**。
|
||
文件走 PHP 临时文件,路由读完即弃,不落存储、不建附件行。
|
||
⚠️ 不这么改的话,拖进来的文件只到暂存区、`<input>.files` 是空的,
|
||
required 的原生校验会弹「请选择一个文本。」(与 ics 完全同一个坑)。 */
|
||
$sdName = 'contacts'; $sdId = 'imp-file';
|
||
$sdAccept = '.vcf,.csv,.txt,text/vcard,text/csv,text/plain';
|
||
$sdMultiple = false; $sdRequired = true; $sdStage = false;
|
||
require __DIR__ . '/../_stage_drop.php'; ?>
|
||
</div>
|
||
<div class="field">
|
||
<label for="imp-group">加进哪一组</label>
|
||
<select class="select" id="imp-group" name="group_id">
|
||
<option value="0">先不分类</option>
|
||
<?php foreach ($groups as $g): ?>
|
||
<option value="<?= (int)$g['id'] ?>"><?= e((string)$g['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<span class="tip">只影响<b>新建</b>的那些人;已有的人保持他原来的分组。</span>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn" type="submit">开始导入</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<?php if ($report): ?>
|
||
<div class="card card-pad">
|
||
<h2 class="cinema-h2">
|
||
导入结果
|
||
<span class="hint">新增 <?= (int)$report['added'] ?> · 补充 <?= (int)$report['updated'] ?> · 无需改动 <?= (int)$report['skipped'] ?></span>
|
||
</h2>
|
||
<?php if (!$report['people']): ?>
|
||
<p class="empty-hint">文件里没有可导入的联系人。</p>
|
||
<?php else: ?>
|
||
<?php /* ⚠️ 报告的是"这条记录做了什么",逐条列出来:
|
||
批量操作最怕"导完不知道改了什么"(尤其"补充"这一类会动到已有的人)。 */ ?>
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead><tr><th>联系人</th><th>结果</th><th>补上了什么</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($report['people'] as $row): ?>
|
||
<?php
|
||
$act = (string)$row['action'];
|
||
$word = $act === 'added' ? '新建' : ($act === 'updated' ? '补充信息' : '无需改动');
|
||
$cls = $act === 'added' ? 'led-asset' : ($act === 'updated' ? 'led-asset' : '');
|
||
?>
|
||
<tr>
|
||
<td><?= e((string)$row['name']) ?></td>
|
||
<td class="<?= $cls ?>"><?= e($word) ?></td>
|
||
<td class="small muted">
|
||
<?= $row['added'] ? e(implode('、', (array)$row['added'])) : '—' ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="btn-row" style="margin-top:14px">
|
||
<a class="btn btn-ghost" href="<?= e(url('people')) ?>">去看名册</a>
|
||
<a class="btn btn-ghost" href="<?= e(url('people/import')) ?>">再导一份</a>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|