增加企微旧包刷新护栏(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 17:40:25 +08:00
父节点 39c3e808a3
当前提交 e123d50f86
共修改 9 个文件,包含 308 行新增和 1 行删除
@@ -0,0 +1,29 @@
import { Button } from "../ui/button";
import { usePwaUpdatePrompt } from "../../hooks/usePwaUpdatePrompt";
export function AppPwaUpdateNotice() {
const { updateReady, applyUpdate } = usePwaUpdatePrompt();
if (!updateReady) {
return null;
}
return (
<div
aria-label="系统更新"
className="mb-4 flex flex-col gap-3 rounded-xl border border-primary/20 bg-white px-4 py-3 text-sm text-slate-700 shadow-sm sm:flex-row sm:items-center sm:justify-between"
data-testid="app-pwa-update-notice"
role="region"
>
<div aria-live="polite">
<div className="font-semibold text-slate-900">发现新版本</div>
<div className="mt-1 leading-6">刷新后即可使用最新功能,当前页面不会自动重载。</div>
</div>
<div className="flex shrink-0 gap-2">
<Button className="min-w-24" type="button" onClick={applyUpdate}>
点击刷新
</Button>
</div>
</div>
);
}
+2
查看文件
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { AppPwaUpdateNotice } from "../components/common/AppPwaUpdateNotice";
import { ErrorBoundary } from "../components/common/ErrorBoundary";
import { MobileBottomNav } from "../components/mobile/MobileBottomNav";
import { Sidebar } from "../components/navigation/Sidebar";
@@ -70,6 +71,7 @@ export function AppShellLayout() {
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? `min-h-0 overscroll-contain overflow-y-auto px-[var(--mobile-page-x)] py-[var(--mobile-page-y)] ${showMobileBottomNav ? "pb-[calc(var(--mobile-bottom-nav-height,68px)+env(safe-area-inset-bottom,0px)+var(--mobile-page-y)+0.5rem)]" : "pb-5 sm:pb-6"}` : "sm:px-7 sm:py-7 lg:px-8"}`}>
<div className="layout-container max-w-none">
<AppPwaUpdateNotice />
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
+2 -1
查看文件
@@ -6,7 +6,7 @@ import { ErrorBoundary } from "./components/common/ErrorBoundary";
import { Toaster } from "./components/ui/sonner";
import { queryClient } from "./query/queryClient";
import { appRouter } from "./router";
import { clearPwaRuntimeCaches, registerPwaIfEnabled } from "./pwa";
import { clearPwaRuntimeCaches, installBuildFreshnessMonitor, registerPwaIfEnabled } from "./pwa";
import "./index.css";
document.documentElement.dataset.buildSha = String(import.meta.env.VITE_BUILD_SHA || "development").slice(0, 12);
@@ -107,6 +107,7 @@ function installChunkLoadRecovery() {
}
installChunkLoadRecovery();
installBuildFreshnessMonitor();
registerPwaIfEnabled();
ReactDOM.createRoot(document.getElementById("root")!).render(
+85
查看文件
@@ -6,6 +6,15 @@ 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 AUTH_INPUT_PATHS = ["/login", "/change-password", "/forgot-password", "/reset-password"];
const BUILD_SHA_DISPLAY_LENGTH = 12;
type ReadyResponse = {
data?: {
buildSha?: unknown;
};
};
let buildFreshnessMonitorInstalled = false;
function isAuthInputPath(pathname: string) {
return AUTH_INPUT_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
@@ -34,6 +43,82 @@ function bindServiceWorkerUpdateCleanup(registration: ServiceWorkerRegistration
});
}
function normalizeBuildSha(value: unknown) {
const normalized = String(value ?? "").trim().toLowerCase();
if (
!normalized ||
normalized === "unknown" ||
normalized === "development" ||
!/^[a-f0-9]{7,64}$/.test(normalized)
) {
return null;
}
return normalized.slice(0, BUILD_SHA_DISPLAY_LENGTH);
}
function buildUserControlledRefresh() {
return () => {
void clearPwaRuntimeCaches()
.catch(() => undefined)
.finally(() => {
window.location.reload();
});
};
}
export function installBuildFreshnessMonitor() {
if (!canUseWindow() || buildFreshnessMonitorInstalled) {
return;
}
buildFreshnessMonitorInstalled = true;
let checkInFlight = false;
let mismatchNotified = false;
const checkBuildFreshness = async () => {
if (document.visibilityState !== "visible" || checkInFlight || mismatchNotified) {
return;
}
const clientBuildSha = normalizeBuildSha(document.documentElement.dataset.buildSha);
if (!clientBuildSha) {
return;
}
checkInFlight = true;
try {
const response = await window.fetch("/ready", {
cache: "no-store",
credentials: "same-origin",
headers: { accept: "application/json" },
});
if (!response.ok) {
return;
}
const payload = await response.json() as ReadyResponse;
const serverBuildSha = normalizeBuildSha(payload.data?.buildSha);
if (!serverBuildSha || serverBuildSha === clientBuildSha) {
return;
}
mismatchNotified = true;
window.dispatchEvent(new CustomEvent(PWA_UPDATE_READY_EVENT, {
detail: { applyUpdate: buildUserControlledRefresh() },
}));
} catch {
} finally {
checkInFlight = false;
}
};
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
void checkBuildFreshness();
}
});
}
export function registerPwaIfEnabled() {
if (import.meta.env.DEV) {
return;
+83
查看文件
@@ -551,6 +551,89 @@ test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page
await expect(page.getByText("试点观测摘要")).not.toBeVisible();
});
async function prepareEmployeePortal(page: import("@playwright/test").Page) {
await mockPasswordLogin(page);
await mockNavigation(page, "employee");
await mockPortalHome(page, "employee");
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "陈工程");
await login(page, EMPLOYEE);
}
async function triggerVisibleBuildCheck(page: import("@playwright/test").Page, clientBuildSha: string) {
const readyRequest = page.waitForRequest((request) => new URL(request.url()).pathname === "/ready");
await page.evaluate((buildSha) => {
document.documentElement.dataset.buildSha = buildSha;
document.dispatchEvent(new Event("visibilitychange"));
}, clientBuildSha);
await readyRequest;
}
test("已登录页面回到前台时发现旧构建并提示用户刷新", async ({ page }) => {
let readyChecks = 0;
await page.route("**/ready", async (route) => {
readyChecks += 1;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(envelope({ ok: true, buildSha: "222222222222" })),
});
});
await prepareEmployeePortal(page);
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/mobile");
await expect(page.getByTestId("mobile-today-focus")).toBeVisible();
await triggerVisibleBuildCheck(page, "111111111111");
const notice = page.getByTestId("app-pwa-update-notice");
await expect(notice).toBeVisible();
await expect(notice).toContainText("发现新版本");
const refreshButton = notice.getByRole("button", { name: "点击刷新" });
await expect(refreshButton).toBeVisible();
await page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
await expect.poll(() => readyChecks).toBe(1);
const refreshButtonBox = await refreshButton.boundingBox();
expect(refreshButtonBox?.height ?? 0).toBeGreaterThanOrEqual(44);
expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(2);
await page.setViewportSize({ width: 667, height: 375 });
await expect(notice).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(2);
await Promise.all([
page.waitForNavigation({ waitUntil: "domcontentloaded" }),
refreshButton.click(),
]);
await expect(page).toHaveURL(/\/mobile$/);
});
test("已登录页面回到前台时构建一致保持静默", async ({ page }) => {
await page.route("**/ready", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(envelope({ ok: true, buildSha: "333333333333" })),
});
});
await prepareEmployeePortal(page);
await triggerVisibleBuildCheck(page, "333333333333");
await expect(page.getByTestId("app-pwa-update-notice")).toHaveCount(0);
});
test("已登录页面回到前台时版本检查失败保持静默", async ({ page }) => {
await page.route("**/ready", async (route) => {
await route.fulfill({ status: 503, contentType: "application/json", body: "{}" });
});
await prepareEmployeePortal(page);
await triggerVisibleBuildCheck(page, "444444444444");
await expect(page.getByTestId("app-pwa-update-notice")).toHaveCount(0);
});
test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清单", async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("hyb_oa_show_welcome", "1");