R34: 收口搜索查看更多跳转与采购 submit 二次减重

这个提交包含在:
2026-03-24 23:03:47 +08:00
父节点 509676b714
当前提交 0c7f765201
共修改 4 个文件,包含 115 行新增和 39 行删除
+52 -8
查看文件
@@ -1,7 +1,7 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { searchGlobal } from "../api/search";
import { searchGlobal, type GlobalSearchHit } from "../api/search";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { Button } from "./ui/button";
@@ -56,10 +56,46 @@ function resolveSearchGroup(entityType: string): SearchGroupKey {
return "other";
}
function resolveBrowseHref(groupKey: SearchGroupKey, items: GlobalSearchHit[]) {
const firstItem = items[0];
if (!firstItem) {
return null;
}
if (groupKey === "people") {
return "/people";
}
if (groupKey === "documents") {
if (firstItem.entityType === "reimbursement") return "/reimbursements";
if (firstItem.entityType === "leave") return "/leave-requests";
if (firstItem.entityType === "purchase_request") return "/purchase-requests";
if (firstItem.entityType === "travel_request") return "/travel-requests";
if (firstItem.entityType === "weekly_report") return "/weekly-reports";
if (firstItem.entityType === "todo") return "/todos";
return null;
}
if (groupKey === "projectAnnouncement") {
return firstItem.entityType === "announcement" ? "/announcements" : "/projects";
}
if (firstItem.entityType === "asset") return "/assets";
if (["contract_registry", "confidential_doc_ref"].includes(firstItem.entityType)) {
return "/contracts-registry";
}
return null;
}
export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const navigate = useNavigate();
const [keyword, setKeyword] = useState("");
const trimmed = keyword.trim();
const closeAndNavigate = (targetHref: string) => {
onOpenChange(false);
window.setTimeout(() => navigate(targetHref), 0);
};
const searchQuery = useQuery({
enabled: open && trimmed.length >= 2,
queryKey: ["global-search", trimmed],
@@ -74,6 +110,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
...group,
total: matches.length,
items: matches.slice(0, 3),
browseHref: resolveBrowseHref(group.key, matches),
};
}).filter((group) => group.total > 0);
}, [searchQuery.data?.items]);
@@ -93,7 +130,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
<DialogHeader>
<DialogTitle>全局搜索</DialogTitle>
</DialogHeader>
@@ -121,10 +158,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
key={`${item.entityType}-${item.entityId}`}
type="button"
className="w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"
onClick={() => {
onOpenChange(false);
navigate(item.href);
}}
onClick={() => closeAndNavigate(item.href)}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.title}</div>
@@ -136,8 +170,18 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
))}
</div>
{group.total > group.items.length ? (
<div className="text-xs text-zinc-400">查看更多结果</div>
{group.total > group.items.length && group.browseHref ? (
<button
type="button"
className="text-xs font-medium text-zinc-500 transition-colors hover:text-zinc-800"
onClick={() => {
const browseHref = group.browseHref;
if (!browseHref) return;
closeAndNavigate(browseHref);
}}
>
查看更多结果
</button>
) : null}
</section>
))}
+26 -2
查看文件
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect, type Page } from "@playwright/test";
import { ADMIN, login } from "./helpers/auth";
@@ -9,7 +9,7 @@ function envelope<T>(data: T) {
};
}
test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳转", async ({ page }) => {
async function mockSearchResults(page: Page) {
await page.route("**/api/search?**", async (route) => {
await route.fulfill({
status: 200,
@@ -30,6 +30,10 @@ test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳
),
});
});
}
test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳转", async ({ page }) => {
await mockSearchResults(page);
await login(page, ADMIN);
await page.goto("/portal");
@@ -56,6 +60,26 @@ test("E2E 全局搜索:按钮打开后按分组展示结果并支持点击跳
await expect(page).toHaveURL(/\/reimbursements\/reimbursement-1/);
});
test("E2E 全局搜索:查看更多结果会关闭弹窗并跳到分组列表页", async ({ page }) => {
await mockSearchResults(page);
await login(page, ADMIN);
await page.goto("/portal");
await page.getByTestId("global-search-trigger").click();
await expect(page.getByRole("heading", { name: "全局搜索" })).toBeVisible();
const searchResponse = page.waitForResponse((response) => response.url().includes("/api/search?"));
await page.getByPlaceholder("搜索人员、项目、合同、公告、资产、请假、报销、周报、待办").fill("海音");
await searchResponse;
const documentsSection = page.locator("section").filter({ has: page.getByRole("heading", { name: "业务单据" }) });
await expect(documentsSection.getByRole("button", { name: "查看更多结果" })).toBeVisible();
await documentsSection.getByRole("button", { name: "查看更多结果" }).click();
await expect(page).toHaveURL(/\/reimbursements$/);
await expect(page.getByRole("heading", { name: "全局搜索" })).not.toBeVisible();
});
test("E2E 全局搜索:Ctrl+K 和 Cmd+K 可打开搜索弹窗", async ({ page }) => {
await login(page, ADMIN);
await page.goto("/portal");