Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/hooks/UseInitialLoaded.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, expect, it } from "vitest";
import { useInitialLoaded } from "./UseInitialLoaded.ts";

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

describe("useInitialLoaded", () => {
it("stays false until the first load finishes, then remains true on refreshes", async () => {
let result = false;
function Host({ loading }: { loading: boolean }) {
result = useInitialLoaded(loading);
return null;
}

const root = createRoot(document.createElement("div"));
try {
await act(async () => root.render(<Host loading={true} />));
expect(result).toBe(false);

await act(async () => root.render(<Host loading={false} />));
expect(result).toBe(true);

await act(async () => root.render(<Host loading={true} />));
expect(result).toBe(true);
} finally {
await act(async () => root.unmount());
}
});
});
19 changes: 19 additions & 0 deletions src/hooks/UseInitialLoaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from "react";

/**
* 区分「首次加载」与「筛选/翻页等后续刷新」。
*
* 首次 loading 结束后返回 true,之后保持 true,页面可据此只在首次加载时
* 展示整页 Loading,后续刷新只更新列表区域。
*/
export function useInitialLoaded(loading: boolean): boolean {
const [initialLoaded, setInitialLoaded] = useState(!loading);

useEffect(() => {
if (!loading) {
setInitialLoaded(true);
}
}, [loading]);

return initialLoaded;
}
2 changes: 2 additions & 0 deletions src/hooks/UseRdList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ describe("R&D list request lifecycle", () => {
const root = createRoot(document.createElement("div"));
try {
await act(async () => root.render(<Host page={1} />));
expect(state.initialLoaded).toBe(false);
await act(async () => root.render(<Host page={2} />));
expect(load).toHaveBeenLastCalledWith({ search: "", page: 2, pageSize: 10, organizationIds: ["7"] });
await act(async () => pending[1].resolve({ data: ["new"], total: 11 }));
await act(async () => pending[0].resolve({ data: ["old"], total: 1 }));
expect(state.data).toEqual(["new"]);
expect(state.total).toBe(11);
expect(state.loading).toBe(false);
expect(state.initialLoaded).toBe(true);
await act(async () => {
void state.refresh();
});
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/UseRdList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import message from "../components/message/Message";
import i18n from "@/i18n";
import { useInitialLoaded } from "./UseInitialLoaded";

interface PageQuery {
page: number;
Expand All @@ -19,6 +20,7 @@ export function useRdList<T, F extends object>(
const [data, setData] = useState<T[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const requestId = useRef(0);

const refresh = useCallback(async () => {
Expand Down Expand Up @@ -49,5 +51,5 @@ export function useRdList<T, F extends object>(
};
}, [refresh]);

return { data, total, loading, refresh };
return { data, total, loading, initialLoaded, refresh };
}
12 changes: 10 additions & 2 deletions src/pages/admin/ArticleManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { confirm } from "@/components/confirm/Confirm";
import type { SelectOption } from "@/types";
import { PlatformRoleByKey, type PlatformRole } from "@/types/Roles.ts";
import { useEnumLabels } from "@/i18n/UseEnumLabels";
import { useInitialLoaded } from "@/hooks/UseInitialLoaded";
import styles from "./ArticleManagement.module.css";
import ArticleService, { type ListAdminArticlesResponse, type ArticleStatsResponse } from "@/services/ArticleService.ts";
import { CategoryService } from "@/services/CategoryService.ts";
Expand Down Expand Up @@ -123,6 +124,7 @@ const ArticleManagement: React.FC = () => {
});
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const [categoryOptions, setCategoryOptions] = useState<SelectOption[]>([
{ id: "", name: t("articles.filter.allCategories"), color: "#6c757d" },
]);
Expand Down Expand Up @@ -572,7 +574,7 @@ const ArticleManagement: React.FC = () => {
return pages;
};

if (loading) {
if (loading && !initialLoaded) {
return (
<div className={styles.articleManagement}>
<Loading text={t("articles.loading")} size="large" />
Expand Down Expand Up @@ -734,7 +736,13 @@ const ArticleManagement: React.FC = () => {
</tr>
</thead>
<tbody>
{currentArticles.length > 0 ? (
{loading ? (
<tr>
<td colSpan={7} style={{ textAlign: "center", padding: "40px" }}>
<Loading size="small" />
</td>
</tr>
) : currentArticles.length > 0 ? (
currentArticles.map((article) => (
<tr key={article.id} className={`${article.featured ? styles.featured : ""}`}>
<td>
Expand Down
12 changes: 10 additions & 2 deletions src/pages/admin/AssignmentManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Modal from "@/components/modal/Modal";
import styles from "./AssignmentManagement.module.css";
import AssignmentService, { type AssignmentStatsResponse } from "@/services/AssignmentService.ts";
import { AssignmentStatus, AssignmentPriority } from "@/types/Enums.ts";
import { useInitialLoaded } from "@/hooks/UseInitialLoaded";

// 模拟任务数据接口
interface Assignment {
Expand Down Expand Up @@ -76,6 +77,7 @@ const PRIORITY_NUMBER_MAP_STR: Record<number, string> = {
const AssignmentManagement: React.FC = () => {
const { t } = useTranslation("admin");
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const [assignments, setAssignments] = useState<Assignment[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
Expand Down Expand Up @@ -370,7 +372,7 @@ const AssignmentManagement: React.FC = () => {
}
};

if (loading) {
if (loading && !initialLoaded) {
return <Loading />;
}

Expand Down Expand Up @@ -492,7 +494,13 @@ const AssignmentManagement: React.FC = () => {
</tr>
</thead>
<tbody>
{currentData.length > 0 ? (
{loading ? (
<tr>
<td colSpan={10} style={{ textAlign: "center", padding: "40px" }}>
<Loading size="small" />
</td>
</tr>
) : currentData.length > 0 ? (
currentData.map((item) => (
<tr key={item.id}>
<td>
Expand Down
10 changes: 8 additions & 2 deletions src/pages/admin/CommentManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { AdminComment } from "@/types/Comment.ts";
import type { SelectOption } from "@/types";
import type { CommentStatus } from "@/types/Enums.ts";
import { useEnumLabels } from "@/i18n/UseEnumLabels";
import { useInitialLoaded } from "@/hooks/UseInitialLoaded";
import styles from "./CommentManagement.module.css";

interface FilterOptions {
Expand All @@ -54,6 +55,7 @@ const CommentManagement: React.FC = () => {
});
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const [selectedComments, setSelectedComments] = useState<string[]>([]);
const [showReplies, setShowReplies] = useState<string[]>([]);

Expand Down Expand Up @@ -273,7 +275,7 @@ const CommentManagement: React.FC = () => {
return pages;
};

if (loading) {
if (loading && !initialLoaded) {
return (
<div className={styles.commentManagement}>
<Loading text={t("comments.loading")} size="large" />
Expand Down Expand Up @@ -393,7 +395,11 @@ const CommentManagement: React.FC = () => {

{/* 评论列表 */}
<div className={styles.commentsContainer}>
{comments.length > 0 ? (
{loading ? (
<div style={{ textAlign: "center", padding: "40px" }}>
<Loading size="small" />
</div>
) : comments.length > 0 ? (
comments.map((comment) => {
const replies = getReplies(comment.id);
const hasReplies = replies.length > 0;
Expand Down
12 changes: 10 additions & 2 deletions src/pages/admin/OrganizationManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Modal from "@/components/modal/Modal";
import CustomSelect from "@/components/customSelect/CustomSelect";
import styles from "./OrganizationManagement.module.css";
import OrganizationService, { type OrganizationStatsResponse, type ApplyItem } from "@/services/OrganizationService.ts";
import { useInitialLoaded } from "@/hooks/UseInitialLoaded";

interface Organization {
id: string;
Expand All @@ -39,6 +40,7 @@ interface Organization {
const OrganizationManagement: React.FC = () => {
const { t } = useTranslation("admin");
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
Expand Down Expand Up @@ -326,7 +328,7 @@ const OrganizationManagement: React.FC = () => {
}
};

if (loading && activeTab === "orgs") {
if (loading && !initialLoaded && activeTab === "orgs") {
return <Loading />;
}

Expand Down Expand Up @@ -488,7 +490,13 @@ const OrganizationManagement: React.FC = () => {
</tr>
</thead>
<tbody>
{currentData.length > 0 ? (
{loading ? (
<tr>
<td colSpan={6} style={{ textAlign: "center", padding: "40px" }}>
<Loading size="small" />
</td>
</tr>
) : currentData.length > 0 ? (
currentData.map((item) => (
<tr key={item.id}>
<td>
Expand Down
12 changes: 10 additions & 2 deletions src/pages/admin/UserManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import message from "@/components/message/Message";
import type { SelectOption } from "@/types";
import { PlatformRoleCSS, PlatformRoleByKey, type PlatformRole } from "@/types/Roles.ts";
import { useEnumLabels } from "@/i18n/UseEnumLabels";
import { useInitialLoaded } from "@/hooks/UseInitialLoaded";
import styles from "./UserManagement.module.css";
import { AuthService } from "@/services/AuthService.ts";
import { formatToChinaTime } from "@/utils/Utils.ts";
Expand Down Expand Up @@ -76,6 +77,7 @@ const UserManagement: React.FC = () => {
});
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(true);
const initialLoaded = useInitialLoaded(loading);
const [totalUsers, setTotalUsers] = useState(0);
const [isModalVisible, setIsModalVisible] = useState(false);
const [submitting, setSubmitting] = useState(false);
Expand Down Expand Up @@ -422,7 +424,7 @@ const UserManagement: React.FC = () => {
return pages;
};

if (loading) {
if (loading && !initialLoaded) {
return (
<div className={styles.userManagement}>
<Loading text={t("users.loading")} size="large" />
Expand Down Expand Up @@ -566,7 +568,13 @@ const UserManagement: React.FC = () => {
</tr>
</thead>
<tbody>
{currentUsers.length > 0 ? (
{loading ? (
<tr>
<td colSpan={8} style={{ textAlign: "center", padding: "40px" }}>
<Loading size="small" />
</td>
</tr>
) : currentUsers.length > 0 ? (
currentUsers.map((user) => (
<tr key={user.id}>
<td>
Expand Down
Loading
Loading