完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
+55
-54
@@ -1,10 +1,10 @@
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
import { canUseWindow, isEnterpriseMessengerContainer, matchesMedia } from "./lib/runtimeContext";
|
||||
import { PWA_UPDATE_READY_EVENT } from "./lib/pwaUpdateEvents";
|
||||
|
||||
const MOBILE_APPROVAL_TASK_PATH = /^\/workflow\/tasks\/([^/]+)$/i;
|
||||
const OFFLINE_ALLOWED_PATHS = ["/mobile/approvals"];
|
||||
const CLEARABLE_CACHE_PREFIXES = ["app-api-runtime", "app-static-runtime", "workbox-precache"];
|
||||
const RUNTIME_CACHE_PREFIXES = ["app-api-runtime", "app-static-runtime"];
|
||||
const CLEARABLE_CACHE_PREFIXES = [...RUNTIME_CACHE_PREFIXES, "workbox-precache"];
|
||||
const AUTH_INPUT_PATHS = ["/login", "/change-password", "/forgot-password", "/reset-password"];
|
||||
const BUILD_SHA_DISPLAY_LENGTH = 12;
|
||||
|
||||
@@ -15,31 +15,22 @@ type ReadyResponse = {
|
||||
};
|
||||
|
||||
let buildFreshnessMonitorInstalled = false;
|
||||
let pwaRegistrationStarted = false;
|
||||
|
||||
function isAuthInputPath(pathname: string) {
|
||||
return AUTH_INPUT_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
|
||||
}
|
||||
|
||||
function bindServiceWorkerUpdateCleanup(registration: ServiceWorkerRegistration | undefined) {
|
||||
if (!canUseWindow() || !registration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const watchInstallingWorker = (worker: ServiceWorker | null) => {
|
||||
if (!worker) {
|
||||
return;
|
||||
}
|
||||
|
||||
worker.addEventListener("statechange", () => {
|
||||
if (worker.state === "installed" && window.navigator.serviceWorker.controller) {
|
||||
void clearPwaRuntimeCaches();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watchInstallingWorker(registration.installing);
|
||||
registration.addEventListener("updatefound", () => {
|
||||
watchInstallingWorker(registration.installing);
|
||||
function waitForActivatedWorker(worker: ServiceWorker): Promise<boolean> {
|
||||
if (worker.state === "activated") return Promise.resolve(true);
|
||||
if (worker.state === "redundant") return Promise.resolve(false);
|
||||
return new Promise((resolve) => {
|
||||
const onStateChange = () => {
|
||||
if (worker.state !== "activated" && worker.state !== "redundant") return;
|
||||
worker.removeEventListener("statechange", onStateChange);
|
||||
resolve(worker.state === "activated");
|
||||
};
|
||||
worker.addEventListener("statechange", onStateChange);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,43 +111,49 @@ export function installBuildFreshnessMonitor() {
|
||||
}
|
||||
|
||||
export function registerPwaIfEnabled() {
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV || import.meta.env.VITE_PWA_ENABLED === "false" ||
|
||||
!canUseWindow() || !("serviceWorker" in window.navigator) || pwaRegistrationStarted) {
|
||||
return;
|
||||
}
|
||||
pwaRegistrationStarted = true;
|
||||
const serviceWorker = window.navigator.serviceWorker;
|
||||
let previousController = serviceWorker.controller;
|
||||
let refreshScheduled = false;
|
||||
let refreshApplied = false;
|
||||
|
||||
if (import.meta.env.VITE_PWA_ENABLED === "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
let updateServiceWorker: ((reloadPage?: boolean) => Promise<void>) | null = null;
|
||||
|
||||
updateServiceWorker = registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh() {
|
||||
const applyUpdate = () => {
|
||||
void clearPwaRuntimeCaches()
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
void updateServiceWorker?.(true);
|
||||
});
|
||||
};
|
||||
|
||||
if (canUseWindow() && isAuthInputPath(window.location.pathname)) {
|
||||
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } }));
|
||||
// Native navigation may start an update before register() resolves. Subscribe
|
||||
// first: updatefound/activated listeners installed afterward can miss it.
|
||||
serviceWorker.addEventListener("controllerchange", () => {
|
||||
const controller = serviceWorker.controller;
|
||||
if (!controller || controller === previousController) return;
|
||||
const isUpdate = previousController !== null;
|
||||
previousController = controller;
|
||||
void waitForActivatedWorker(controller).then((activated) => {
|
||||
if (!activated || controller !== serviceWorker.controller) return;
|
||||
if (!isUpdate) {
|
||||
window.dispatchEvent(new CustomEvent("hyb-oa:pwa-offline-ready"));
|
||||
return;
|
||||
}
|
||||
|
||||
applyUpdate();
|
||||
},
|
||||
onOfflineReady() {
|
||||
if (canUseWindow()) {
|
||||
window.dispatchEvent(new CustomEvent("hyb-oa:pwa-offline-ready"));
|
||||
if (refreshScheduled) return;
|
||||
refreshScheduled = true;
|
||||
const applyUpdate = () => {
|
||||
if (refreshApplied) return;
|
||||
refreshApplied = true;
|
||||
// Workbox owns new precache installation and old-revision cleanup.
|
||||
// Deleting it here races activation and destroys the new offline shell.
|
||||
void clearPwaCaches(RUNTIME_CACHE_PREFIXES)
|
||||
.catch(() => undefined)
|
||||
.finally(() => window.location.reload());
|
||||
};
|
||||
if (isAuthInputPath(window.location.pathname)) {
|
||||
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, { detail: { applyUpdate } }));
|
||||
} else {
|
||||
applyUpdate();
|
||||
}
|
||||
},
|
||||
onRegisteredSW(_swScriptUrl, registration) {
|
||||
bindServiceWorkerUpdateCleanup(registration);
|
||||
},
|
||||
});
|
||||
});
|
||||
void serviceWorker.register(`${import.meta.env.BASE_URL}sw.js`, { scope: import.meta.env.BASE_URL })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
export function buildMobileApprovalsPath(taskId?: string | null) {
|
||||
@@ -196,14 +193,14 @@ export function shouldPreferMobileApprovalExperience() {
|
||||
return isStandalone || isEnterpriseMessengerContainer() || isTouchSmallScreen;
|
||||
}
|
||||
|
||||
export async function clearPwaRuntimeCaches() {
|
||||
async function clearPwaCaches(prefixes: readonly string[]) {
|
||||
if (!canUseWindow() || !("caches" in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheNames = await window.caches.keys();
|
||||
const cachesToDelete = cacheNames.filter((cacheName) =>
|
||||
CLEARABLE_CACHE_PREFIXES.some((prefix) => cacheName.startsWith(prefix)),
|
||||
prefixes.some((prefix) => cacheName.startsWith(prefix)),
|
||||
);
|
||||
|
||||
await Promise.all(cachesToDelete.map(async (cacheName) => {
|
||||
@@ -213,3 +210,7 @@ export async function clearPwaRuntimeCaches() {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export async function clearPwaRuntimeCaches() {
|
||||
await clearPwaCaches(CLEARABLE_CACHE_PREFIXES);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户