HEL-351: 修复夜间下拉/日期弹层、一次性初始密码领取与缩放滚动条

夜间控件改走现有 token 与 color-scheme,日期弹层锚定触发器并可翻转;
创建公司账号改为一次性可复制口令窗口,列表与后续接口不再回明文。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-01 21:58:56 +08:00
co-authored by Cursor multica-agent
parent 9d06445d81
commit acc3c29c49
13 changed files with 917 additions and 36 deletions
+268
View File
@@ -0,0 +1,268 @@
/* 锚定在日期输入旁的页内日历。不引入第三方库;滚动/缩放后跟随触发器。 */
(function (global) {
var WEEK = ["日", "一", "二", "三", "四", "五", "六"];
var panel = null;
var grid = null;
var titleEl = null;
var activeInput = null;
var viewYear = 0;
var viewMonth = 0;
var bound = false;
function pad(n) {
return n < 10 ? "0" + n : String(n);
}
function toISO(year, month, day) {
return year + "-" + pad(month + 1) + "-" + pad(day);
}
function parseISO(value) {
var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || "").trim());
if (!match) return null;
var year = Number(match[1]);
var month = Number(match[2]) - 1;
var day = Number(match[3]);
var date = new Date(year, month, day);
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
return null;
}
return date;
}
function todayISO() {
var now = new Date();
return toISO(now.getFullYear(), now.getMonth(), now.getDate());
}
function ensurePanel() {
if (panel) return;
panel = document.createElement("div");
panel.className = "ds-datepicker";
panel.id = "ds-datepicker";
panel.hidden = true;
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-label", "选择日期");
panel.innerHTML =
'<div class="ds-dp-head">' +
'<button type="button" class="ds-dp-nav" data-dp-nav="-1" aria-label="上一月"></button>' +
'<div class="ds-dp-title"></div>' +
'<button type="button" class="ds-dp-nav" data-dp-nav="1" aria-label="下一月"></button>' +
"</div>" +
'<div class="ds-dp-week">' + WEEK.map(function (d) { return "<span>" + d + "</span>"; }).join("") + "</div>" +
'<div class="ds-dp-grid" role="grid"></div>' +
'<div class="ds-dp-foot">' +
'<button type="button" data-dp-today>今天</button>' +
'<button type="button" data-dp-clear>清除</button>' +
"</div>";
document.body.appendChild(panel);
titleEl = panel.querySelector(".ds-dp-title");
grid = panel.querySelector(".ds-dp-grid");
panel.addEventListener("mousedown", function (event) {
event.preventDefault();
});
panel.addEventListener("click", function (event) {
var nav = event.target.closest("[data-dp-nav]");
if (nav) {
shiftMonth(Number(nav.getAttribute("data-dp-nav")));
return;
}
if (event.target.closest("[data-dp-today]")) {
commit(todayISO());
return;
}
if (event.target.closest("[data-dp-clear]")) {
commit("");
return;
}
var dayBtn = event.target.closest("[data-iso]");
if (dayBtn && !dayBtn.disabled) commit(dayBtn.getAttribute("data-iso"));
});
}
function enhance(input) {
if (!input || input.dataset.dsDate === "1") return;
input.dataset.dsDate = "1";
if (input.type === "date") {
var value = input.value;
try {
input.type = "text";
} catch (err) {
/* keep native type if the engine refuses */
}
if (value) input.value = value;
}
input.setAttribute("inputmode", "numeric");
input.setAttribute("autocomplete", "off");
input.setAttribute("spellcheck", "false");
input.setAttribute("placeholder", input.getAttribute("placeholder") || "YYYY-MM-DD");
if (!input.getAttribute("pattern")) input.setAttribute("pattern", "\\d{4}-\\d{2}-\\d{2}");
if (!input.classList.contains("num-input")) input.classList.add("num-input");
input.setAttribute("aria-haspopup", "dialog");
}
function scan() {
document.querySelectorAll('input[type="date"]').forEach(enhance);
}
function render() {
if (!grid || !titleEl) return;
titleEl.textContent = viewYear + "年" + pad(viewMonth + 1) + "月";
var first = new Date(viewYear, viewMonth, 1);
var start = first.getDay();
var selected = activeInput ? parseISO(activeInput.value) : null;
var selectedISO = selected ? toISO(selected.getFullYear(), selected.getMonth(), selected.getDate()) : "";
var today = todayISO();
var min = activeInput && activeInput.min ? activeInput.min : "";
var max = activeInput && activeInput.max ? activeInput.max : "";
var html = "";
var cursor = new Date(viewYear, viewMonth, 1 - start);
for (var i = 0; i < 42; i += 1) {
var iso = toISO(cursor.getFullYear(), cursor.getMonth(), cursor.getDate());
var other = cursor.getMonth() !== viewMonth;
var disabled = (min && iso < min) || (max && iso > max);
var cls = "ds-dp-day";
if (other) cls += " is-other";
if (iso === today) cls += " is-today";
if (iso === selectedISO) cls += " is-selected";
html +=
'<button type="button" class="' + cls + '" data-iso="' + iso + '"' +
(disabled ? " disabled" : "") +
' aria-label="' + iso + '"' +
(iso === selectedISO ? ' aria-current="date"' : "") +
">" + cursor.getDate() + "</button>";
cursor.setDate(cursor.getDate() + 1);
}
grid.innerHTML = html;
position();
}
function viewportBox() {
var vv = global.visualViewport;
if (vv) {
return {
left: vv.offsetLeft,
top: vv.offsetTop,
width: vv.width,
height: vv.height,
};
}
return { left: 0, top: 0, width: global.innerWidth, height: global.innerHeight };
}
function position() {
if (!panel || panel.hidden || !activeInput) return;
var rect = activeInput.getBoundingClientRect();
var view = viewportBox();
var gap = 6;
var width = panel.offsetWidth || 280;
var height = panel.offsetHeight || 320;
var left = rect.left;
var top = rect.bottom + gap;
if (top + height > view.top + view.height - 8 && rect.top - gap - height >= view.top + 8) {
top = rect.top - gap - height;
}
if (left + width > view.left + view.width - 8) {
left = Math.max(view.left + 8, rect.right - width);
}
if (left < view.left + 8) left = view.left + 8;
if (top < view.top + 8) top = view.top + 8;
panel.style.left = Math.round(left) + "px";
panel.style.top = Math.round(top) + "px";
}
function openPicker(input) {
if (!input || input.disabled) return;
enhance(input);
ensurePanel();
activeInput = input;
var parsed = parseISO(input.value) || new Date();
viewYear = parsed.getFullYear();
viewMonth = parsed.getMonth();
panel.hidden = false;
render();
input.setAttribute("aria-expanded", "true");
}
function closePicker() {
if (!panel || panel.hidden) return;
panel.hidden = true;
if (activeInput) activeInput.setAttribute("aria-expanded", "false");
activeInput = null;
}
function shiftMonth(delta) {
viewMonth += delta;
while (viewMonth < 0) {
viewMonth += 12;
viewYear -= 1;
}
while (viewMonth > 11) {
viewMonth -= 12;
viewYear += 1;
}
render();
}
function commit(iso) {
if (!activeInput) return;
var input = activeInput;
input.value = iso;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
closePicker();
input.focus({ preventScroll: true });
}
function onPointer(event) {
var input = event.target.closest && event.target.closest('input[type="date"], input[data-ds-date="1"]');
if (input) {
if (event.type === "mousedown" && event.button === 0) event.preventDefault();
openPicker(input);
return;
}
if (panel && !panel.hidden && !panel.contains(event.target)) closePicker();
}
function onKey(event) {
if (event.key === "Escape") {
closePicker();
return;
}
var input = event.target.closest && event.target.closest('input[data-ds-date="1"]');
if (!input) return;
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
event.preventDefault();
openPicker(input);
}
}
function bind() {
if (bound) return;
bound = true;
document.addEventListener("mousedown", onPointer, true);
document.addEventListener("focusin", function (event) {
var input = event.target.closest && event.target.closest('input[type="date"]');
if (input) openPicker(input);
});
document.addEventListener("keydown", onKey);
document.addEventListener("scroll", position, true);
global.addEventListener("resize", position);
if (global.visualViewport) {
global.visualViewport.addEventListener("resize", position);
global.visualViewport.addEventListener("scroll", position);
}
if (typeof MutationObserver === "function") {
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
}
scan();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind);
} else {
bind();
}
global.JinniuDatePicker = { open: openPicker, close: closePicker, scan: scan };
})(window);