feat(08-05-business-detail-routing): 完成采购与资产独立路由

这个提交包含在:
2026-08-07 11:00:56 +08:00
父节点 969b426e64
当前提交 1118bfadd7
共修改 24 个文件,包含 891 行新增和 196 行删除
+200
查看文件
@@ -0,0 +1,200 @@
import type { PurchaseRequestStatus } from "../api/purchaseRequests";
import type { AssetSensitivityLevel, AssetStatus, AssetType } from "../api/assets";
const PURCHASE_STATUS_VALUES = new Set<PurchaseRequestStatus>([
"draft",
"reviewing",
"approved",
"rejected",
"cancelled",
]);
const ASSET_STATUS_VALUES = new Set<AssetStatus>([
"in_stock",
"in_use",
"under_repair",
"retired",
"archived",
]);
const ASSET_TYPE_VALUES = new Set<AssetType>([
"fixed_asset",
"electronic_document",
"electronic_system",
"code_repository",
"backup_media",
]);
const ASSET_SENSITIVITY_VALUES = new Set<AssetSensitivityLevel>([
"public_candidate",
"internal",
"confidential",
"strictly_confidential",
]);
export const ASSET_CREATE_PATH = "/assets/new";
function normalizedEntityId(value: string) {
return value.trim();
}
function optionalSearchValue(searchParams: URLSearchParams, key: string) {
return (searchParams.get(key) ?? "").trim();
}
function positivePage(value: string | null) {
if (!value || !/^[1-9]\d*$/.test(value)) {
return 1;
}
const parsed = Number(value);
return Number.isSafeInteger(parsed) ? parsed : 1;
}
function appendOptional(target: URLSearchParams, key: string, value: string) {
if (value) {
target.set(key, value);
}
}
export function buildPurchaseRequestDetailPath(purchaseRequestId: string) {
const normalized = normalizedEntityId(purchaseRequestId);
return normalized ? `/purchase-requests/${encodeURIComponent(normalized)}` : "/purchase-requests";
}
export function buildAssetDetailPath(assetId: string) {
const normalized = normalizedEntityId(assetId);
return normalized ? `/assets/${encodeURIComponent(normalized)}` : "/assets";
}
export function readPurchaseListFilters(
searchParams: URLSearchParams,
canViewAll: boolean,
): {
status: PurchaseRequestStatus | "";
mine: boolean;
supplierId: string;
legalEntityId: string;
costCenterId: string;
page: number;
} {
const rawStatus = searchParams.get("status");
const status = rawStatus === "all"
? ""
: rawStatus && PURCHASE_STATUS_VALUES.has(rawStatus as PurchaseRequestStatus)
? rawStatus as PurchaseRequestStatus
: "draft";
const scope = searchParams.get("scope");
return {
status,
mine: !(canViewAll && scope === "all"),
supplierId: optionalSearchValue(searchParams, "supplierId"),
legalEntityId: optionalSearchValue(searchParams, "legalEntityId"),
costCenterId: optionalSearchValue(searchParams, "costCenterId"),
page: positivePage(searchParams.get("page")),
};
}
export function normalizePurchaseListSearchParams(searchParams: URLSearchParams) {
const normalized = new URLSearchParams();
const rawStatus = searchParams.get("status");
if (rawStatus === "all" || (rawStatus && PURCHASE_STATUS_VALUES.has(rawStatus as PurchaseRequestStatus))) {
normalized.set("status", rawStatus);
}
const rawScope = searchParams.get("scope");
if (rawScope === "own" || rawScope === "all") {
normalized.set("scope", rawScope);
}
appendOptional(normalized, "supplierId", optionalSearchValue(searchParams, "supplierId"));
appendOptional(normalized, "legalEntityId", optionalSearchValue(searchParams, "legalEntityId"));
appendOptional(normalized, "costCenterId", optionalSearchValue(searchParams, "costCenterId"));
const page = positivePage(searchParams.get("page"));
if (page > 1) {
normalized.set("page", String(page));
}
return normalized;
}
export function readAssetListFilters(
searchParams: URLSearchParams,
canViewAll: boolean,
): {
q: string;
status: AssetStatus | "";
assetType: AssetType | "";
sensitivity: AssetSensitivityLevel | "";
categoryId: string;
legalEntityId: string;
costCenterId: string;
assigneeId: string;
mine: boolean;
} {
const rawStatus = searchParams.get("status");
const rawAssetType = searchParams.get("assetType");
const rawSensitivity = searchParams.get("sensitivity");
const scope = searchParams.get("scope");
return {
q: searchParams.get("q") ?? "",
status: rawStatus && ASSET_STATUS_VALUES.has(rawStatus as AssetStatus) ? rawStatus as AssetStatus : "",
assetType: rawAssetType && ASSET_TYPE_VALUES.has(rawAssetType as AssetType) ? rawAssetType as AssetType : "",
sensitivity: rawSensitivity && ASSET_SENSITIVITY_VALUES.has(rawSensitivity as AssetSensitivityLevel)
? rawSensitivity as AssetSensitivityLevel
: "",
categoryId: optionalSearchValue(searchParams, "categoryId"),
legalEntityId: optionalSearchValue(searchParams, "legalEntityId"),
costCenterId: optionalSearchValue(searchParams, "costCenterId"),
assigneeId: optionalSearchValue(searchParams, "assigneeId"),
mine: scope === "own" || !(canViewAll && (scope === "all" || scope == null)),
};
}
export function normalizeAssetListSearchParams(searchParams: URLSearchParams) {
const normalized = new URLSearchParams();
appendOptional(normalized, "q", optionalSearchValue(searchParams, "q"));
const rawStatus = searchParams.get("status");
if (rawStatus && ASSET_STATUS_VALUES.has(rawStatus as AssetStatus)) {
normalized.set("status", rawStatus);
}
const rawAssetType = searchParams.get("assetType");
if (rawAssetType && ASSET_TYPE_VALUES.has(rawAssetType as AssetType)) {
normalized.set("assetType", rawAssetType);
}
const rawSensitivity = searchParams.get("sensitivity");
if (rawSensitivity && ASSET_SENSITIVITY_VALUES.has(rawSensitivity as AssetSensitivityLevel)) {
normalized.set("sensitivity", rawSensitivity);
}
appendOptional(normalized, "categoryId", optionalSearchValue(searchParams, "categoryId"));
appendOptional(normalized, "legalEntityId", optionalSearchValue(searchParams, "legalEntityId"));
appendOptional(normalized, "costCenterId", optionalSearchValue(searchParams, "costCenterId"));
appendOptional(normalized, "assigneeId", optionalSearchValue(searchParams, "assigneeId"));
const rawScope = searchParams.get("scope");
if (rawScope === "own" || rawScope === "all") {
normalized.set("scope", rawScope);
}
return normalized;
}
function listSource(pathname: string, searchParams: URLSearchParams) {
const search = searchParams.toString();
return search ? `${pathname}?${search}` : pathname;
}
export function resolvePurchaseRequestLegacyRedirect(searchParams: URLSearchParams) {
const purchaseRequestId = normalizedEntityId(searchParams.get("id") ?? "");
if (!purchaseRequestId) {
return null;
}
return {
to: buildPurchaseRequestDetailPath(purchaseRequestId),
from: listSource("/purchase-requests", normalizePurchaseListSearchParams(searchParams)),
};
}
export function resolveAssetLegacyRedirect(searchParams: URLSearchParams) {
const assetId = normalizedEntityId(searchParams.get("id") ?? "");
if (!assetId) {
return null;
}
return {
to: buildAssetDetailPath(assetId),
from: listSource("/assets", normalizeAssetListSearchParams(searchParams)),
};
}
+4 -2
查看文件
@@ -1,3 +1,5 @@
import { buildAssetDetailPath, buildPurchaseRequestDetailPath } from "./businessDetailRoutes";
type NotificationTarget = {
targetType: string | null;
targetId: string | null;
@@ -31,7 +33,7 @@ export function resolveNotificationTargetLink(item: NotificationTarget) {
}
if (item.targetType === "purchase_request") {
return `/purchase-requests?highlight=${item.targetId}`;
return buildPurchaseRequestDetailPath(item.targetId);
}
if (item.targetType === "travel_request") {
@@ -39,7 +41,7 @@ export function resolveNotificationTargetLink(item: NotificationTarget) {
}
if (item.targetType === "asset" || item.targetType === "asset_card") {
return `/assets?highlight=${item.targetId}`;
return buildAssetDetailPath(item.targetId);
}
if (item.targetType === "inventory_check") {
+17 -5
查看文件
@@ -2,22 +2,34 @@ export type PcDetailNavigationState = {
from?: string;
};
export type PcDetailSourcePolicy = "reimbursement" | "purchase-request" | "asset";
export function buildPcDetailSource(pathname: string, search: string) {
return `${pathname}${search}`;
}
export function resolvePcDetailSource(value: unknown, currentPath: string) {
export function resolvePcDetailSource(
value: unknown,
currentPath: string,
policy: PcDetailSourcePolicy = "reimbursement",
) {
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("#")) {
return null;
}
const url = new URL(value, "https://hyb-oa.invalid");
const isReimbursementList = url.pathname === "/reimbursements";
const isWorkflowTask = /^\/workflow\/tasks\/[^/]+$/.test(url.pathname);
if (!isReimbursementList && !isWorkflowTask) {
const baseUrl = new URL("https://hyb-oa.invalid");
const url = new URL(value, baseUrl);
if (url.origin !== baseUrl.origin) {
return null;
}
const allowed = policy === "purchase-request"
? url.pathname === "/purchase-requests" || /^\/projects\/[^/]+$/.test(url.pathname)
: policy === "asset"
? url.pathname === "/assets"
: url.pathname === "/reimbursements" || /^\/workflow\/tasks\/[^/]+$/.test(url.pathname);
if (!allowed) return null;
const source = `${url.pathname}${url.search}`;
return source === currentPath ? null : source;
}
+1 -1
查看文件
@@ -100,7 +100,7 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") {
return "/mobile/approvals";
}
if (normalizedPath === "/assets" || normalizedPath === "/inventory-checks") {
if (normalizedPath === "/assets" || normalizedPath.startsWith("/assets/") || normalizedPath === "/inventory-checks") {
return "/mobile/assets/scan";
}