时间框跳段 + 弹窗不再脏地址栏 + 统计脚本做成后台开关
时间框(flatpickr allowInput 的手输体验)
- flatpickr 把 <input type=datetime-local> 就地改成 type=text,原生分段编辑器被替换掉;
allowInput 又是「整串解析」语义:实测 parseDate('2026') 只出年份、'202666' 才出月份,
于是「年份很长才跳下一位」;'20260606' 还会被静默错解成 2030-12-06 且不报错。
- 新增 static/js/datetime-input.js:自定义 parseDate(纯数字只收 4/6/8/12/14 位,
歧义与非法日期一律拒)+ 输入引导(段满即补分隔符,敲完 4 位年份立刻跳月份位)。
按标准格式不做自动补零、不前滚;越界值原样保留交报错,不静默改成合法值。
- 三处页面(编辑/照片/专题)共用 attachPicker,配置与原先一致,模板净减 5 行。
弹窗
- window.prompt 会把提示文本与默认值写进地址栏并留进历史(confirm/alert 无此问题)。
- 新增 static/js/prompt-box.js 替代:原生 <dialog>.showModal(),遮罩/Esc/焦点齐全,
旧浏览器降级成页面顶部一条。已换 report.html 复制链接兜底与 edit.html 活动改名。
- 门禁 no_prompt_test.go:静态扫模板禁止 prompt(,并钉住 JS 与 CSS 都在。
统计脚本
- 原来只能改 conf/app.ini(要重启),现在后台「站点设置」可开关 + 填写,默认关闭。
- cleanTrackScript 挡住注入面:开闭 <script> 标签数不配平(提前闭合会让页面散架)、
src 的 javascript:/data: 伪协议、超长截断。注意不能用「出现 </script 就拒」当规则——
百度统计、Umami 贴的就是完整 HTML,正常用法会被挡死。
门禁
- datetime-input.test.js:34 条断言(node 直跑,无依赖)。
- TestCleanTrackScript:逐条边界 + 定义性不变量(任何输入都不得产出含 </script 的非空结果)。
- 三项均做过变异验证,改坏实现都会被对应测试抓到。
实测:真实站点逐键 2→20→202→2026-→2026-06-→2026-06-06T;
统计脚本六个场景(默认关/只填不开/开+URL/后台与登录页不挂/危险输入/关掉恢复)全部符合预期。
全量 internal/web 204 例 0 skip,其余包全绿,gofmt 干净。
This commit is contained in:
@@ -804,14 +804,7 @@
|
||||
if (window.flatpickr) {
|
||||
if (flatpickr.l10ns && flatpickr.l10ns.zh) flatpickr.localize(flatpickr.l10ns.zh);
|
||||
document.querySelectorAll('input[type=datetime-local]').forEach(function (el) {
|
||||
flatpickr(el, {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
allowInput: true,
|
||||
// 补录靠回忆,没人记得 12:59:47——5 分钟步进贴合「大概那个点」的精度
|
||||
minuteIncrement: 5,
|
||||
dateFormat: 'Y-m-dTH:i'
|
||||
});
|
||||
window.rondDatetime.attachPicker(el);
|
||||
});
|
||||
}
|
||||
} catch (e) { console.error('flatpickr 初始化失败', e); }
|
||||
@@ -1177,10 +1170,11 @@
|
||||
var actSel = document.getElementById('activity-select');
|
||||
var actRename = document.getElementById('activity-rename');
|
||||
if (actSel && actRename) {
|
||||
actRename.addEventListener('click', function () {
|
||||
actRename.addEventListener('click', async function () {
|
||||
var opt = actSel.options[actSel.selectedIndex];
|
||||
if (!actSel.value || !opt) { alert('请先在下拉里选择要改名的活动'); return; }
|
||||
var name = window.prompt('新的活动名称', opt.textContent);
|
||||
// 不用 window.prompt:它会把「新的活动名称=xxx」拼进地址栏并留进历史记录
|
||||
var name = await window.rondPrompt.ask('新的活动名称', opt.textContent, { required: true });
|
||||
if (!name || !name.trim()) return;
|
||||
activityPost('/admin/edit/activity/update',
|
||||
'id=' + encodeURIComponent(actSel.value) + '&name=' + encodeURIComponent(name.trim()),
|
||||
|
||||
@@ -508,8 +508,7 @@
|
||||
if (!window.flatpickr) return;
|
||||
if (flatpickr.l10ns && flatpickr.l10ns.zh) flatpickr.localize(flatpickr.l10ns.zh);
|
||||
rowsBox.querySelectorAll('input[type=datetime-local]').forEach(function (el) {
|
||||
if (el._flatpickr) return;
|
||||
flatpickr(el, { enableTime: true, time_24hr: true, allowInput: true, minuteIncrement: 5, dateFormat: 'Y-m-dTH:i' });
|
||||
window.rondDatetime.attachPicker(el);
|
||||
});
|
||||
} catch (e) { console.error('flatpickr 初始化失败', e); }
|
||||
}
|
||||
|
||||
@@ -142,6 +142,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>统计脚本</h2>
|
||||
<div class="card-sub">第三方访问统计 / 追踪脚本。默认关闭,打开后只注入<b>公开页面</b>,后台与登录页不挂</div>
|
||||
<label class="check"><input type="checkbox" name="track_enabled" {{if .Settings.TrackEnabled}}checked{{end}}> 启用统计脚本</label>
|
||||
<label class="f" style="margin-top:10px">
|
||||
<span>脚本内容</span>
|
||||
<textarea name="track_script" rows="4"
|
||||
placeholder="https://example.com/analytics.js">{{.Settings.TrackScript}}</textarea>
|
||||
</label>
|
||||
<div class="muted" style="font-size:12.5px">
|
||||
<div>填一个 <span class="mono">https://…</span> 地址会包成 <span class="mono"><script src=…></span>;
|
||||
需要初始化代码的服务(Umami、百度统计等)可以把整段 <span class="mono"><script></span> 贴进来。</div>
|
||||
<div>整段 <span class="mono"><script></span> 需要闭合标签配对(多一个 <span class="mono"></script></span> 会被判为粘贴错误);
|
||||
<span class="mono">src</span> 只接受 <span class="mono">https://</span>、<span class="mono">http://</span> 或以 <span class="mono">/</span> 开头的地址,
|
||||
超过 4096 字符会被截断。填了但没勾开关不会生效。</div>
|
||||
<div><span class="mono">conf/app.ini</span> 的 <span class="mono">track_script</span> 会作为这里的默认值——
|
||||
它也需要开关打开才注入。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button class="btn primary" type="submit">保存设置</button>
|
||||
<a class="btn" href="/admin">取消</a>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
if (!window.flatpickr) return;
|
||||
if (flatpickr.l10ns && flatpickr.l10ns.zh) flatpickr.localize(flatpickr.l10ns.zh);
|
||||
document.querySelectorAll('input[type=datetime-local]').forEach(function (el) {
|
||||
flatpickr(el, { enableTime: true, time_24hr: true, allowInput: true, minuteIncrement: 5, dateFormat: 'Y-m-dTH:i' });
|
||||
window.rondDatetime.attachPicker(el);
|
||||
});
|
||||
} catch (e) { console.error('flatpickr 初始化失败', e); }
|
||||
});
|
||||
|
||||
@@ -225,7 +225,13 @@
|
||||
{{if .Flatpickr}}
|
||||
<script src="/static/vendor/flatpickr/flatpickr.min.js?v={{.AssetV}}"></script>
|
||||
<script src="/static/vendor/flatpickr/l10n/zh.js?v={{.AssetV}}"></script>
|
||||
{{/* 时间框的解析器与输入引导。必须在 flatpickr 之后、页面内联脚本之前:
|
||||
它不自己调 flatpickr,只提供 parseDate 与 attach,由各页 whenReady 调 attachPicker。 */}}
|
||||
<script src="/static/js/datetime-input.js?v={{.AssetV}}"></script>
|
||||
{{end}}
|
||||
{{/* 输入弹层:替代 window.prompt(它会把提示文本与默认值写进地址栏)。
|
||||
全站都可能用到(报告页复制链接、后台活动改名),所以无条件引入。 */}}
|
||||
<script src="/static/js/prompt-box.js?v={{.AssetV}}"></script>
|
||||
{{/* 第三方统计脚本:只在公开页面挂,后台/登录页不挂(免得把自己的操作也统计进去) */}}
|
||||
{{if and .TrackScript (ne .Nav "admin")}}
|
||||
{{.TrackScript}}
|
||||
|
||||
@@ -123,19 +123,22 @@
|
||||
(function () {
|
||||
var share = document.getElementById('btn-share');
|
||||
var pdf = document.getElementById('btn-pdf');
|
||||
if (share) share.addEventListener('click', function () {
|
||||
if (share) share.addEventListener('click', async function () {
|
||||
var url = location.href;
|
||||
if (navigator.share) {
|
||||
navigator.share({ title: document.title, url: url }).catch(function () {});
|
||||
return;
|
||||
}
|
||||
// 复制失败时不再用 window.prompt 兜底(它会把整条链接写进地址栏并留进历史),
|
||||
// 改成只读弹层:内容照样能选中 Ctrl+C,地址栏保持干净。
|
||||
var fallback = function () { return window.rondPrompt.ask('复制下面的链接:', url, { readonly: true }); };
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(function () {
|
||||
share.textContent = '已复制链接';
|
||||
setTimeout(function () { share.textContent = '复制链接'; }, 1600);
|
||||
}).catch(function () { window.prompt('复制下面的链接:', url); });
|
||||
}).catch(fallback);
|
||||
} else {
|
||||
window.prompt('复制下面的链接:', url);
|
||||
fallback();
|
||||
}
|
||||
});
|
||||
if (pdf) pdf.addEventListener('click', function () { window.print(); });
|
||||
|
||||
Reference in New Issue
Block a user