diff --git a/mis-attendance-fill.user.js b/mis-attendance-fill.user.js
index 3553f48..fcf6120 100644
--- a/mis-attendance-fill.user.js
+++ b/mis-attendance-fill.user.js
@@ -4,28 +4,60 @@
// @version 0.4.2
// @description 固定考勤表头,三列独立开关控制向后填充,出勤跳过节假日,自动保存
// @match https://mis.murongtech.com/mrmis/toMenu.do*
-// @updateURL none
+// @updateURL https://git.alawn.cn/Alawn/script/raw/branch/main/mis-attendance-fill.user.js
+// @downloadURL https://git.alawn.cn/Alawn/script/raw/branch/main/mis-attendance-fill.user.js
// @run-at document-idle
// @sandbox JavaScript
// @grant unsafeWindow
// ==/UserScript==
(() => {
- 'use strict';
- if (new URLSearchParams(location.search).get('menu_id') !== '332005') return;
- const page = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
- const projectFields = ['pro_id', 'pro_nm', 'pro_state', 'fin_pro_id', 'fin_pro_nm',
- 'assistant_id', 'assistant_nm', 'att_man', 'att_man_nm', 'cen_man', 'pro_man', 'pro_ass'];
- const saveFields = ['att_no', 'att_dt', 'att_tm', 'hld_flg', 'travel_flg', 'att_typ', ...projectFields, 'rmk'];
- const key = row => [row.att_dt, row.att_tm, row.id_num].join('|');
- const date = row => String(row.att_dt ?? '').replace(/\D/g, '').slice(0, 8);
- const later = (row, source) => date(row).slice(0, 6) === date(source).slice(0, 6)
- && (date(row) > date(source) || (date(row) === date(source) && Number(row.id_num) > Number(source.id_num)));
+ "use strict";
+ if (new URLSearchParams(location.search).get("menu_id") !== "332005") return;
+ const page = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
+ const projectFields = [
+ "pro_id",
+ "pro_nm",
+ "pro_state",
+ "fin_pro_id",
+ "fin_pro_nm",
+ "assistant_id",
+ "assistant_nm",
+ "att_man",
+ "att_man_nm",
+ "cen_man",
+ "pro_man",
+ "pro_ass",
+ ];
+ const saveFields = [
+ "att_no",
+ "att_dt",
+ "att_tm",
+ "hld_flg",
+ "travel_flg",
+ "att_typ",
+ ...projectFields,
+ "rmk",
+ ];
+ const key = (row) => [row.att_dt, row.att_tm, row.id_num].join("|");
+ const date = (row) =>
+ String(row.att_dt ?? "")
+ .replace(/\D/g, "")
+ .slice(0, 8);
+ const later = (row, source) =>
+ date(row).slice(0, 6) === date(source).slice(0, 6) &&
+ (date(row) > date(source) ||
+ (date(row) === date(source) &&
+ Number(row.id_num) > Number(source.id_num)));
// Keep this UI in the userscript so colleagues can install a single file.
function installHeader(table, enabled) {
- const labels = { travel_flg: '是否出差', att_typ: '出勤情况', pro_id: '所在项目' };
- const style = document.createElement('style');
+ const labels = {
+ travel_flg: "是否出差",
+ att_typ: "出勤情况",
+ pro_id: "所在项目",
+ };
+ const style = document.createElement("style");
style.textContent = `
#mis-fixed-header th { background: #fff; }
.mis-fill-heading { white-space: nowrap; }
@@ -53,150 +85,234 @@
@media print { #mis-fixed-header, .mis-fill-switch { display: none !important; } }
`;
document.head.append(style);
- const floating = document.createElement('div');
- floating.id = 'mis-fixed-header';
- floating.className = 'fixed-table-container';
+ const floating = document.createElement("div");
+ floating.id = "mis-fixed-header";
+ floating.className = "fixed-table-container";
floating.hidden = true;
document.body.append(floating);
- let frame = 0, dirty = true, sourceHead, copyHead;
- const switches = () => document.querySelectorAll('.mis-fill-switch');
- const paint = () => switches().forEach(button => {
- const field = button.dataset.misFill;
- button.setAttribute('aria-checked', String(enabled[field]));
- button.setAttribute('aria-label', `${labels[field]}自动向后填充`);
- button.title = `${labels[field]}:自动向后填充已${enabled[field] ? '开启' : '关闭'},点击${enabled[field] ? '关闭' : '开启'}`;
- button.querySelector('.mis-fill-label').textContent = '向后填充';
- });
+ let frame = 0,
+ dirty = true,
+ sourceHead,
+ copyHead;
+ const switches = () => document.querySelectorAll(".mis-fill-switch");
+ const paint = () =>
+ switches().forEach((button) => {
+ const field = button.dataset.misFill;
+ button.setAttribute("aria-checked", String(enabled[field]));
+ button.setAttribute("aria-label", `${labels[field]}自动向后填充`);
+ button.title = `${labels[field]}:自动向后填充已${enabled[field] ? "开启" : "关闭"},点击${enabled[field] ? "关闭" : "开启"}`;
+ button.querySelector(".mis-fill-label").textContent = "向后填充";
+ });
function decorate(head) {
Object.entries(labels).forEach(([field, label]) => {
- const th = [...head.querySelectorAll('th')].find(cell =>
- cell.dataset.field === field || cell.textContent.trim() === label);
- if (!th || th.querySelector('.mis-fill-switch')) return;
- const title = th.querySelector('.th-inner') || th;
- title.classList.add('mis-fill-heading');
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'mis-fill-switch';
+ const th = [...head.querySelectorAll("th")].find(
+ (cell) =>
+ cell.dataset.field === field || cell.textContent.trim() === label,
+ );
+ if (!th || th.querySelector(".mis-fill-switch")) return;
+ const title = th.querySelector(".th-inner") || th;
+ title.classList.add("mis-fill-heading");
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "mis-fill-switch";
button.dataset.misFill = field;
- button.setAttribute('role', 'switch');
- button.innerHTML = '';
+ button.setAttribute("role", "switch");
+ button.innerHTML =
+ '';
title.append(button);
});
paint();
}
function layout() {
frame = 0;
- const element = document.querySelector('#murong-table');
+ const element = document.querySelector("#murong-table");
const head = element.tHead;
- if (!head) { floating.hidden = true; return; }
+ if (!head) {
+ floating.hidden = true;
+ return;
+ }
if (dirty || head !== sourceHead) {
decorate(head);
sourceHead = head;
const copy = element.cloneNode(false);
- copy.removeAttribute('id');
+ copy.removeAttribute("id");
copyHead = head.cloneNode(true);
- copyHead.querySelectorAll('[id]').forEach(node => node.removeAttribute('id'));
+ copyHead
+ .querySelectorAll("[id]")
+ .forEach((node) => node.removeAttribute("id"));
copy.append(copyHead);
floating.replaceChildren(copy);
dirty = false;
}
- const rect = head.getBoundingClientRect(), bounds = element.getBoundingClientRect();
- let top = 0, left = Math.max(0, rect.left), right = Math.min(innerWidth, rect.right), bottom = innerHeight;
- for (let parent = element.parentElement; parent && parent !== document.body; parent = parent.parentElement) {
- const css = getComputedStyle(parent), box = parent.getBoundingClientRect();
+ const rect = head.getBoundingClientRect(),
+ bounds = element.getBoundingClientRect();
+ let top = 0,
+ left = Math.max(0, rect.left),
+ right = Math.min(innerWidth, rect.right),
+ bottom = innerHeight;
+ for (
+ let parent = element.parentElement;
+ parent && parent !== document.body;
+ parent = parent.parentElement
+ ) {
+ const css = getComputedStyle(parent),
+ box = parent.getBoundingClientRect();
if (/(auto|scroll|hidden|clip)/.test(css.overflowY)) {
top = Math.max(top, box.top + parent.clientTop);
- bottom = Math.min(bottom, box.top + parent.clientTop + parent.clientHeight);
+ bottom = Math.min(
+ bottom,
+ box.top + parent.clientTop + parent.clientHeight,
+ );
}
if (/(auto|scroll|hidden|clip)/.test(css.overflowX)) {
left = Math.max(left, box.left + parent.clientLeft);
- right = Math.min(right, box.left + parent.clientLeft + parent.clientWidth);
+ right = Math.min(
+ right,
+ box.left + parent.clientLeft + parent.clientWidth,
+ );
}
}
- document.querySelectorAll('.navbar-fixed-top').forEach(nav => {
+ document.querySelectorAll(".navbar-fixed-top").forEach((nav) => {
const box = nav.getBoundingClientRect();
if (box.top <= 0 && box.bottom > 0) top = Math.max(top, box.bottom);
});
- floating.hidden = !(rect.width && rect.top < top && bounds.bottom > top + rect.height && bottom > top + rect.height && right > left);
+ floating.hidden = !(
+ rect.width &&
+ rect.top < top &&
+ bounds.bottom > top + rect.height &&
+ bottom > top + rect.height &&
+ right > left
+ );
if (floating.hidden) return;
- Object.assign(floating.style, { top: `${top}px`, left: `${left}px`, width: `${right - left}px` });
+ Object.assign(floating.style, {
+ top: `${top}px`,
+ left: `${left}px`,
+ width: `${right - left}px`,
+ });
const copyTable = copyHead.parentElement;
- Object.assign(copyTable.style, { width: `${rect.width}px`, transform: `translateX(${rect.left - left}px)` });
- const cells = head.querySelectorAll('th');
- copyHead.querySelectorAll('th').forEach((cell, index) => {
+ Object.assign(copyTable.style, {
+ width: `${rect.width}px`,
+ transform: `translateX(${rect.left - left}px)`,
+ });
+ const cells = head.querySelectorAll("th");
+ copyHead.querySelectorAll("th").forEach((cell, index) => {
cell.style.width = `${cells[index].getBoundingClientRect().width}px`;
cell.style.height = `${cells[index].getBoundingClientRect().height}px`;
- const input = cell.querySelector('input');
- const original = cells[index].querySelector('input');
- if (input && original) { input.checked = original.checked; input.indeterminate = original.indeterminate; }
+ const input = cell.querySelector("input");
+ const original = cells[index].querySelector("input");
+ if (input && original) {
+ input.checked = original.checked;
+ input.indeterminate = original.indeterminate;
+ }
});
}
- const schedule = () => { if (!frame) frame = requestAnimationFrame(layout); };
- document.addEventListener('click', event => {
- const button = event.target.closest('.mis-fill-switch');
- if (button) {
- event.preventDefault(); event.stopImmediatePropagation();
- const field = button.dataset.misFill;
- enabled[field] = !enabled[field];
- paint();
- return;
- }
- // Retain the original header's sorting and select-all interactions.
- if (!floating.contains(event.target) || !copyHead?.contains(event.target)) return;
- event.preventDefault(); event.stopImmediatePropagation();
- const path = [];
- for (let node = event.target; node !== copyHead; node = node.parentElement) {
- path.unshift([...node.parentElement.children].indexOf(node));
- }
- let original = sourceHead;
- path.forEach(index => { original = original.children[index]; });
- original?.click();
- dirty = true;
- schedule();
- }, true);
- document.addEventListener('scroll', schedule, { capture: true, passive: true });
- window.addEventListener('resize', schedule, { passive: true });
- table().on('post-header.bs.table post-body.bs.table reset-view.bs.table', () => { dirty = true; schedule(); });
- new ResizeObserver(schedule).observe(document.querySelector('#murong-table'));
+ const schedule = () => {
+ if (!frame) frame = requestAnimationFrame(layout);
+ };
+ document.addEventListener(
+ "click",
+ (event) => {
+ const button = event.target.closest(".mis-fill-switch");
+ if (button) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ const field = button.dataset.misFill;
+ enabled[field] = !enabled[field];
+ paint();
+ return;
+ }
+ // Retain the original header's sorting and select-all interactions.
+ if (
+ !floating.contains(event.target) ||
+ !copyHead?.contains(event.target)
+ )
+ return;
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ const path = [];
+ for (
+ let node = event.target;
+ node !== copyHead;
+ node = node.parentElement
+ ) {
+ path.unshift([...node.parentElement.children].indexOf(node));
+ }
+ let original = sourceHead;
+ path.forEach((index) => {
+ original = original.children[index];
+ });
+ original?.click();
+ dirty = true;
+ schedule();
+ },
+ true,
+ );
+ document.addEventListener("scroll", schedule, {
+ capture: true,
+ passive: true,
+ });
+ window.addEventListener("resize", schedule, { passive: true });
+ table().on(
+ "post-header.bs.table post-body.bs.table reset-view.bs.table",
+ () => {
+ dirty = true;
+ schedule();
+ },
+ );
+ new ResizeObserver(schedule).observe(
+ document.querySelector("#murong-table"),
+ );
layout();
}
function initialize($) {
- const table = () => $('#murong-table');
- const rows = () => table().bootstrapTable('getData') || [];
+ const table = () => $("#murong-table");
+ const rows = () => table().bootstrapTable("getData") || [];
const projects = new Map();
const pending = new Set();
const enabled = { travel_flg: true, att_typ: true, pro_id: true };
let saving = false;
let generation = 0;
- const status = document.createElement('span');
- status.id = 'mis-brush-status';
- status.style.cssText = 'display:none;margin-left:12px;color:#7c8795;font-size:12px;line-height:20px;vertical-align:middle;';
- status.setAttribute('role', 'status');
- status.title = '考勤向后填充 v0.4.2';
- document.querySelector('#addBtn').insertAdjacentElement('afterend', status);
+ const status = document.createElement("span");
+ status.id = "mis-brush-status";
+ status.style.cssText =
+ "display:none;margin-left:12px;color:#7c8795;font-size:12px;line-height:20px;vertical-align:middle;";
+ status.setAttribute("role", "status");
+ status.title = "考勤向后填充 v0.4.2";
+ document.querySelector("#addBtn").insertAdjacentElement("afterend", status);
const show = (message, failed = false) => {
status.textContent = message;
- status.style.display = 'inline-block';
- status.style.color = failed ? '#b84b48' : '#7c8795';
+ status.style.display = "inline-block";
+ status.style.color = failed ? "#b84b48" : "#7c8795";
};
installHeader(table, enabled);
- const remember = row => projects.set(key(row), JSON.stringify(projectFields.map(field => row[field] ?? '')));
+ const remember = (row) =>
+ projects.set(
+ key(row),
+ JSON.stringify(projectFields.map((field) => row[field] ?? "")),
+ );
const fullData = () => {
const data = rows();
- const total = Number(table().bootstrapTable('getOptions').totalRows);
- if (total > data.length) throw new Error('记录未全部加载,稍后再选择一次。');
+ const total = Number(table().bootstrapTable("getOptions").totalRows);
+ if (total > data.length)
+ throw new Error("记录未全部加载,稍后再选择一次。");
return data;
};
// Capture before MIS handles the edit, including nested scroll containers.
function rememberScroll() {
const elements = new Set([document.scrollingElement]);
- for (let element = document.querySelector('#murong-table'); element; element = element.parentElement) {
+ for (
+ let element = document.querySelector("#murong-table");
+ element;
+ element = element.parentElement
+ ) {
elements.add(element);
}
- const positions = [...elements].filter(Boolean).map(element => ({
- element, top: element.scrollTop, left: element.scrollLeft
+ const positions = [...elements].filter(Boolean).map((element) => ({
+ element,
+ top: element.scrollTop,
+ left: element.scrollLeft,
}));
const version = generation;
return () => {
@@ -211,29 +327,42 @@
async function save() {
if (saving) return;
saving = true;
- show('保存中…');
+ show("保存中…");
const version = generation;
let count = 0;
try {
while (pending.size && version === generation) {
const id = [...pending][0];
pending.delete(id);
- const row = rows().find(item => key(item) === id);
- if (!row || String(row.chk_sts) !== '00') continue;
+ const row = rows().find((item) => key(item) === id);
+ if (!row || String(row.chk_sts) !== "00") continue;
// MIS requires a project. A later project selection queues these rows again.
- if (!String(row.pro_id ?? '').trim()) continue;
- const params = { rec_num: 1, flg: '01' };
- saveFields.forEach(field => { params[`rec_1.${field}`] = row[field] ?? ''; });
- const result = await new Promise((resolve, reject) => $.ajax({
- type: 'POST', url: '/mrmis/attAdd.do', data: params, dataType: 'json', timeout: 20000,
- success: resolve, error: (_xhr, reason) => reject(new Error(reason === 'timeout' ? '保存超时' : '保存失败'))
- }));
+ if (!String(row.pro_id ?? "").trim()) continue;
+ const params = { rec_num: 1, flg: "01" };
+ saveFields.forEach((field) => {
+ params[`rec_1.${field}`] = row[field] ?? "";
+ });
+ const result = await new Promise((resolve, reject) =>
+ $.ajax({
+ type: "POST",
+ url: "/mrmis/attAdd.do",
+ data: params,
+ dataType: "json",
+ timeout: 20000,
+ success: resolve,
+ error: (_xhr, reason) =>
+ reject(
+ new Error(reason === "timeout" ? "保存超时" : "保存失败"),
+ ),
+ }),
+ );
if (version !== generation) break;
- if (String(result?.gda?.msg_cd ?? '').slice(3) !== '00000') throw new Error(result?.gda?.msg_inf || '保存失败');
+ if (String(result?.gda?.msg_cd ?? "").slice(3) !== "00000")
+ throw new Error(result?.gda?.msg_inf || "保存失败");
count++;
}
if (version === generation) {
- show(count ? `已保存 ${count} 条` : '待选择项目');
+ show(count ? `已保存 ${count} 条` : "待选择项目");
}
} catch (error) {
if (version === generation) {
@@ -249,13 +378,15 @@
function brush(source, fields, restoreScroll) {
const autoFill = enabled[fields[0]];
const data = autoFill ? fullData() : [];
- if (String(source.chk_sts) !== '00') return;
- const patch = Object.fromEntries(fields.map(field => [field, source[field] ?? '']));
- const skipHolidays = fields.includes('att_typ');
+ if (String(source.chk_sts) !== "00") return;
+ const patch = Object.fromEntries(
+ fields.map((field) => [field, source[field] ?? ""]),
+ );
+ const skipHolidays = fields.includes("att_typ");
data.forEach((row, index) => {
- if (String(row.chk_sts) !== '00' || !later(row, source)) return;
- if (skipHolidays && String(row.hld_flg) === '1') return;
- table().bootstrapTable('updateRow', { index, row: { ...patch } });
+ if (String(row.chk_sts) !== "00" || !later(row, source)) return;
+ if (skipHolidays && String(row.hld_flg) === "1") return;
+ table().bootstrapTable("updateRow", { index, row: { ...patch } });
restoreScroll();
remember(row);
pending.add(key(row));
@@ -266,67 +397,104 @@
}
// Capture before MIS rerenders the edited row; propagate after its handler finishes.
- document.addEventListener('change', event => {
- const input = event.target;
- const field = { TRAVEL_FLG: 'travel_flg', ATT_TYP: 'att_typ' }[input.name];
- const tr = input.closest('#murong-table tbody tr[data-index]');
- if (!field || !tr || input.disabled) return;
- const index = Number(tr.dataset.index);
- const source = rows()[index];
- if (!source) return;
- const restoreScroll = rememberScroll();
- const id = key(source), value = input.value, version = generation;
- setTimeout(() => {
- if (version !== generation) return;
- const current = rows().find(row => key(row) === id);
- if (!current) return;
- current[field] = value;
- try { brush(current, [field], restoreScroll); } catch (error) { show(error.message, true); }
- finally { restoreScroll(); requestAnimationFrame(restoreScroll); }
- }, 0);
- }, true);
+ document.addEventListener(
+ "change",
+ (event) => {
+ const input = event.target;
+ const field = { TRAVEL_FLG: "travel_flg", ATT_TYP: "att_typ" }[
+ input.name
+ ];
+ const tr = input.closest("#murong-table tbody tr[data-index]");
+ if (!field || !tr || input.disabled) return;
+ const index = Number(tr.dataset.index);
+ const source = rows()[index];
+ if (!source) return;
+ const restoreScroll = rememberScroll();
+ const id = key(source),
+ value = input.value,
+ version = generation;
+ setTimeout(() => {
+ if (version !== generation) return;
+ const current = rows().find((row) => key(row) === id);
+ if (!current) return;
+ current[field] = value;
+ try {
+ brush(current, [field], restoreScroll);
+ } catch (error) {
+ show(error.message, true);
+ } finally {
+ restoreScroll();
+ requestAnimationFrame(restoreScroll);
+ }
+ }, 0);
+ },
+ true,
+ );
// MIS saves immediately after its project picker resolves. Copy the complete metadata then.
- document.addEventListener('click', event => {
- const button = event.target.closest('buttom[name="save"]');
- const tr = button?.closest('#murong-table tbody tr[data-index]');
- if (!tr) return;
- const source = rows()[Number(tr.dataset.index)];
- if (!source?.pro_id) return;
- const signature = JSON.stringify(projectFields.map(field => source[field] ?? ''));
- if (projects.get(key(source)) === signature) return;
- const version = generation;
- const restoreScroll = rememberScroll();
- setTimeout(() => {
- if (version !== generation) return;
- try { brush(source, projectFields, restoreScroll); } catch (error) { show(error.message, true); }
- finally { restoreScroll(); requestAnimationFrame(restoreScroll); }
- }, 0);
- }, true);
+ document.addEventListener(
+ "click",
+ (event) => {
+ const button = event.target.closest('buttom[name="save"]');
+ const tr = button?.closest("#murong-table tbody tr[data-index]");
+ if (!tr) return;
+ const source = rows()[Number(tr.dataset.index)];
+ if (!source?.pro_id) return;
+ const signature = JSON.stringify(
+ projectFields.map((field) => source[field] ?? ""),
+ );
+ if (projects.get(key(source)) === signature) return;
+ const version = generation;
+ const restoreScroll = rememberScroll();
+ setTimeout(() => {
+ if (version !== generation) return;
+ try {
+ brush(source, projectFields, restoreScroll);
+ } catch (error) {
+ show(error.message, true);
+ } finally {
+ restoreScroll();
+ requestAnimationFrame(restoreScroll);
+ }
+ }, 0);
+ },
+ true,
+ );
function loaded() {
generation++;
- pending.clear(); projects.clear();
+ pending.clear();
+ projects.clear();
rows().forEach(remember);
- const options = table().bootstrapTable('getOptions');
+ const options = table().bootstrapTable("getOptions");
if (Number(options.totalRows) > rows().length) {
- table().bootstrapTable('refreshOptions', { pageSize: Math.max(100, Number(options.totalRows)), pageNumber: 1 });
+ table().bootstrapTable("refreshOptions", {
+ pageSize: Math.max(100, Number(options.totalRows)),
+ pageNumber: 1,
+ });
}
- status.textContent = '';
- status.style.display = 'none';
+ status.textContent = "";
+ status.style.display = "none";
}
- table().on('load-success.bs.table', loaded);
+ table().on("load-success.bs.table", loaded);
if (rows().length) loaded();
}
let attempts = 0;
function start() {
const $ = page.jQuery;
- if (!$?.fn?.bootstrapTable || !document.querySelector('#addBtn') || !$('#murong-table').data('bootstrap.table')) return false;
+ if (
+ !$?.fn?.bootstrapTable ||
+ !document.querySelector("#addBtn") ||
+ !$("#murong-table").data("bootstrap.table")
+ )
+ return false;
initialize($);
return true;
}
if (!start()) {
- const timer = setInterval(() => { if (start() || ++attempts >= 60) clearInterval(timer); }, 500);
+ const timer = setInterval(() => {
+ if (start() || ++attempts >= 60) clearInterval(timer);
+ }, 500);
}
})();