v41 第 41 批:流水线提速 + 12 项界面/交互开发 + DAV 彻底修复
DAV(用户「务必彻底解决」): - 真根因是 home-set 层级错:CalDav::HOME_PATH 原为 /dav/calendars/、 CardDav 原为 /carddav/addressbooks/,而日历本/通讯录的 href 在 /dav/N.ics/ 与 /carddav/N.vcf/ —— home-set 下面一本都没有。 Thunderbird 按 home-set 前缀认成员,于是「找不到日历本 / 找不到联系人」。 改为 /dav/ 与 /carddav/(所有既有 URL 一字不变,老配置不作废)。 - 又发现并修掉 Depth:0 违反 RFC 4918:原实现无论 depth 都列出全部子集合, 且第一条 response 是日历本(不含 current-user-principal)。改为按 depth 分流, Depth:0 只回资源自身、Depth:1 才列出子集合。 界面与交互: - 观影:新建影院弹窗层级修正 + 详细地址;照片左右按钮悬停不再位移; 弹框底栏改「确定 / 取消」。 - 旅程:新建旅程改弹窗并按观影式排版;详情页四个动作按钮按用户指定顺序与配色; 编辑与新增行程弹窗化(单容器 + AJAX);行程时间控件改 AntD(精确到分); 参与人默认带上自己;车次号/车牌号紧跟交通方式词。 - 资源:新建/编辑改弹窗 + 观影式排版 + 新建时即定共享人。 - 纪念日:新建改弹框 + 观影式排版。 验证: - e2e 1510/0;v41 十二项真机 48/0;DAV 反向验证 8/0; 部署断言空跑 337/0;模板 div 深度三形态穷举 33 组全配平。 - 线上 DAV 终验(--resolve 真实入口 + 真 DAV 口令)12/0; 线上确认 Depth:0 恰好 1 个 response。
This commit is contained in:
+156
-45
@@ -91,58 +91,100 @@ $myPerson = (int)\App\Auth::personId();
|
||||
</div>
|
||||
|
||||
<?php if ($canWrite): ?>
|
||||
<div class="card card-pad" id="resAddCard" <?= $isEditing ? '' : 'hidden' ?>>
|
||||
<h2 class="cinema-h2"><?= $isEditing ? '编辑资源' : '新建资源' ?></h2>
|
||||
<form method="post" enctype="multipart/form-data"
|
||||
<?php /* 🔴 v41(用户 2026-10-10 第 9 条):「添加资源,采用弹窗形式,并在新建资源的
|
||||
时候,就确定共享人。排版和观影一样,前面是标题,后面跟着冒号,然后是各类输入框。
|
||||
各类框,放在一个页面,包括共享人和附件,编辑也是一样,直接复用添加这个输入框即可。」
|
||||
|
||||
⇒ 三件事:
|
||||
① 从"页面里可开合的一块"改成**弹框**(`#resAddCard` → `.cal-modal`);
|
||||
② 排版改观影式(`.mv-row`:左标签固定 62px + 右对齐 + 自动冒号);
|
||||
③ **共享人**与**附件**都放进这一张表单(原来共享只在那条的「共享」弹框里做)。
|
||||
⚠️ `$isEditing` 时弹框**自动打开**(从列表点"编辑"进来就是要改它)。 */ ?>
|
||||
<div class="cal-modal" id="resAddCard" role="dialog" aria-modal="true"
|
||||
aria-label="<?= $isEditing ? '编辑资源' : '新建资源' ?>"
|
||||
<?= ($isEditing || !empty($resOpen)) ? '' : 'hidden' ?>>
|
||||
<div class="cal-modal-panel mv-panel">
|
||||
<div class="cal-modal-head">
|
||||
<b id="resAddTitle"><?= $isEditing ? '编辑资源' : '新建资源' ?></b>
|
||||
<button type="button" class="cal-modal-x" id="resAddX" aria-label="关闭">✕</button>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" id="resForm"
|
||||
action="<?= e(url($isEditing ? 'resources/update' : 'resources/create')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<?php if ($isEditing): ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
|
||||
<?php endif; ?>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>归属于谁 *</label>
|
||||
<?php /* v24 第 11 条:与全局选人组件统一(原来是下拉,名册一长就难找) */ ?>
|
||||
<?php
|
||||
$ppName = 'owner_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = [(int)($editing['owner_id'] ?? (int)\App\Auth::personId())];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择归属人';
|
||||
$ppTitle = '资源归属于谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="rs-title">标题 *</label>
|
||||
<input class="input" id="rs-title" name="title" required
|
||||
value="<?= e($editing['title'] ?? '') ?>" placeholder="如:身份证 / 学位证">
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="rs-content">正文</label>
|
||||
<textarea class="input" id="rs-content" name="content" rows="3"
|
||||
placeholder="编号、有效期、放在哪里…"><?= e($editing['content'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label for="sd-attach">附件(可多选)</label>
|
||||
<?php /* ⚠️ 新建时资源还不存在,附件只能在这里一起提交(建完再挂)。
|
||||
列表里那条的附件管理则走右上角蓝色的「附件」弹框。
|
||||
两处的文件框 id 必须**不同**(同 id 会撞),所以这里显式给一个。 */
|
||||
$sdName = 'attach[]'; $sdSlot = 'photo'; $sdId = 'sd-attach';
|
||||
require __DIR__ . '/../_stage_drop.php'; ?>
|
||||
<div class="mv-form-inner">
|
||||
<div class="mv-grid">
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">归属于谁<i>*</i></span>
|
||||
<div class="mv-fill">
|
||||
<?php
|
||||
$ppName = 'owner_id';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = [(int)($editing['owner_id'] ?? (int)\App\Auth::personId())];
|
||||
$ppMode = 'single';
|
||||
$ppOpenBtn = '选择归属人';
|
||||
$ppTitle = '资源归属于谁';
|
||||
$ppEmptyText = '未选择';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">标题<i>*</i></span>
|
||||
<input class="input" id="rs-title" name="title" required
|
||||
value="<?= e($editing['title'] ?? '') ?>" placeholder="如:身份证 / 学位证">
|
||||
</div>
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">正文</span>
|
||||
<textarea class="input" id="rs-content" name="content" rows="3"
|
||||
placeholder="编号、有效期、放在哪里…"><?= e($editing['content'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<?php /* 🔴 v41:**共享给谁**(用户第 9 条"新建的时候就确定共享人")。
|
||||
⚠️ 这里只给"只读" —— 新建时最多到只读;要给人可写,
|
||||
去列表里那条资源的「共享」弹框单独设(那里能选权限)。
|
||||
⚠️ 归属人自己会被服务端自动跳过(不会产生多余共享记录)。 */ ?>
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">共享</span>
|
||||
<div class="mv-fill">
|
||||
<?php
|
||||
$ppName = 'share_ids[]';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = array_map('intval', array_column(
|
||||
$isEditing ? \App\Resource::shares((int)$editing['id']) : [], 'person_id'));
|
||||
$ppMode = 'multi';
|
||||
$ppOpenBtn = '+ 共享给谁';
|
||||
$ppTitle = '这条资源共享给谁(只读)';
|
||||
$ppTip = '归属人不用勾(本来就是他的)。要给人「可读写」,到列表里那条的「共享」里设。';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mv-row mv-row-media">
|
||||
<span class="mv-lb">附件</span>
|
||||
<div class="mv-media-body">
|
||||
<?php /* ⚠️ 新建时资源还不存在,附件只能在这里一起提交(建完再挂)。
|
||||
列表里那条的附件管理则走右上角蓝色的「附件」弹框。
|
||||
两处的文件框 id 必须**不同**(同 id 会撞),所以这里显式给一个。 */
|
||||
$sdName = 'attach[]'; $sdSlot = 'photo'; $sdId = 'sd-attach';
|
||||
require __DIR__ . '/../_stage_drop.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit"><?= $isEditing ? '保存' : '新建' ?></button>
|
||||
<?php if ($isEditing): ?>
|
||||
<a class="btn btn-ghost" href="<?= e(url('resources')) ?>">取消</a>
|
||||
<?php endif; ?>
|
||||
<div class="cal-modal-foot">
|
||||
<span class="cal-modal-tip" id="resAddTip"></span>
|
||||
<button class="btn btn-sm" type="submit" form="resForm" id="resAddSubmit">确定</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" id="resAddCancel">取消</button>
|
||||
</div>
|
||||
</div><?php /* /.cal-modal-panel */ ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* 🔴 v41:列表内容包一层 `#resListHost` —— 弹框保存成功后前端只换这一块。 */ ?>
|
||||
<div id="resListHost">
|
||||
<div class="card card-pad">
|
||||
<h2 class="cinema-h2">
|
||||
资源列表
|
||||
@@ -458,19 +500,88 @@ $myPerson = (int)\App\Auth::personId();
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><?php /* /#resListHost */ ?>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* ---- 「新建资源」开合(与旧版一致)---- */
|
||||
/* ---- 🔴 v41(用户第 9 条):「新建资源」改成**弹框** ----
|
||||
⚠️ 原来是"页面里可开合的一块"(点一下展开、再点收起)。
|
||||
现在它与「编辑」共用同一个弹框(`#resAddCard`),
|
||||
所以打开时要清空上次的输入(否则会带着上一次的内容)。
|
||||
⚠️ 提交走 AJAX + 只换列表容器:用户没要求,但"弹框里点确定却整页跳走"
|
||||
会丢掉滚动位置;既然列表页已经有 `#resListHost` 这样的容器就顺手接上。 */
|
||||
var addBtn = document.getElementById('resAddBtn');
|
||||
var addCard = document.getElementById('resAddCard');
|
||||
function setResOpen(on) {
|
||||
if (!addCard) { return; }
|
||||
addCard.hidden = !on;
|
||||
if (on) {
|
||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||
var t = document.getElementById('rs-title');
|
||||
if (t) { t.focus(); }
|
||||
}
|
||||
}
|
||||
if (addBtn && addCard) {
|
||||
addBtn.addEventListener('click', function () {
|
||||
addCard.hidden = !addCard.hidden;
|
||||
addBtn.textContent = addCard.hidden ? '新建资源' : '收起';
|
||||
if (!addCard.hidden) { addCard.scrollIntoView({ block: 'nearest' }); }
|
||||
addBtn.addEventListener('click', function () { setResOpen(true); });
|
||||
}
|
||||
['resAddX', 'resAddCancel'].forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) { el.addEventListener('click', function () { setResOpen(false); }); }
|
||||
});
|
||||
if (addCard) {
|
||||
addCard.addEventListener('click', function (e) {
|
||||
if (e.target === addCard) { setResOpen(false); } // 点遮罩关
|
||||
});
|
||||
}
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape' || !addCard || addCard.hidden) { return; }
|
||||
var d = document.querySelector('.ant-picker-dropdown');
|
||||
if (d && !d.classList.contains('ant-picker-dropdown-hidden')) { return; }
|
||||
setResOpen(false);
|
||||
});
|
||||
/* 提交(click 主路 + submit 兜回车;理由见观影/旅程那两处注释) */
|
||||
function resSave() {
|
||||
var form = document.getElementById('resForm');
|
||||
if (!form || !window.fetch || !window.FormData) { return false; }
|
||||
var s = document.getElementById('resAddSubmit');
|
||||
if (s) { s.disabled = true; s.textContent = '保存中…'; }
|
||||
fetch(form.action, {
|
||||
method: 'POST', body: new FormData(form),
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
}).then(function (r) {
|
||||
return r.text().then(function (html) {
|
||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
var fresh = doc.getElementById('resListHost');
|
||||
var cur = document.getElementById('resListHost');
|
||||
if (fresh && cur) { cur.innerHTML = fresh.innerHTML; }
|
||||
else { window.location.reload(); return; }
|
||||
setResOpen(false);
|
||||
});
|
||||
}).catch(function () {
|
||||
if (s) { s.disabled = false; s.textContent = '确定'; }
|
||||
form.submit();
|
||||
}).then(function () {
|
||||
if (s) { s.disabled = false; s.textContent = '确定'; }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
document.addEventListener('click', function (ev) {
|
||||
var t = ev.target;
|
||||
if (!t || !t.closest || !t.closest('#resAddSubmit')) { return; }
|
||||
if (resSave()) { ev.preventDefault(); }
|
||||
});
|
||||
document.addEventListener('submit', function (ev) {
|
||||
if (ev.target && ev.target.id === 'resForm') {
|
||||
if (resSave()) { ev.preventDefault(); }
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* ============================================================
|
||||
动作 → 一份共享弹框(v24 第 23 批 用户第 1 条;
|
||||
v28 第 7 条把「编辑 / 附件 / 共享」合成一个**带页签**的面板)
|
||||
|
||||
Reference in New Issue
Block a user