// ==UserScript== // @name 小工单-工单快捷创建 // @namespace https://xiaogongdan.local/userscripts // @version 1.0.3 // @description 快捷创建工单,支持按产品规格和材质批量生成 // @match https://liteweb.blacklake.cn/* // @match https://liteweb-dingtalk.blacklake.cn/* // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @connect * // ==/UserScript== (function () { "use strict"; // ============================================================ // SHARED AUTH STORE (跨插件共享) // ============================================================ const SHARED_AUTH_STORE = { baseUrl: "xgd_shared_page_auth_base_url", token: "xgd_shared_page_auth_token", userLabel: "xgd_shared_page_auth_user_label", loginAt: "xgd_shared_page_auth_login_at", source: "xgd_shared_page_auth_source" }; // ============================================================ // PLUGIN STORE KEYS // ============================================================ const STORE = { baseUrl: "xgd-quick-wo_base_url", token: "xgd-quick-wo_token", userLabel: "xgd-quick-wo_user_label", loginAt: "xgd-quick-wo_login_at", logs: "xgd_plugin_logs", launcherPosition: "xgd_quick_wo_pos", tokenSource: "xgd-quick-wo_token_source", workTimeConfig: "xgd-quick-wo_work_time_config" }; const DEFAULT_WORK_TIME = { morning: { start: "07:00", end: "11:00" }, afternoon: { start: "11:30", end: "17:00" }, evening: { start: "", end: "" } }; // ============================================================ // API ENDPOINTS // ============================================================ const API = { projectList: "/api/dytin/external/project/queryList3", projectCreate: "/api/dytin/external/project/create", projectUpdate: "/api/dytin/external/project/update", productQuery: "/api/dytin/external/product/queryList2", customFieldMeta: "/api/dytin/external/customField/queryCustomFieldMetadata" }; // ============================================================ // PLUGIN METADATA // ============================================================ const SUCCESS_CODE = "01000000"; const GREEN = "#24af76"; const DEFAULT_BASE_URL = "https://liteweb.blacklake.cn"; const WORK_ORDER_ROUTE = "/productionManagement/workOrder"; const ROOT_ID = "xgd-quick-wo-root"; const PLUGIN_META = { id: "xgd-quick-wo", title: "工单快捷创建", route: WORK_ORDER_ROUTE }; let pluginOpenBound = false; // ============================================================ // STATUS TEXTS // ============================================================ const statusText = { 0: "新建", 10: "执行中", 20: "已结束", 30: "已取消" }; // ============================================================ // ERROR HINTS // ============================================================ const errorHints = { "1005": "请填写计划开始/结束时间", "1007": "计划开始时间不能大于计划结束时间", "1010": "产品编号不存在", "1011": "计划数不能小于0" }; // ============================================================ // BUSINESS STATE // ============================================================ const state = { loading: false, workOrders: [], pageNum: 1, pageSize: 20, total: 0, currentPage: "list", detailRows: [{ spec: "", material: "", plannedNum: "" }], submitting: false, materialOptions: [], specProducts: [] }; const html = String.raw; hookPageTokenCapture(); // ============================================================ // AUTH LAYER (VERBATIM) // ============================================================ function getConfig() { const savedBaseUrl = GM_getValue(STORE.baseUrl, ""); return { baseUrl: (savedBaseUrl || defaultBaseUrl()).replace(/\/$/, ""), token: GM_getValue(STORE.token, "") }; } function defaultBaseUrl() { return location.hostname.includes("dingtalk") ? "https://liteweb-dingtalk.blacklake.cn" : DEFAULT_BASE_URL; } function hookPageTokenCapture() { const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; if (!pageWindow) return; pageWindow.__xgdPageAuthListeners = pageWindow.__xgdPageAuthListeners || []; if (!pageWindow.__xgdPageAuthListeners.some((listener) => listener && listener.__pluginId === PLUGIN_META.id)) { const listener = function (auth) { saveDetectedPageAuth(auth); }; listener.__pluginId = PLUGIN_META.id; pageWindow.__xgdPageAuthListeners.push(listener); } if (pageWindow.__xgdPageTokenHooked) return; pageWindow.__xgdPageTokenHooked = true; const publishPageAuth = function (auth) { try { pageWindow.__xgdSharedPageAuth = Object.assign({}, auth, { time: Date.now() }); } catch (error) {} try { (pageWindow.__xgdPageAuthListeners || []).forEach((listener) => { try { listener(auth); } catch (error) {} }); } catch (error) {} }; try { const rawFetch = pageWindow.fetch; if (typeof rawFetch === "function") { pageWindow.fetch = function (input, init) { try { const headers = new Headers((init && init.headers) || (input && input.headers) || {}); const token = headers.get("X-AUTH") || headers.get("X-Auth") || headers.get("x-auth"); if (token) publishPageAuth({ token, userLabel: readCurrentPageUserLabel() || "当前网页登录态", source: "fetch-header" }); } catch (error) {} return rawFetch.apply(this, arguments); }; } } catch (error) {} try { const rawSetRequestHeader = pageWindow.XMLHttpRequest && pageWindow.XMLHttpRequest.prototype.setRequestHeader; if (typeof rawSetRequestHeader === "function") { pageWindow.XMLHttpRequest.prototype.setRequestHeader = function (name, value) { try { if (/^x-auth$/i.test(name || "") && value) { publishPageAuth({ token: value, userLabel: readCurrentPageUserLabel() || "当前网页登录态", source: "xhr-header" }); } } catch (error) {} return rawSetRequestHeader.apply(this, arguments); }; } } catch (error) {} } function normalizeAuthToken(value) { return String(value || "").trim().replace(/^Bearer\s+/i, "").replace(/^["']|["']$/g, ""); } function isValidAuthToken(value) { const token = normalizeAuthToken(value); return /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token); } function readCapturedHeaderAuth() { const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const pageShared = pageWindow && pageWindow.__xgdSharedPageAuth; if (pageShared && /header$/i.test(pageShared.source || "") && isValidAuthToken(pageShared.token)) { return { token: normalizeAuthToken(pageShared.token), userLabel: pageShared.userLabel || "当前网页登录态", source: pageShared.source }; } const sharedToken = normalizeAuthToken(GM_getValue(SHARED_AUTH_STORE.token, "")); const sharedSource = GM_getValue(SHARED_AUTH_STORE.source, ""); if (/header$/i.test(sharedSource || "") && isValidAuthToken(sharedToken)) { return { token: sharedToken, userLabel: GM_getValue(SHARED_AUTH_STORE.userLabel, "") || "当前网页登录态", source: sharedSource }; } const token = normalizeAuthToken(GM_getValue(STORE.token, "")); const source = GM_getValue(STORE.tokenSource, ""); if (!/header$/i.test(source || "") || !isValidAuthToken(token)) return null; return { token, userLabel: GM_getValue(STORE.userLabel, "") || "当前网页登录态", source }; } function readCurrentPageAuth() { hookPageTokenCapture(); const capturedHeaderAuth = readCapturedHeaderAuth(); if (capturedHeaderAuth) return capturedHeaderAuth; const candidates = []; const pushCandidate = (key, value) => { const token = normalizeAuthToken(value); if (isValidAuthToken(token)) candidates.push({ key, value: token }); }; try { [window.localStorage, window.sessionStorage].filter(Boolean).forEach((storage) => { for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index); const value = storage.getItem(key) || ""; if (/token|auth/i.test(key || "")) pushCandidate(key, value); try { const parsed = JSON.parse(value); Object.keys(parsed || {}).forEach((field) => { const fieldValue = parsed[field]; if (/token|auth/i.test(field) && typeof fieldValue === "string") { pushCandidate(key + "." + field, fieldValue); } }); } catch (error) {} } }); } catch (error) {} const tokenCandidate = candidates[0]; if (!tokenCandidate) return null; return { token: tokenCandidate.value, userLabel: readCurrentPageUserLabel() || "当前网页登录态", source: tokenCandidate.key }; } function readCurrentPageUserLabel() { try { const nodes = Array.prototype.slice.call(document.querySelectorAll('[class*="user"], [class*="account"], [class*="name"], [class*="avatar"]')); const text = nodes.map((node) => (node.textContent || "").trim()).find((value) => value && value.length <= 20); return text || ""; } catch (error) { return ""; } } function saveDetectedPageAuth(auth) { if (!auth || !isValidAuthToken(auth.token)) return; const token = normalizeAuthToken(auth.token); const userLabel = auth.userLabel || "当前网页登录态"; const source = auth.source || "page"; GM_setValue(STORE.baseUrl, defaultBaseUrl()); GM_setValue(STORE.token, token); GM_setValue(STORE.userLabel, userLabel); GM_setValue(STORE.loginAt, Date.now()); GM_setValue(STORE.tokenSource, source); GM_setValue(SHARED_AUTH_STORE.baseUrl, defaultBaseUrl()); GM_setValue(SHARED_AUTH_STORE.token, token); GM_setValue(SHARED_AUTH_STORE.userLabel, userLabel); GM_setValue(SHARED_AUTH_STORE.loginAt, Date.now()); GM_setValue(SHARED_AUTH_STORE.source, source); try { const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; if (pageWindow) pageWindow.__xgdSharedPageAuth = { token, userLabel, source, time: Date.now() }; } catch (error) {} } function syncCurrentPageAuth() { const pageAuth = readCurrentPageAuth(); if (pageAuth && pageAuth.token) saveDetectedPageAuth(pageAuth); return pageAuth; } function hasUsableAuth() { const pageAuth = readCurrentPageAuth(); const cachedToken = normalizeAuthToken(GM_getValue(STORE.token, "")); if (cachedToken && !isValidAuthToken(cachedToken)) GM_setValue(STORE.token, ""); return Boolean((pageAuth && pageAuth.token) || isValidAuthToken(cachedToken)); } async function ensureAuthToken() { const pageAuth = readCurrentPageAuth(); if (pageAuth && pageAuth.token) { saveDetectedPageAuth(pageAuth); return pageAuth.token; } const cachedToken = normalizeAuthToken(GM_getValue(STORE.token, "")); if (isValidAuthToken(cachedToken)) return cachedToken; if (cachedToken) GM_setValue(STORE.token, ""); throw new Error("未识别到当前页面登录态,请确认小工单网页已登录。"); } function clearAuthToken() { GM_setValue(STORE.token, ""); GM_setValue(STORE.loginAt, ""); refreshAuthView(); } // ============================================================ // API LAYER (VERBATIM) // ============================================================ function gmPost(url, body, token) { return new Promise(function (resolve, reject) { const headers = { "Content-Type": "application/json" }; if (token) headers["X-AUTH"] = token; GM_xmlhttpRequest({ method: "POST", url: url, headers: headers, data: JSON.stringify(body || {}), timeout: 30000, onload: function (response) { try { const text = response.responseText || "{}"; const data = JSON.parse(text); if (response.status < 200 || response.status >= 300) { appendLog("http-error", { url: url, status: response.status, response: data }); reject(new Error(formatHttpError(response.status, data, text))); return; } resolve(data); } catch (error) { reject(new Error("响应解析失败:" + error.message)); } }, ontimeout: function () { appendLog("timeout", { url: url }); reject(new Error("请求超时,请检查网络或接口白名单。")); }, onerror: function (error) { appendLog("network-error", { url: url, error: error }); reject(new Error("网络请求失败:" + (error.error || "请检查跨域授权或网络"))); } }); }); } async function apiPost(path, body) { const config = getConfig(); const token = await ensureAuthToken(); const result = await gmPost(config.baseUrl + path, body, token); if (isAuthExpiredResult(result)) { const pageAuth = readCurrentPageAuth(); const retryToken = pageAuth && pageAuth.token ? pageAuth.token : ""; if (retryToken) { if (pageAuth && pageAuth.token) saveDetectedPageAuth(pageAuth); const retryResult = await gmPost(config.baseUrl + path, body, retryToken); if (retryResult && retryResult.code === SUCCESS_CODE) return retryResult; if (!isAuthExpiredResult(retryResult)) return throwApiError(path, body, retryResult); } clearAuthToken(); appendLog("auth-expired", { path: path, result: result }); throw new Error("插件登录态已失效,请刷新小工单页面后重试。"); } if (!result || result.code !== SUCCESS_CODE) { return throwApiError(path, body, result); } return result; } function throwApiError(path, body, result) { const hint = errorHints[result && result.code] ? "\n" + errorHints[result.code] : ""; appendLog("api-error", { path: path, body: body, result: result }); throw new Error((result && result.code ? result.code : "API") + " " + (result && (result.msg || result.message) ? (result.msg || result.message) : "接口调用失败") + hint); } function isAuthExpiredResult(result) { const code = result && String(result.code || ""); const subCode = result && String(result.subCode || ""); const message = result && String(result.msg || result.message || ""); return code === "401" || code === "01000004" || subCode === "USER_VERIFICATION_ERROR" || /未授权|重新登录|登录已失效|token/i.test(message); } function formatHttpError(status, data, fallbackText) { const msg = data && data.msg ? data.msg : fallbackText.slice(0, 120); return "HTTP " + status + ": " + msg; } // ============================================================ // CORE CSS (VERBATIM) // ============================================================ function css() { return html` #${ROOT_ID} * { box-sizing: border-box; } #${ROOT_ID} { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #xgd-plan-query-mask { position: fixed; inset: 0; z-index: 2147483647; display: none; align-items: center; justify-content: center; background: rgba(17, 24, 39, .24); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #xgd-plan-query-modal { width: min(1100px, calc(100vw - 32px)); max-height: min(820px, calc(100vh - 32px)); background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; box-shadow: 0 24px 60px rgba(15, 23, 42, .2); overflow: hidden; color: #1f2937; } .xgd-head { height: 52px; display: flex; align-items: center; justify-content: space-between; padding: 0 18px; border-bottom: 1px solid #edf0f2; } .xgd-title { font-size: 16px; font-weight: 650; } .xgd-head-actions { display: flex; align-items: center; gap: 8px; position: relative; } .xgd-account-wrap { position: relative; display: none; } .xgd-account-btn { display: inline-flex; align-items: center; justify-content: center; height: 30px; border: 1px solid #d7dde3; border-radius: 6px; background: #fff; color: #344054; padding: 0 10px; cursor: pointer; font-size: 12px; max-width: 180px; } .xgd-account-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .xgd-account-menu { display: none; position: absolute; right: 0; top: 36px; width: 132px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 6px; background: #fff; box-shadow: 0 12px 28px rgba(15,23,42,.14); z-index: 2; } .xgd-account-menu button { display: flex; align-items: center; justify-content: center; width: 100%; height: 30px; border: 0; border-radius: 5px; background: #fff; color: #344054; cursor: pointer; font-size: 12px; } .xgd-account-menu button:hover { background: #f6f8fa; } .xgd-account-menu .danger { color: #d92d20; } .xgd-account-menu .primary-text { color: #24af76; } .xgd-toast { position: fixed; left: 50%; top: 86px; transform: translateX(-50%); z-index: 2147483647; display: none; max-width: min(560px, calc(100vw - 48px)); padding: 12px 16px; border-radius: 6px; background: #fff; color: #344054; border: 1px solid #ff9f1a; border-left: 4px solid #ff8a00; font-size: 14px; box-shadow: 0 12px 32px rgba(15,23,42,.18); } .xgd-close { width: 32px; height: 32px; border: 0; background: transparent; cursor: pointer; font-size: 22px; color: #667085; } .xgd-body { padding: 14px 18px 18px; overflow: auto; max-height: calc(min(820px, calc(100vh - 32px)) - 52px); } .xgd-panel { display: none; } .xgd-panel.active { display: block; } .xgd-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 10px; align-items: end; } .xgd-field { grid-column: span 3; display: flex; flex-direction: column; gap: 5px; min-width: 0; } .xgd-field.wide { grid-column: span 6; } .xgd-field label { color: #667085; font-size: 12px; } .xgd-input, .xgd-select { height: 34px; border: 1px solid #d7dde3; border-radius: 6px; padding: 0 10px; font-size: 13px; outline: none; background: #fff; min-width: 0; } .xgd-input:focus, .xgd-select:focus { border-color: ${GREEN}; box-shadow: 0 0 0 3px rgba(36,175,118,.12); } .xgd-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 12px; } .xgd-btn { height: 34px; border-radius: 6px; border: 1px solid #d7dde3; background: #fff; color: #344054; padding: 0 13px; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; } .xgd-btn.primary { border-color: ${GREEN}; background: ${GREEN}; color: #fff; } .xgd-btn:disabled { opacity: .55; cursor: not-allowed; } .xgd-status { margin-top: 12px; min-height: 22px; color: #667085; font-size: 13px; white-space: pre-wrap; } .xgd-status.error { color: #d92d20; } .xgd-section-label { grid-column: span 12; color: #98a2b3; font-size: 12px; margin-top: 2px; } .xgd-table-wrap { border: 1px solid #edf0f2; border-radius: 6px; overflow: auto; max-height: 320px; margin-top: 12px; } .xgd-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 1040px; } .xgd-table th { background: #f7f8fa; color: #667085; font-weight: 600; text-align: left; height: 34px; border-bottom: 1px solid #edf0f2; padding: 0 10px; white-space: nowrap; } .xgd-table td { border-bottom: 1px solid #edf0f2; padding: 8px 10px; vertical-align: top; } .xgd-chip { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; background: #eaf8f2; color: ${GREEN}; font-size: 12px; } .xgd-empty { color: #98a2b3; padding: 18px; text-align: center; } .xgd-pager { display: flex; gap: 8px; align-items: center; margin-top: 10px; color: #667085; font-size: 13px; } .xgd-help { margin-top: 10px; border: 1px solid #edf0f2; border-radius: 6px; padding: 12px; line-height: 1.7; color: #475467; font-size: 13px; } @media (max-width: 720px) { .xgd-field, .xgd-field.wide { grid-column: span 12; } #xgd-plan-query-modal { width: calc(100vw - 18px); } } `; } // ============================================================ // MOUNT (VERBATIM) // ============================================================ function mount() { if (!document.body) return; if (isWorkOrderRoute()) { if (!document.getElementById(ROOT_ID)) createPluginRoot(); ensurePluginRegistered(); } ensureXgdPluginHub().render(); } function createPluginRoot() { const root = document.createElement("div"); root.id = ROOT_ID; root.innerHTML = html`
`; document.body.appendChild(root); bindEvents(root); // __BIND_EVENTS_BEGIN__ root.querySelector("#xgd-qw-btn-query").addEventListener("click", function () { onQuery(1); }); root.querySelector("#xgd-qw-prev").addEventListener("click", function () { if (state.pageNum > 1) onQuery(state.pageNum - 1); }); root.querySelector("#xgd-qw-next").addEventListener("click", function () { var totalPages = Math.ceil(state.total / state.pageSize); if (state.pageNum < totalPages) onQuery(state.pageNum + 1); }); root.querySelector("#xgd-qw-btn-add").addEventListener("click", function () { resetCreateForm(); switchPage("create"); }); root.querySelector("#xgd-qw-btn-adjust-time").addEventListener("click", function () { adjustWorkOrderTimes(); }); root.querySelector("#xgd-qw-add-row").addEventListener("click", function () { addDetailRow(); }); root.querySelector("#xgd-qw-cancel").addEventListener("click", function () { switchPage("list"); }); root.querySelector("#xgd-qw-submit").addEventListener("click", function () { onSubmit(); }); root.querySelector("#xgd-qw-spec").addEventListener("input", function () { // 同步表头的产品规格到所有空的明细行 var headerSpec = this.value; state.detailRows.forEach(function (row) { if (!row.spec || row.spec === "") { row.spec = headerSpec; } }); // 如果当前有明细行在显示,重新渲染以同步显示 if (state.currentPage === "create") { renderDetailRows(); } }); root.querySelector("#xgd-qw-spec").addEventListener("blur", function () { loadMaterialOptions(); }); root.querySelector("#xgd-qw-toggle-wt").addEventListener("click", function () { toggleWorkTimePanel(); }); root.querySelector("#xgd-qw-save-wt").addEventListener("click", function () { saveWorkTimeConfigFromUI(); }); loadWorkTimeConfigToUI(); // __BIND_EVENTS_END__ } function bindEvents(root) { root.querySelector(".xgd-close").addEventListener("click", closeModal); root.querySelector("#xgd-plan-query-mask").addEventListener("click", function (event) { if (event.target.id === "xgd-plan-query-mask") closeModal(); }); root.querySelector("#xgd-account-btn").addEventListener("click", toggleAccountMenu); root.querySelector("#xgd-refresh-session").addEventListener("click", onRefreshAuth); root.querySelector("#xgd-download-log").addEventListener("click", downloadLogs); root.querySelector("#xgd-logout").addEventListener("click", onLogout); document.addEventListener("click", closeAccountMenuOnOutside); root.querySelector("#xgd-retry-auth").addEventListener("click", onRefreshAuth); } function openModal() { q("#xgd-plan-query-mask").style.display = "flex"; refreshAuthView(); syncCurrentPageAuth(); switchTab(hasUsableAuth() ? "use" : "login"); } function closeModal() { q("#xgd-plan-query-mask").style.display = "none"; ensureXgdPluginHub().render(); } function switchTab(name) { document.querySelectorAll("#" + ROOT_ID + " .xgd-panel").forEach(function (panel) { panel.classList.toggle("active", panel.dataset.panel === name); }); } function toggleAccountMenu(event) { event.stopPropagation(); const menu = q("#xgd-account-menu"); if (menu) menu.style.display = menu.style.display === "block" ? "none" : "block"; } function closeAccountMenuOnOutside(event) { const wrap = q("#xgd-account-wrap"); const menu = q("#xgd-account-menu"); if (wrap && menu && !wrap.contains(event.target)) menu.style.display = "none"; } function closeAccountMenu() { const menu = q("#xgd-account-menu"); if (menu) menu.style.display = "none"; } function onLogout() { closeAccountMenu(); GM_setValue(STORE.token, ""); GM_setValue(STORE.userLabel, ""); GM_setValue(STORE.loginAt, ""); refreshAuthView(); switchTab("login"); setStatus("login", "已退出登录,请重新登录小工单网页。"); setStatus("use", ""); } async function onRefreshAuth() { closeAccountMenu(); setStatus("login", ""); setStatus("use", ""); try { setStatus(hasUsableAuth() ? "use" : "login", "正在刷新登录态..."); const pageAuth = syncCurrentPageAuth(); if (pageAuth && pageAuth.token) { refreshAuthView(); switchTab("use"); setStatus("use", "已使用当前网页登录态。"); return; } switchTab("login"); setStatus("login", "未识别到当前页面登录态,请确认小工单网页已登录。", true); } catch (error) { clearAuthToken(); switchTab("login"); setStatus("login", normalizeErrorMessage(error), true); } } function normalizeErrorMessage(error) { const message = typeof error === "string" ? error : error && error.message; return message && message !== "undefined" && message !== "null" ? message : "插件执行失败,请稍后重试。"; } function setStatus(area, message, isError) { const el = q(area === "login" ? "#xgd-login-status" : "#xgd-use-status"); if (el) { el.textContent = message || ""; el.classList.toggle("error", Boolean(isError)); } } function refreshAuthView() { const token = GM_getValue(STORE.token, ""); const accountWrap = q("#xgd-account-wrap"); const accountName = q("#xgd-account-name"); if (!accountWrap || !accountName) return; if (token) { accountWrap.style.display = "block"; accountName.textContent = GM_getValue(STORE.userLabel, "对接账号") || "对接账号"; } else { accountWrap.style.display = "none"; closeAccountMenu(); } } // ============================================================ // LOGGING (VERBATIM) // ============================================================ function appendLog(type, detail) { const logs = readLogs(); logs.push({ time: new Date().toISOString(), plugin: PLUGIN_META.title, url: location.href, type: type, detail: detail }); GM_setValue(STORE.logs, JSON.stringify(logs.slice(-200))); } function readLogs() { try { return JSON.parse(GM_getValue(STORE.logs, "[]")); } catch (error) { return []; } } function showToast(message) { const toast = q(".xgd-toast"); if (!toast) return; toast.innerHTML = '' + escapeHtml(message); toast.style.display = "block"; clearTimeout(showToast.timer); showToast.timer = setTimeout(function () { toast.style.display = "none"; }, 4200); } function downloadLogs() { closeAccountMenu(); const payload = { exportedAt: new Date().toISOString(), location: location.href, user: GM_getValue(STORE.userLabel, ""), logs: readLogs() }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "xgd-plugin-log-" + Date.now() + ".json"; document.body.appendChild(link); link.click(); link.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); } function escapeHtml(value) { return String(value === undefined || value === null ? "" : value) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); } function q(selector) { return document.querySelector("#" + ROOT_ID + " " + selector); } function isWorkOrderRoute() { var route = currentRoutePath(); var normalized = WORK_ORDER_ROUTE.replace(/\/+$/, ""); return route === normalized || route.startsWith(normalized + "/"); } function currentRoutePath() { var hashPath = ""; if (location.hash) { hashPath = location.hash.replace(/^#/, "").split("?")[0]; if (hashPath && !hashPath.startsWith("/")) hashPath = "/" + hashPath; } if (hashPath) return hashPath.replace(/\/+$/, "") || "/"; return (location.pathname || "/").replace(/\/+$/, "") || "/"; } // ============================================================ // LAUNCHER (VERBATIM) // ============================================================ let xgdLauncherPlugin = null; function ensureXgdPluginHub() { function ensureRoot() { if (!xgdLauncherPlugin) return null; const rootId = "xgd-plugin-launcher-" + xgdLauncherPlugin.id; let root = document.getElementById(rootId); if (!root) { root = document.createElement("div"); root.id = rootId; root.innerHTML = html` `; document.body.appendChild(root); } const button = root.querySelector(".xgd-plugin-launcher-button"); let dragging = false; let moved = false; let dragOffsetX = 0; let dragOffsetY = 0; applyLauncherPosition(); function isCurrentRoute() { const route = currentRoutePath(); return route === xgdLauncherPlugin.route || route.startsWith(xgdLauncherPlugin.route + "/"); } function render() { button.textContent = xgdLauncherPlugin.title; root.style.display = isCurrentRoute() ? "block" : "none"; applyLauncherPosition(); requestLauncherLayout(); } function applyLauncherPosition() { let position = null; try { position = JSON.parse(GM_getValue(STORE.launcherPosition, "null")); } catch (error) { position = null; } if (position && Number.isFinite(position.left) && Number.isFinite(position.top)) { root.dataset.customPosition = "true"; placeLauncher(position.left, position.top); } else { root.dataset.customPosition = "false"; placeDefaultLauncher(0); } } function layoutVisibleLaunchers() { const roots = Array.prototype.slice.call(document.querySelectorAll('[id^="xgd-plugin-launcher-"]')) .filter((item) => item.style.display !== "none") .sort((left, right) => left.id.localeCompare(right.id)); if (roots.length <= 1) return; roots.forEach((item, index) => { if (item.dataset.customPosition === "true") return; const itemButton = item.querySelector(".xgd-plugin-launcher-button"); if (!itemButton) return; itemButton.style.left = "auto"; itemButton.style.top = "auto"; itemButton.style.right = "24px"; itemButton.style.bottom = (86 + index * 48) + "px"; }); const placed = []; roots.forEach((item, index) => { const itemButton = item.querySelector(".xgd-plugin-launcher-button"); if (!itemButton) return; let rect = itemButton.getBoundingClientRect(); let left = rect.left; let top = rect.top; let attempts = 0; while (placed.some((placedRect) => isLauncherOverlap({ left, top, width: rect.width, height: rect.height }, placedRect)) && attempts < roots.length + 4) { top = Math.max(8, top - 48); attempts += 1; } if (placed.some((placedRect) => isLauncherOverlap({ left, top, width: rect.width, height: rect.height }, placedRect))) { itemButton.style.left = "auto"; itemButton.style.top = "auto"; itemButton.style.right = "24px"; itemButton.style.bottom = (86 + index * 48) + "px"; rect = itemButton.getBoundingClientRect(); left = rect.left; top = rect.top; } else if (top !== rect.top) { placeLauncher(left, top); } placed.push({ left, top, width: rect.width, height: rect.height }); }); } function isLauncherOverlap(a, b) { return a.left < b.left + b.width + 8 && a.left + a.width + 8 > b.left && a.top < b.top + b.height + 8 && a.top + a.height + 8 > b.top; } function requestLauncherLayout() { setTimeout(function () { document.dispatchEvent(new CustomEvent("xgd-plugin-launchers-layout")); layoutVisibleLaunchers(); }, 0); } function placeDefaultLauncher(index) { button.style.left = "auto"; button.style.top = "auto"; button.style.right = "24px"; button.style.bottom = (86 + index * 48) + "px"; } function placeLauncher(left, top) { button.style.left = left + "px"; button.style.top = top + "px"; button.style.right = "auto"; button.style.bottom = "auto"; } if (root.dataset.bound !== "true") { root.dataset.bound = "true"; button.addEventListener("pointerdown", function (event) { dragging = true; moved = false; const rect = button.getBoundingClientRect(); dragOffsetX = event.clientX - rect.left; dragOffsetY = event.clientY - rect.top; if (button.setPointerCapture) button.setPointerCapture(event.pointerId); }); button.addEventListener("pointermove", function (event) { if (!dragging) return; const left = Math.min(Math.max(8, event.clientX - dragOffsetX), window.innerWidth - button.offsetWidth - 8); const top = Math.min(Math.max(8, event.clientY - dragOffsetY), window.innerHeight - button.offsetHeight - 8); if (Math.abs(left - button.getBoundingClientRect().left) > 3 || Math.abs(top - button.getBoundingClientRect().top) > 3) moved = true; placeLauncher(left, top); }); button.addEventListener("pointerup", function (event) { if (!dragging) return; dragging = false; if (button.releasePointerCapture) button.releasePointerCapture(event.pointerId); if (moved) { const rect = button.getBoundingClientRect(); root.dataset.customPosition = "true"; GM_setValue(STORE.launcherPosition, JSON.stringify({ left: rect.left, top: rect.top })); requestLauncherLayout(); event.stopPropagation(); } }); button.addEventListener("click", function (event) { event.stopPropagation(); if (moved) { moved = false; return; } document.dispatchEvent(new CustomEvent("xgd-plugin-open", { detail: { id: xgdLauncherPlugin.id } })); }); window.addEventListener("hashchange", render); window.addEventListener("popstate", render); document.addEventListener("xgd-plugin-launchers-layout", layoutVisibleLaunchers); } return { render: render }; } return { register: function (plugin) { xgdLauncherPlugin = plugin; const root = ensureRoot(); if (root) root.render(); }, render: function () { const root = ensureRoot(); if (root) root.render(); } }; } function ensurePluginRegistered() { const hub = ensureXgdPluginHub(); if (!pluginOpenBound) { pluginOpenBound = true; document.addEventListener("xgd-plugin-open", function (event) { if (event.detail && event.detail.id === PLUGIN_META.id) openModal(); }); } hub.register(PLUGIN_META); } // ============================================================ // BOOT (VERBATIM) // ============================================================ function boot() { const tryMount = function () { if (!document.body) return; try { mount(); } catch (error) { appendLog("plugin-failure", { message: normalizeErrorMessage(error) }); showToast("插件出现错误,请点击右上角下载日志,并发送给您的实施经理。"); } }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", tryMount); else tryMount(); window.addEventListener("hashchange", tryMount); window.addEventListener("popstate", tryMount); ["pushState", "replaceState"].forEach(function (method) { const original = history[method]; history[method] = function () { const result = original.apply(this, arguments); setTimeout(tryMount, 0); return result; }; }); [0, 300, 800, 1500, 3000, 5000].forEach(function (delay) { setTimeout(tryMount, delay); }); } // ============================================================ // BUSINESS API CALLS // ============================================================ // __BUSINESS_API_CALLS_BEGIN__ function getWorkTimeConfig() { try { var saved = GM_getValue(STORE.workTimeConfig, ""); if (saved) return JSON.parse(saved); } catch (e) { /* ignore */ } return JSON.parse(JSON.stringify(DEFAULT_WORK_TIME)); } function toggleWorkTimePanel() { var panel = q("#xgd-qw-wt-panel"); var btn = q("#xgd-qw-toggle-wt"); if (!panel || !btn) return; if (panel.style.display === "none") { panel.style.display = "block"; btn.textContent = "收起"; } else { panel.style.display = "none"; btn.textContent = "展开"; } } function loadWorkTimeConfigToUI() { var config = getWorkTimeConfig(); var mStart = q("#xgd-wt-m-start"); var mEnd = q("#xgd-wt-m-end"); var aStart = q("#xgd-wt-a-start"); var aEnd = q("#xgd-wt-a-end"); var eStart = q("#xgd-wt-e-start"); var eEnd = q("#xgd-wt-e-end"); if (mStart) mStart.value = config.morning.start; if (mEnd) mEnd.value = config.morning.end; if (aStart) aStart.value = config.afternoon.start; if (aEnd) aEnd.value = config.afternoon.end; if (eStart) eStart.value = config.evening.start; if (eEnd) eEnd.value = config.evening.end; } function saveWorkTimeConfigFromUI() { var config = { morning: { start: (q("#xgd-wt-m-start") || {}).value || "", end: (q("#xgd-wt-m-end") || {}).value || "" }, afternoon: { start: (q("#xgd-wt-a-start") || {}).value || "", end: (q("#xgd-wt-a-end") || {}).value || "" }, evening: { start: (q("#xgd-wt-e-start") || {}).value || "", end: (q("#xgd-wt-e-end") || {}).value || "" } }; GM_setValue(STORE.workTimeConfig, JSON.stringify(config)); showToast("工作时间配置已保存"); } function parseTimeToMinutes(timeStr) { var parts = timeStr.split(":"); return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); } function getWorkPeriods() { var config = getWorkTimeConfig(); var periods = []; if (config.morning.start && config.morning.end) { periods.push({ start: parseTimeToMinutes(config.morning.start), end: parseTimeToMinutes(config.morning.end) }); } if (config.afternoon.start && config.afternoon.end) { periods.push({ start: parseTimeToMinutes(config.afternoon.start), end: parseTimeToMinutes(config.afternoon.end) }); } if (config.evening.start && config.evening.end) { periods.push({ start: parseTimeToMinutes(config.evening.start), end: parseTimeToMinutes(config.evening.end) }); } return periods; } function getDailyWorkMinutes() { var periods = getWorkPeriods(); var total = 0; periods.forEach(function (p) { total += p.end - p.start; }); return total; } function adjustToWorkTime(date) { var periods = getWorkPeriods(); var d = new Date(date); if (periods.length === 0) return d; var minutes = d.getHours() * 60 + d.getMinutes(); for (var i = 0; i < periods.length; i++) { var p = periods[i]; if (minutes >= p.start && minutes < p.end) return d; if (minutes < p.start) { d.setHours(Math.floor(p.start / 60), p.start % 60, 0, 0); return d; } } d.setDate(d.getDate() + 1); d.setHours(Math.floor(periods[0].start / 60), periods[0].start % 60, 0, 0); return d; } function addWorkHours(startDate, totalHours) { var periods = getWorkPeriods(); var d = new Date(startDate); if (periods.length === 0) return d; var remainingMinutes = totalHours * 60; var maxIterations = 1000; var iteration = 0; while (remainingMinutes > 0 && iteration < maxIterations) { iteration++; var dayMinutes = d.getHours() * 60 + d.getMinutes(); for (var i = 0; i < periods.length; i++) { var p = periods[i]; if (dayMinutes >= p.start && dayMinutes < p.end) { var available = p.end - dayMinutes; var use = Math.min(available, remainingMinutes); d.setMinutes(d.getMinutes() + use); remainingMinutes -= use; dayMinutes = d.getHours() * 60 + d.getMinutes(); if (remainingMinutes <= 0) return d; } } d.setDate(d.getDate() + 1); d.setHours(Math.floor(periods[0].start / 60), periods[0].start % 60, 0, 0); } return d; } function buildQueryBody(pageNum) { var now = new Date(); var sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); return { createdAtGte: formatDateTime(sevenDaysAgo), createdAtLte: formatDateTime(now), page: { pageNum: pageNum, pageSize: state.pageSize } }; } function formatDateTime(date) { var y = date.getFullYear(); var m = String(date.getMonth() + 1).padStart(2, "0"); var d = String(date.getDate()).padStart(2, "0"); var h = String(date.getHours()).padStart(2, "0"); var min = String(date.getMinutes()).padStart(2, "0"); var s = String(date.getSeconds()).padStart(2, "0"); return y + "-" + m + "-" + d + " " + h + ":" + min + ":" + s; } async function onQuery(pageNum) { if (state.loading) return; if (!hasUsableAuth()) { switchTab("login"); return; } try { state.loading = true; setStatus("use", "正在查询..."); var result = await apiPost(API.projectList, buildQueryBody(pageNum)); var data = result.data || []; var page = result.page || {}; state.workOrders = data; state.pageNum = page.pageNum || pageNum; state.total = page.total || 0; renderList(); updatePager(); setStatus("use", "查询完成,共 " + state.total + " 条记录"); } catch (e) { setStatus("use", e.message, true); } finally { state.loading = false; } } function renderList() { var tbody = q("#xgd-qw-list-body"); if (!tbody) return; if (state.workOrders.length === 0) { tbody.innerHTML = '暂无数据'; return; } var html = ""; state.workOrders.forEach(function (item) { var status = statusText[item.status] || item.status || "-"; html += ""; html += "" + escapeHtml(item.projectCode || "-") + ""; html += "" + escapeHtml(item.productName || "-") + ""; html += "" + escapeHtml(item.productSpecification || "-") + ""; html += "" + escapeHtml(item.planAmount || "-") + ""; html += '' + escapeHtml(status) + ""; html += "" + escapeHtml(item.planStartTime || "-") + ""; html += "" + escapeHtml(item.planEndTime || "-") + ""; html += "" + escapeHtml(item.createTime || "-") + ""; html += ""; }); tbody.innerHTML = html; } function updatePager() { var pager = q("#xgd-qw-pager"); var prevBtn = q("#xgd-qw-prev"); var nextBtn = q("#xgd-qw-next"); var pageInfo = q("#xgd-qw-page-info"); if (!pager) return; var totalPages = Math.ceil(state.total / state.pageSize) || 1; if (state.total > 0) { pager.style.display = "flex"; pageInfo.textContent = "第 " + state.pageNum + " / " + totalPages + " 页"; prevBtn.disabled = state.pageNum <= 1; nextBtn.disabled = state.pageNum >= totalPages; } else { pager.style.display = "none"; } } function switchPage(pageName) { state.currentPage = pageName; var listPage = q("#xgd-qw-page-list"); var createPage = q("#xgd-qw-page-create"); if (listPage) listPage.style.display = pageName === "list" ? "block" : "none"; if (createPage) createPage.style.display = pageName === "create" ? "block" : "none"; if (pageName === "create") { renderDetailRows(); } } function renderDetailRows() { var tbody = q("#xgd-qw-detail-body"); var thead = q("#xgd-qw-detail-head"); if (!tbody) return; var hasMatched = state.detailRows.some(function (row) { return row.productCode; }); var html = ""; if (hasMatched) { if (thead) { thead.innerHTML = '序号产品规格材质计划数产品编号制造车间日产量计划开始计划结束'; } state.detailRows.forEach(function (row, index) { html += ""; html += "" + (index + 1) + ""; html += "" + escapeHtml(row.spec || "-") + ""; html += "" + escapeHtml(row.material || "-") + ""; html += "" + escapeHtml(row.plannedNum || "-") + ""; html += "" + escapeHtml(row.productCode || "-") + ""; html += "" + escapeHtml(row.workshop || "-") + ""; html += "" + escapeHtml(row.dailyOutput || "-") + ""; html += "" + escapeHtml(row.planStartTime || "-") + ""; html += "" + escapeHtml(row.planEndTime || "-") + ""; html += ""; }); } else { if (thead) { thead.innerHTML = '序号产品规格材质计划数操作'; } var optionsHtml = ''; state.materialOptions.forEach(function (opt) { optionsHtml += ''; }); state.detailRows.forEach(function (row, index) { html += ""; html += "" + (index + 1) + ""; html += ''; html += ''; html += ''; html += ''; html += ""; }); html += '' + optionsHtml + ''; } tbody.innerHTML = html || '暂无明细行,请点击「+ 添加行」'; if (!hasMatched) { // datalist handles search natively } bindDetailEvents(); } function bindDetailEvents() { var root = document.getElementById(ROOT_ID); if (!root) return; root.querySelectorAll(".xgd-qw-spec-row").forEach(function (input) { input.addEventListener("input", function () { var idx = parseInt(this.getAttribute("data-index"), 10); state.detailRows[idx].spec = this.value; // 同步到表头的产品规格 var headerSpec = q("#xgd-qw-spec"); if (headerSpec && this.value) { headerSpec.value = this.value; } }); input.addEventListener("blur", function () { loadMaterialOptionsForRow(); }); }); root.querySelectorAll(".xgd-qw-material").forEach(function (input) { input.addEventListener("input", function () { var idx = parseInt(this.getAttribute("data-index"), 10); state.detailRows[idx].material = this.value; }); }); root.querySelectorAll(".xgd-qw-planned-num").forEach(function (input) { input.addEventListener("input", function () { var idx = parseInt(this.getAttribute("data-index"), 10); state.detailRows[idx].plannedNum = this.value; }); }); root.querySelectorAll(".xgd-qw-del-row").forEach(function (btn) { btn.addEventListener("click", function () { var idx = parseInt(this.getAttribute("data-index"), 10); state.detailRows.splice(idx, 1); if (state.detailRows.length === 0) { var headerSpec = (q("#xgd-qw-spec") || {}).value || ""; state.detailRows.push({ spec: headerSpec, material: "", plannedNum: "" }); } renderDetailRows(); }); }); } function addDetailRow() { var headerSpec = (q("#xgd-qw-spec") || {}).value || ""; state.detailRows.push({ spec: headerSpec, material: "", plannedNum: "" }); renderDetailRows(); } function resetCreateForm() { var specInput = q("#xgd-qw-spec"); if (specInput) specInput.value = ""; state.detailRows = [{ spec: "", material: "", plannedNum: "" }]; state.materialOptions = []; state.specProducts = []; } async function queryProductsBySpec(spec) { var products = []; var seen = {}; var pageNum = 1; var hasMore = true; try { while (hasMore) { var result = await apiPost(API.productQuery, { productSpecification: spec, page: { pageNum: pageNum, pageSize: 100 } }); var list = (result.data && result.data.data) || []; var page = result.data && result.data.page; list.forEach(function (p) { if (!seen[p.productCode]) { seen[p.productCode] = true; products.push(p); } }); // 检查是否还有更多数据 if (page && page.total) { hasMore = products.length < page.total && list.length === 100; } else { hasMore = list.length === 100; } pageNum++; // 最多查询10页,防止无限循环 if (pageNum > 10) break; } appendLog("query-products-complete", { spec: spec, totalProducts: products.length, pages: pageNum - 1 }); } catch (e) { appendLog("query-products-error", { spec: spec, error: e.message, pageNum: pageNum }); } return products; } async function queryProductByCode(productCode) { try { var result = await apiPost(API.productQuery, { productCode: productCode, page: { pageNum: 1, pageSize: 100 } }); var list = (result.data && result.data.data) || []; appendLog("product-query-by-code", { productCode: productCode, resultCount: list.length, results: list.map(function (p) { return { code: p.productCode, name: p.productName, spec: p.productSpecification }; }) }); var exact = list.find(function (p) { return p.productCode === productCode; }); return exact || null; } catch (e) { appendLog("product-query-by-code-error", { productCode: productCode, error: e.message }); return null; } } async function loadMaterialOptions() { var spec = (q("#xgd-qw-spec") || {}).value || ""; if (!spec.trim()) return; spec = spec.trim(); try { setStatus("use", "正在加载材质选项..."); var products = await queryProductsBySpec(spec); appendLog("load-material-debug", { inputSpec: spec, totalProducts: products.length, firstProducts: products.slice(0, 3).map(function(p) { return { code: p.productCode, name: p.productName, spec: p.productSpecification, hasCustomFields: !!p.customFieldValues, customFieldNames: p.customFieldValues ? p.customFieldValues.map(function(cf) { return cf.name; }) : [] }; }) }); // 不过滤,直接使用API返回的产品(API本身是模糊查询) var materials = []; products.forEach(function (p) { // 优先从自定义字段中提取材质 if (p.customFieldValues) { p.customFieldValues.forEach(function (cf) { if (cf.name === "材质" && cf.value && materials.indexOf(cf.value) === -1) { materials.push(cf.value); } }); } // 如果没有自定义字段"材质",尝试从产品编号中提取(格式:材质-规格) if (materials.length === 0 && p.productCode && p.productCode.indexOf("-") !== -1) { var codeMaterial = p.productCode.split("-")[0]; if (codeMaterial && materials.indexOf(codeMaterial) === -1) { materials.push(codeMaterial); } } }); state.materialOptions = materials.sort(); state.specProducts = products; renderDetailRows(); if (materials.length === 0) { setStatus("use", "未找到该规格对应的材质,请检查产品规格是否正确,或产品是否配置了「材质」自定义字段", true); } else { setStatus("use", "已加载 " + materials.length + " 种材质选项"); } } catch (e) { appendLog("load-material-error", { spec: spec, error: e.message }); setStatus("use", "加载材质选项失败:" + e.message, true); } } async function loadMaterialOptionsForRow() { var specs = []; state.detailRows.forEach(function (row) { var s = (row.spec || "").trim(); if (s && specs.indexOf(s) === -1) specs.push(s); }); if (specs.length === 0) return; try { setStatus("use", "正在加载材质选项..."); var allProducts = []; var seen = {}; for (var i = 0; i < specs.length; i++) { var spec = specs[i]; var products = await queryProductsBySpec(spec); appendLog("load-material-row-debug", { inputSpec: spec, totalProducts: products.length, firstProducts: products.slice(0, 3).map(function(p) { return { code: p.productCode, name: p.productName, spec: p.productSpecification, hasCustomFields: !!p.customFieldValues, customFieldNames: p.customFieldValues ? p.customFieldValues.map(function(cf) { return cf.name; }) : [] }; }) }); products.forEach(function (p) { if (!seen[p.productCode]) { seen[p.productCode] = true; allProducts.push(p); } }); } var materials = []; allProducts.forEach(function (p) { // 优先从自定义字段中提取材质 if (p.customFieldValues) { p.customFieldValues.forEach(function (cf) { if (cf.name === "材质" && cf.value && materials.indexOf(cf.value) === -1) { materials.push(cf.value); } }); } // 如果没有自定义字段"材质",尝试从产品编号中提取(格式:材质-规格) if (materials.length === 0 && p.productCode && p.productCode.indexOf("-") !== -1) { var codeMaterial = p.productCode.split("-")[0]; if (codeMaterial && materials.indexOf(codeMaterial) === -1) { materials.push(codeMaterial); } } }); state.materialOptions = materials.sort(); state.specProducts = allProducts; renderDetailRows(); if (materials.length === 0) { setStatus("use", "未找到该规格对应的材质,请检查产品规格是否正确,或产品是否配置了「材质」自定义字段", true); } else { setStatus("use", "已加载 " + materials.length + " 种材质选项"); } } catch (e) { appendLog("load-material-row-error", { specs: specs, error: e.message }); setStatus("use", "加载材质选项失败:" + e.message, true); } } async function onSubmit() { if (state.submitting) return; if (!hasUsableAuth()) { switchTab("login"); return; } var spec = (q("#xgd-qw-spec") || {}).value || ""; var validRows = state.detailRows.filter(function (row) { return row.spec && row.material && row.plannedNum; }); if (validRows.length === 0) { showToast("请至少填写一行完整的明细(产品规格、材质和计划数)"); return; } state.submitting = true; setStatus("use", "正在匹配产品..."); try { var matchedRows = []; var unmatchedItems = []; var productCache = {}; for (var ri = 0; ri < validRows.length; ri++) { var row = validRows[ri]; var material = row.material.trim(); var rowSpec = (row.spec || "").trim(); var matchedProduct = null; var productCode = material + "-" + rowSpec; if (productCache[productCode] !== undefined) { matchedProduct = productCache[productCode]; } else { var byCode = await queryProductByCode(productCode); if (byCode) matchedProduct = byCode; productCache[productCode] = matchedProduct; } if (matchedProduct) { appendLog("product-matched", { material: material, spec: rowSpec, matchedCode: matchedProduct.productCode, matchedName: matchedProduct.productName }); var workshop = "", dailyOutput = 0; if (matchedProduct.customFieldValues) { matchedProduct.customFieldValues.forEach(function (cf) { if (cf.name === "制造车间") workshop = cf.value || ""; if (cf.name === "日产量") dailyOutput = parseFloat(cf.value) || 0; }); } var planNum = parseFloat(row.plannedNum) || 0; var leadTime = dailyOutput > 0 ? parseFloat((planNum / dailyOutput).toFixed(3)) : 1; matchedRows.push({ spec: rowSpec, material: material, plannedNum: row.plannedNum, productCode: matchedProduct.productCode, productName: matchedProduct.productName, workshop: workshop, dailyOutput: dailyOutput, leadTime: leadTime }); } else { appendLog("product-not-matched", { material: material, spec: rowSpec, productCode: productCode }); unmatchedItems.push(productCode); } } if (unmatchedItems.length > 0) { showToast("以下产品编号不存在:" + unmatchedItems.join("、")); setStatus("use", "部分产品不存在", true); state.submitting = false; return; } setStatus("use", "正在查询车间排产情况..."); var workshopMaxEndTime = await getWorkshopMaxEndTime(matchedRows); var now = new Date(); var workshopNextStart = {}; matchedRows.forEach(function (row) { var maxEnd = workshopMaxEndTime[row.workshop]; var startTime; if (maxEnd) { startTime = adjustToWorkTime(new Date(maxEnd)); } else if (workshopNextStart[row.workshop]) { startTime = adjustToWorkTime(new Date(workshopNextStart[row.workshop])); } else { startTime = adjustToWorkTime(new Date(now)); } row.planStartTime = formatDateTime(startTime); var dailyWorkMinutes = getDailyWorkMinutes(); var dailyWorkHours = dailyWorkMinutes / 60; var workDays = row.leadTime > 0 ? row.leadTime : 1; var totalWorkHours = workDays * dailyWorkHours; var endTime = addWorkHours(startTime, totalWorkHours); row.planEndTime = formatDateTime(endTime); workshopNextStart[row.workshop] = row.planEndTime; }); state.detailRows = matchedRows.map(function (row) { return { spec: row.spec, material: row.material, plannedNum: row.plannedNum, productCode: row.productCode, productName: row.productName, workshop: row.workshop, dailyOutput: row.dailyOutput, leadTime: row.leadTime, planStartTime: row.planStartTime, planEndTime: row.planEndTime }; }); renderDetailRows(); setStatus("use", "产品匹配成功,正在逐个创建工单..."); var createdCount = 0; for (var i = 0; i < matchedRows.length; i++) { var row = matchedRows[i]; setStatus("use", "正在创建第 " + (i + 1) + "/" + matchedRows.length + " 个工单:" + row.material); try { await apiPost(API.projectCreate, { productCode: row.productCode, plannedNum: String(row.plannedNum), planStartTime: row.planStartTime, planEndTime: row.planEndTime }); createdCount++; } catch (createErr) { showToast("创建工单失败(" + row.material + "):" + createErr.message); break; } } if (createdCount === matchedRows.length) { showToast("全部创建成功,共 " + createdCount + " 个工单"); setStatus("use", "全部创建成功,共 " + createdCount + " 个工单"); } else { setStatus("use", "部分创建成功:" + createdCount + "/" + matchedRows.length, true); } } catch (e) { setStatus("use", e.message, true); showToast("操作失败:" + e.message); } finally { state.submitting = false; } } async function getWorkshopMaxEndTime(matchedRows) { var result = {}; var workshops = []; matchedRows.forEach(function (row) { if (row.workshop && workshops.indexOf(row.workshop) === -1) workshops.push(row.workshop); }); if (workshops.length === 0) return result; var now = new Date(); var futureDate = new Date(now.getTime() + 180 * 24 * 60 * 60 * 1000); var allWorkOrders = []; var statuses = ["0", "10"]; for (var s = 0; s < statuses.length; s++) { var pageNum = 1; var hasMore = true; while (hasMore) { try { var woResult = await apiPost(API.projectList, { status: statuses[s], planStartTimeGte: formatDateTime(now), planStartTimeLte: formatDateTime(futureDate), page: { pageNum: pageNum, pageSize: 100 } }); var workOrders = woResult.data || []; allWorkOrders = allWorkOrders.concat(workOrders); var page = woResult.page || {}; var totalPages = Math.ceil((page.total || 0) / 100); hasMore = pageNum < totalPages && workOrders.length === 100; pageNum++; } catch (e) { hasMore = false; } } } var productCodes = []; allWorkOrders.forEach(function (wo) { if (wo.productCode && productCodes.indexOf(wo.productCode) === -1) productCodes.push(wo.productCode); }); var productWorkshopMap = {}; for (var i = 0; i < productCodes.length; i++) { try { var prodResult = await apiPost(API.productQuery, { productCode: productCodes[i], page: { pageNum: 1, pageSize: 1 } }); var productList = (prodResult.data && prodResult.data.data) || []; if (productList.length > 0 && productList[0].customFieldValues) { var wf = productList[0].customFieldValues.find(function (cf) { return cf.name === "制造车间"; }); if (wf && wf.value) productWorkshopMap[productCodes[i]] = wf.value; } } catch (e) { /* ignore */ } } workshops.forEach(function (workshop) { var maxEndTime = null; allWorkOrders.forEach(function (wo) { if (productWorkshopMap[wo.productCode] === workshop && wo.planEndTime) { if (!maxEndTime || wo.planEndTime > maxEndTime) maxEndTime = wo.planEndTime; } }); if (maxEndTime) result[workshop] = maxEndTime; }); return result; } // 调整工单时间到作息时间内 async function adjustWorkOrderTimes() { if (!hasUsableAuth()) { switchTab("login"); return; } if (state.workOrders.length === 0) { showToast("请先查询工单列表"); return; } var config = getWorkTimeConfig(); var periods = getWorkPeriods(); if (periods.length === 0) { showToast("请先配置工作时间"); return; } // 获取第一个时间段(上午)和第二个时间段(下午) var morningPeriod = periods[0]; var afternoonPeriod = periods.length > 1 ? periods[1] : null; if (!morningPeriod || !afternoonPeriod) { showToast("工作时间配置不完整,需要上午和下午时间段"); return; } var adjustedCount = 0; var failedCount = 0; setStatus("use", "正在调整工单时间..."); for (var i = 0; i < state.workOrders.length; i++) { var wo = state.workOrders[i]; if (!wo.planStartTime || !wo.planEndTime) continue; try { var startTime = new Date(wo.planStartTime); var endTime = new Date(wo.planEndTime); // 检查是否跨天(遇到0点) var startDay = startTime.getDate(); var endDay = endTime.getDate(); var isCrossDay = startDay !== endDay; var newStartTime, newEndTime; if (isCrossDay) { // 跨天情况:开始时间取上午上班时间,结束时间取下午下班时间 newStartTime = new Date(startTime); newStartTime.setHours(Math.floor(morningPeriod.start / 60), morningPeriod.start % 60, 0, 0); newEndTime = new Date(endTime); newEndTime.setHours(Math.floor(afternoonPeriod.end / 60), afternoonPeriod.end % 60, 0, 0); } else { // 不跨天:按原逻辑调整 newStartTime = adjustTimeToWorkHours(startTime, morningPeriod, afternoonPeriod); newEndTime = adjustTimeToWorkHours(endTime, morningPeriod, afternoonPeriod); } // 如果时间有变化,才调用更新接口 if (newStartTime.getTime() !== startTime.getTime() || newEndTime.getTime() !== endTime.getTime()) { await apiPost(API.projectUpdate, { projectCode: wo.projectCode, planStartTime: formatDateTime(newStartTime), planEndTime: formatDateTime(newEndTime) }); adjustedCount++; } } catch (e) { appendLog("adjust-time-error", { projectCode: wo.projectCode, error: e.message }); failedCount++; } } if (failedCount > 0) { setStatus("use", "调整完成:成功 " + adjustedCount + " 条,失败 " + failedCount + " 条", true); showToast("调整完成,部分失败请查看日志"); } else { setStatus("use", "调整完成:成功 " + adjustedCount + " 条"); showToast("时间调整成功"); // 重新查询列表 await onQuery(state.pageNum); } } // 调整单个时间到作息时间内 function adjustTimeToWorkHours(date, morningPeriod, afternoonPeriod) { var d = new Date(date); var minutes = d.getHours() * 60 + d.getMinutes(); // 早于早上上班时间 → 改为上班时间 if (minutes < morningPeriod.start) { d.setHours(Math.floor(morningPeriod.start / 60), morningPeriod.start % 60, 0, 0); return d; } // 在上午工作时间内 → 保持不变 if (minutes >= morningPeriod.start && minutes < morningPeriod.end) { return d; } // 在中午休息时间 → 加上午休间隔时间 if (minutes >= morningPeriod.end && minutes < afternoonPeriod.start) { var lunchBreakMinutes = afternoonPeriod.start - morningPeriod.end; var newMinutes = minutes + lunchBreakMinutes; d.setHours(Math.floor(newMinutes / 60), newMinutes % 60, 0, 0); return d; } // 在下午工作时间内 → 保持不变 if (minutes >= afternoonPeriod.start && minutes < afternoonPeriod.end) { return d; } // 晚于下午下班时间 → 改为下班时间 if (minutes >= afternoonPeriod.end) { d.setHours(Math.floor(afternoonPeriod.end / 60), afternoonPeriod.end % 60, 0, 0); return d; } return d; } // __BUSINESS_API_CALLS_END__ // ============================================================ // BOOTSTRAP // ============================================================ boot(); })();