-
-
- {Array.from({ length: 8 }).map((_, i) => (
-
- ))}
-
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
)
}
diff --git a/src/features/books/hooks.ts b/src/features/books/hooks.ts
index 0001394..061a339 100644
--- a/src/features/books/hooks.ts
+++ b/src/features/books/hooks.ts
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { getAllBooks } from "@/api/books"
+import { getAllBooks, searchBook } from "@/api/books"
import { borrowBookForMe } from "@/api/borrows"
import { toast } from "sonner"
import { getErrorMessage } from "@/lib/errors"
@@ -12,6 +12,14 @@ export function useBooks() {
})
}
+export function useSearchBooks(query: string) {
+ return useQuery({
+ queryKey: ["books", "search", query],
+ queryFn: () => searchBook(query),
+ enabled: query.length > 0,
+ })
+}
+
export function useBorrowBook() {
const qc = useQueryClient()
return useMutation({
diff --git a/src/features/borrows/MyBorrowsPage.tsx b/src/features/borrows/MyBorrowsPage.tsx
index d20e04e..a5b7be2 100644
--- a/src/features/borrows/MyBorrowsPage.tsx
+++ b/src/features/borrows/MyBorrowsPage.tsx
@@ -1,131 +1,172 @@
-import { Loader2Icon } from "lucide-react"
+import { useState, type FormEvent } from "react"
+import { Loader2Icon, SearchIcon, XIcon } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
-import { useMyBorrows, useReturnBook } from "./hooks"
+import { useMyBorrows, useSearchMyBorrows, useReturnBook } from "./hooks"
import { formatDateTime } from "@/lib/formatters"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
import { getErrorMessage } from "@/lib/errors"
export default function MyBorrowsPage() {
- const { data: records, isLoading, error } = useMyBorrows()
const returnBook = useReturnBook()
const platform = usePlatform()
const isMobile = useIsMobile()
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
- if (isLoading) return
+ const [searchText, setSearchText] = useState("")
+ const [submitted, setSubmitted] = useState("")
- if (error) {
- return (
-
-
- 加载失败:{getErrorMessage(error)}
-
-
- )
+ const isSearching = submitted.length > 0
+ const allQuery = useMyBorrows()
+ const searchQuery = useSearchMyBorrows(submitted)
+ const { data, isLoading, error } = isSearching ? searchQuery : allQuery
+
+ function doSearch(e: FormEvent) {
+ e.preventDefault()
+ setSubmitted(searchText.trim())
}
- const list = records ?? []
-
- if (list.length === 0) {
- return (
-
-
暂无借阅记录
-
去书目页面借阅你感兴趣的图书吧
-
- )
+ function clearSearch() {
+ setSearchText("")
+ setSubmitted("")
}
- if (!isMobile) {
+ const list = data ?? []
+
+ const searchBar = (
+
+ )
+
+ const content = isLoading ? (
+
+ ) : error ? (
+
+ 加载失败:{getErrorMessage(error)}
+
+ ) : list.length === 0 ? (
+
+
{isSearching ? "无匹配记录" : "暂无借阅记录"}
+ {!isSearching &&
去书目页面借阅你感兴趣的图书吧
}
+
+ ) : !isMobile ? (
+
+
+
+ 书名
+ 作者
+ 借阅时间
+ 归还时间
+ 状态
+ 操作
+
+
+
+ {list.map((r) => (
+
+ {r.bookBorrowVo?.name ?? "-"}
+ {r.bookBorrowVo?.author ?? "-"}
+ {formatDateTime(r.borrowTime)}
+
+ {r.returnTime ? formatDateTime(r.returnTime) : "-"}
+
+
+
+ {r.status === "BORROWED" ? "借阅中" : "已归还"}
+
+
+
+ {r.status === "BORROWED" && (
+
+ )}
+
+
+ ))}
+
+
+ ) : (
+
+ {list.map((r) => (
+
+
+
+ #{r.id}
+
+ {r.status === "BORROWED" ? "借阅中" : "已归还"}
+
+
+ {r.bookBorrowVo?.name ?? "-"}
+ {r.bookBorrowVo?.author ?? "-"}
+
+ 借阅:{formatDateTime(r.borrowTime)}
+ {r.returnTime && ` | 归还:${formatDateTime(r.returnTime)}`}
+
+ {r.status === "BORROWED" && (
+
+
+
+ )}
+
+
+ ))}
+
+ )
+
+ if (isMobile) {
return (
-
+
我的借阅
-
-
-
- 书名
- 作者
- 借阅时间
- 归还时间
- 状态
- 操作
-
-
-
- {list.map((r) => (
-
- {r.bookBorrowVo?.name ?? "-"}
- {r.bookBorrowVo?.author ?? "-"}
- {formatDateTime(r.borrowTime)}
-
- {r.returnTime ? formatDateTime(r.returnTime) : "-"}
-
-
-
- {r.status === "BORROWED" ? "借阅中" : "已归还"}
-
-
-
- {r.status === "BORROWED" && (
-
- )}
-
-
- ))}
-
-
+ {searchBar}
+ {content}
)
}
return (
-
+
我的借阅
-
- {list.map((r) => (
-
-
-
- #{r.id}
-
- {r.status === "BORROWED" ? "借阅中" : "已归还"}
-
-
- {r.bookBorrowVo?.name ?? "-"}
- {r.bookBorrowVo?.author ?? "-"}
-
- 借阅:{formatDateTime(r.borrowTime)}
- {r.returnTime && ` | 归还:${formatDateTime(r.returnTime)}`}
-
- {r.status === "BORROWED" && (
-
-
-
- )}
-
-
- ))}
-
+ {searchBar}
+ {content}
)
}
@@ -133,8 +174,7 @@ export default function MyBorrowsPage() {
function MyBorrowsSkeleton({ isMobile }: { isMobile: boolean }) {
if (isMobile) {
return (
-
-
+
{Array.from({ length: 5 }).map((_, i) => (
@@ -150,13 +190,10 @@ function MyBorrowsSkeleton({ isMobile }: { isMobile: boolean }) {
}
return (
-
-
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
)
}
diff --git a/src/features/borrows/hooks.ts b/src/features/borrows/hooks.ts
index 5be11d0..74b5172 100644
--- a/src/features/borrows/hooks.ts
+++ b/src/features/borrows/hooks.ts
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { getAllMyBorrows, returnBookForMe } from "@/api/borrows"
+import { getAllMyBorrows, searchMyBorrows, returnBookForMe } from "@/api/borrows"
import { toast } from "sonner"
import { getErrorMessage } from "@/lib/errors"
@@ -10,6 +10,14 @@ export function useMyBorrows() {
})
}
+export function useSearchMyBorrows(query: string) {
+ return useQuery({
+ queryKey: ["myBorrows", "search", query],
+ queryFn: () => searchMyBorrows(query),
+ enabled: query.length > 0,
+ })
+}
+
export function useReturnBook() {
const qc = useQueryClient()
return useMutation({
diff --git a/src/hooks/useIsMobile.ts b/src/hooks/useIsMobile.ts
index 6b1d55f..4ae8d01 100644
--- a/src/hooks/useIsMobile.ts
+++ b/src/hooks/useIsMobile.ts
@@ -16,3 +16,27 @@ export function useIsMobile(): boolean {
return mobile
}
+
+export function MobileClass() {
+ useEffect(() => {
+ const root = document.documentElement
+ const mql = window.matchMedia(BP)
+
+ function sync() {
+ if (mql.matches) {
+ root.classList.add("mobile")
+ } else {
+ root.classList.remove("mobile")
+ }
+ }
+
+ sync()
+ mql.addEventListener("change", sync)
+ return () => {
+ mql.removeEventListener("change", sync)
+ root.classList.remove("mobile")
+ }
+ }, [])
+
+ return null
+}
diff --git a/src/hooks/usePlatform.tsx b/src/hooks/usePlatform.tsx
index ab55f0e..cb898b6 100644
--- a/src/hooks/usePlatform.tsx
+++ b/src/hooks/usePlatform.tsx
@@ -4,18 +4,31 @@ export type Platform = "web" | "desktop" | "mobile"
const PlatformContext = createContext
("web")
+function isTauri(): boolean {
+ return !!(window as unknown as Record).__TAURI_INTERNALS__
+}
+
+// Default to safe: add no-select immediately, remove only if definitely web
+document.documentElement.classList.add("no-select")
+if (!isTauri()) {
+ document.documentElement.classList.remove("no-select")
+}
+
export function PlatformProvider({ children }: { children: ReactNode }) {
- const [platform, setPlatform] = useState("web")
+ const [platform, setPlatform] = useState(isTauri() ? "desktop" : "web")
useEffect(() => {
+ if (!isTauri()) return
let cancelled = false
;(async () => {
- const p = await detectPlatform()
- if (!cancelled) {
- setPlatform(p)
- if (p !== "web") {
- document.documentElement.classList.add("no-select")
+ try {
+ const { platform: osPlatform } = await import("@tauri-apps/plugin-os")
+ const p = osPlatform()
+ if (!cancelled) {
+ setPlatform(p === "android" || p === "ios" ? "mobile" : "desktop")
}
+ } catch {
+ // stays "desktop"
}
})()
return () => {
@@ -33,16 +46,3 @@ export function PlatformProvider({ children }: { children: ReactNode }) {
export function usePlatform(): Platform {
return useContext(PlatformContext)
}
-
-async function detectPlatform(): Promise {
- const w = window as unknown as Record
- if (!w.__TAURI_INTERNALS__) return "web"
-
- try {
- const { platform } = await import("@tauri-apps/plugin-os")
- const p = platform()
- return p === "android" || p === "ios" ? "mobile" : "desktop"
- } catch {
- return "desktop"
- }
-}
diff --git a/src/index.css b/src/index.css
index 17b76d0..0ad9218 100644
--- a/src/index.css
+++ b/src/index.css
@@ -129,12 +129,22 @@
}
}
-.no-select * {
- user-select: none;
+html.mobile {
+ --safe-top: env(safe-area-inset-top, 0px);
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
}
-.no-select input,
-.no-select textarea,
-.no-select [data-selectable] {
- user-select: text;
+html.mobile body {
+ padding-top: var(--safe-top);
+ padding-bottom: var(--safe-bottom);
+}
+
+html.no-select * {
+ user-select: none !important;
+}
+
+html.no-select input,
+html.no-select textarea,
+html.no-select [data-selectable] {
+ user-select: text !important;
}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index 0191160..33ea2f8 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,6 +4,7 @@ import { RouterProvider } from "react-router-dom"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Toaster } from "@/components/ui/sonner"
import { PlatformProvider } from "@/hooks/usePlatform"
+import { MobileClass } from "@/hooks/useIsMobile"
import { router } from "@/router"
import "./index.css"
@@ -12,6 +13,7 @@ const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+