Compare commits

..

No commits in common. "271f2e081e9aea49e43233fc0c51191d08861db2" and "89102d7f8c70e61f3dedcd5ebf552c4e54aa6300" have entirely different histories.

5 changed files with 147 additions and 246 deletions

View File

@ -10,9 +10,6 @@ import {
Input, Input,
Alert, Alert,
Pagination, Pagination,
Select,
Space,
Tag,
} from "antd"; } from "antd";
import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { SendOutlined, DeleteOutlined, LoginOutlined } from "@ant-design/icons"; import { SendOutlined, DeleteOutlined, LoginOutlined } from "@ant-design/icons";
@ -30,7 +27,6 @@ export function VideoContent() {
const [fileIds, setFileIds] = useState<string[]>([]); const [fileIds, setFileIds] = useState<string[]>([]);
const [uploaderKey, setUploaderKey] = useState<number>(0); const [uploaderKey, setUploaderKey] = useState<number>(0);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [fileType, setFileType] = useState<string>("all");
// 分页状态 // 分页状态
const [imagePage, setImagePage] = useState(1); const [imagePage, setImagePage] = useState(1);
@ -47,34 +43,18 @@ export function VideoContent() {
data: resources, data: resources,
refetch, refetch,
isLoading, isLoading,
}: { }: { data: ResourceDto[]; refetch: () => void; isLoading: boolean } =
data: ResourceDto[]; api.resource.findMany.useQuery({
refetch: () => void; where: {
isLoading: boolean; deletedAt: null,
} = api.resource.findMany.useQuery({ postId: null,
where: { },
deletedAt: null, orderBy: {
postId: null, createdAt: "desc",
}, },
orderBy: { });
createdAt: "desc",
},
});
// 定义常见文件类型和它们的扩展名 // 处理资源数据
const fileTypes = {
all: { label: "全部", extensions: [] },
document: { label: "文档", extensions: ["doc", "docx", "pdf", "txt"] },
spreadsheet: { label: "表格", extensions: ["xls", "xlsx", "csv"] },
presentation: { label: "ppt", extensions: ["ppt", "pptx"] },
video: {
label: "音视频",
extensions: ["mp4", "avi", "mov", "webm", "mp3", "wav", "ogg"],
},
archive: { label: "压缩包", extensions: ["zip", "rar", "7z"] },
};
// 修改资源处理逻辑,加入文件类型筛选
const { imageResources, fileResources, imagePagination, filePagination } = const { imageResources, fileResources, imagePagination, filePagination } =
useMemo(() => { useMemo(() => {
if (!resources) { if (!resources) {
@ -94,11 +74,10 @@ export function VideoContent() {
const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`; const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`;
const isImg = isImage(resource.url); const isImg = isImage(resource.url);
// 提取文件扩展名 // 确保 title 存在,优先使用 resource.title然后是 resource.meta.filename
const extension = resource.url.split(".").pop()?.toLowerCase() || "";
const displayTitle = const displayTitle =
resource.title || resource.meta?.filename || "未命名文件"; resource.title || resource.meta?.filename || "未命名文件";
// 用于搜索的名称,确保从 meta.filename 获取(如果存在)
const searchableFilename = resource.meta?.filename || ""; const searchableFilename = resource.meta?.filename || "";
return { return {
@ -106,27 +85,20 @@ export function VideoContent() {
url: isImg ? getCompressedImageUrl(original) : original, url: isImg ? getCompressedImageUrl(original) : original,
originalUrl: original, originalUrl: original,
isImage: isImg, isImage: isImg,
title: displayTitle, title: displayTitle, // 用于显示
searchableFilename: searchableFilename, searchableFilename: searchableFilename, // 用于搜索
extension: extension,
}; };
}) })
.filter(Boolean); .filter(Boolean);
// 根据搜索词和文件类型筛选 // 根据搜索词筛选文件资源 (基于 searchableFilename)
const filteredFileResources = processedResources.filter((res) => { const filteredFileResources = processedResources.filter(
// 首先检查是否符合搜索词 (res) =>
const matchesSearch = res.searchableFilename !res.isImage &&
.toLowerCase() res.searchableFilename
.includes(searchTerm.toLowerCase()); .toLowerCase()
.includes(searchTerm.toLowerCase())
// 然后检查文件类型 );
const matchesType =
fileType === "all" ||
fileTypes[fileType]?.extensions.includes(res.extension);
return !res.isImage && matchesSearch && matchesType;
});
const allImageResources = processedResources.filter((res) => res.isImage); const allImageResources = processedResources.filter((res) => res.isImage);
@ -152,7 +124,7 @@ export function VideoContent() {
data: filteredFileResources, data: filteredFileResources,
}, },
}; };
}, [resources, imagePage, filePage, searchTerm, fileType]); // 添加fileType依赖 }, [resources, imagePage, filePage, searchTerm]); // searchTerm 作为依赖项
const createMutation = api.resource.create.useMutation({}); const createMutation = api.resource.create.useMutation({});
const handleSubmit = async () => { const handleSubmit = async () => {
@ -251,31 +223,13 @@ export function VideoContent() {
<p>excelppt等多种格式文件</p> <p>excelppt等多种格式文件</p>
</div> </div>
</header> </header>
<div className="flex flex-wrap items-center gap-4 mb-4"> <Input.Search
<Input.Search placeholder="搜索文件名"
placeholder="搜索文件名" allowClear
allowClear onSearch={(value) => setSearchTerm(value)}
onSearch={(value) => setSearchTerm(value)} onChange={(e) => setSearchTerm(e.target.value)}
onChange={(e) => setSearchTerm(e.target.value)} style={{ width: 300 }}
style={{ width: 300 }} />
/>
<Select
value={fileType}
onChange={(value) => setFileType(value)}
style={{ width: 120 }}
options={Object.entries(fileTypes).map(([key, { label }]) => ({
value: key,
label: label,
}))}
/>
{/* {fileType !== "all" && (
<Tag color="blue" closable onClose={() => setFileType("all")}>
{fileTypes[fileType]?.label}
</Tag>
)} */}
</div>
{!isAuthenticated && ( {!isAuthenticated && (
<Alert <Alert
message="请先登录" message="请先登录"
@ -419,20 +373,20 @@ export function VideoContent() {
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate">
{resource.meta?.filename} {resource.title}
</div> </div>
{/* {resource.meta?.filetype && ( {resource.description && (
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
: {resource.meta?.filetype} : {resource.description}
</div> </div>
)} */} )}
<div className="text-xs text-gray-500 flex items-center gap-2"> <div className="text-xs text-gray-500 flex items-center gap-2">
<span> <span>
{dayjs(resource.createdAt).format("YYYY-MM-DD")} {dayjs(resource.createdAt).format("YYYY-MM-DD")}
</span> </span>
<span> <span>
{resource.meta?.size && {resource.meta?.size &&
formatFileSize(resource.meta?.size)} formatFileSize(resource.meta.size)}
</span> </span>
</div> </div>
</div> </div>
@ -462,6 +416,12 @@ export function VideoContent() {
</div> </div>
)} )}
{fileResources.length === 0 && searchTerm && (
<div className="text-center py-4 text-gray-500">
"{searchTerm}"
</div>
)}
{/* 文件分页 */} {/* 文件分页 */}
{filePagination.total > pageSize && ( {filePagination.total > pageSize && (
<div className="flex justify-center mt-6"> <div className="flex justify-center mt-6">

View File

@ -14,7 +14,7 @@ export function ExampleContent() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const pageSize = 10; // 每页显示5条案例 const pageSize = 6; // 每页显示5条案例
const isDomainAdmin = useMemo(() => { const isDomainAdmin = useMemo(() => {
return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF");
}, [hasSomePermissions]); }, [hasSomePermissions]);

View File

@ -14,7 +14,7 @@ export function PublicityContent() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const pageSize = 10; // 每页显示5条新闻 const pageSize = 6; // 每页显示5条新闻
const isDomainAdmin = useMemo(() => { const isDomainAdmin = useMemo(() => {
return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF");
}, [hasSomePermissions]); }, [hasSomePermissions]);

View File

@ -14,7 +14,7 @@ export function ScienceContent() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const pageSize = 10; // 每页显示5条科普 const pageSize = 6; // 每页显示5条科普
const isDomainAdmin = useMemo(() => { const isDomainAdmin = useMemo(() => {
return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF");
}, [hasSomePermissions]); }, [hasSomePermissions]);

View File

@ -1,180 +1,121 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { Input, Pagination, Empty, Spin, Radio, Space, Tag } from "antd"; import { Input, Pagination, Empty, Spin } from "antd";
import { api, RouterInputs } from "@nice/client"; import { api, RouterInputs } from "@nice/client";
import { LetterCard } from "../LetterCard"; import { LetterCard } from "../LetterCard";
import { NonVoid } from "@nice/utils"; import { NonVoid } from "@nice/utils";
import { SearchOutlined, FilterOutlined } from "@ant-design/icons"; import { SearchOutlined } from "@ant-design/icons";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { postDetailSelect } from "@nice/common"; import { postDetailSelect } from "@nice/common";
export default function LetterList({ export default function LetterList({
params, params,
search = "", search = ''
}: { }: {
search?: string; search?: string,
params: NonVoid<RouterInputs["post"]["findManyWithPagination"]>; params: NonVoid<RouterInputs["post"]["findManyWithPagination"]>;
}) { }) {
const [keyword, setKeyword] = useState<string | undefined>(""); const [keyword, setKeyword] = useState<string | undefined>('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [selectedCategory, setSelectedCategory] = useState<string>("all"); useEffect(() => {
const { data: categoriesData } = api.term.findMany.useQuery({ setKeyword(search || '')
where: { }, [search])
taxonomy: { const { data, isLoading } = api.post.findManyWithPagination.useQuery({
slug: "category", page: currentPage,
}, pageSize: params.pageSize,
}, where: {
}); OR: [
{
const categoryOptions = useMemo(() => { title: {
const options = [{ value: "all", label: "全部分类" }]; contains: keyword,
if (categoriesData) { },
categoriesData.forEach((category) => { },
options.push({ ],
value: category.id, ...params?.where,
label: category.name,
});
});
}
return options;
}, [categoriesData]);
useEffect(() => {
setKeyword(search || "");
}, [search]);
const { data, isLoading } = api.post.findManyWithPagination.useQuery({
page: currentPage,
pageSize: params.pageSize,
where: {
OR: [
{
title: {
contains: keyword,
},
}, },
], orderBy: {
...(selectedCategory !== "all" updatedAt: "desc",
? { },
terms: { select: {
some: { ...postDetailSelect,
id: selectedCategory, ...params.select,
}, },
}, });
}
: {}),
...params?.where,
},
orderBy: {
updatedAt: "desc",
},
select: {
...postDetailSelect,
...params.select,
},
});
const debouncedSearch = useMemo( const debouncedSearch = useMemo(
() => () =>
debounce((value: string) => { debounce((value: string) => {
setKeyword(value); setKeyword(value);
setCurrentPage(1); setCurrentPage(1);
}, 300), }, 300),
[] []
); );
// Cleanup debounce on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
debouncedSearch.cancel(); debouncedSearch.cancel();
};
}, [debouncedSearch]);
const handleSearch = (value: string) => {
debouncedSearch(value);
}; };
}, [debouncedSearch]);
const handleSearch = (value: string) => { const handlePageChange = (page: number) => {
debouncedSearch(value); setCurrentPage(page);
}; // Scroll to top when page changes
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handlePageChange = (page: number) => { return (
setCurrentPage(page); <div className="flex flex-col h-full">
window.scrollTo({ top: 0, behavior: "smooth" }); {/* Search Bar */}
}; <div className="p-6 transition-all ">
<Input
value={keyword}
variant="filled"
className="w-full bg-white"
placeholder="搜索信件标题..."
allowClear
size="large"
onChange={(e) => handleSearch(e.target.value)}
prefix={<SearchOutlined className="text-gray-400" />}
/>
</div>
const handleCategoryChange = (value: string) => { {/* Content Area */}
setSelectedCategory(value); <div className="flex-grow px-6">
setCurrentPage(1); {isLoading ? (
}; <div className="flex justify-center items-center pt-6">
<Spin size="large"></Spin>
</div>
) : data?.items.length ? (
<>
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-4 mb-6">
{data.items.map((letter: any) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
<div className="flex justify-center pb-6">
<Pagination
current={currentPage}
total={data.totalCount}
pageSize={params.pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
/>
</div>
</>
) : (
<div className="flex flex-col justify-center items-center pt-6">
<Empty
return ( description={
<div className="flex flex-col h-full"> keyword ? "未找到相关信件" : "暂无信件"
<div className="p-6 transition-all"> }
<div className="flex items-center mb-4"> />
<Radio.Group </div>
value={selectedCategory} )}
onChange={(e) => handleCategoryChange(e.target.value)} </div>
optionType="button"
buttonStyle="solid"
className="flex-wrap"
>
{categoryOptions.map((option) => (
<Radio.Button
key={option.value}
value={option.value}
className="my-1"
>
{option.label}
</Radio.Button>
))}
</Radio.Group>
</div> </div>
);
<Space direction="vertical" className="w-full" size="middle">
<Input
value={keyword}
variant="filled"
className="w-full bg-white"
placeholder="搜索信件标题..."
allowClear
size="large"
onChange={(e) => handleSearch(e.target.value)}
prefix={<SearchOutlined className="text-gray-400" />}
/>
</Space>
</div>
<div className="flex-grow px-6">
{isLoading ? (
<div className="flex justify-center items-center pt-6">
<Spin size="large"></Spin>
</div>
) : data?.items.length ? (
<>
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-4 mb-6">
{data.items.map((letter: any) => (
<LetterCard key={letter.id} letter={letter} />
))}
</div>
<div className="flex justify-center pb-6">
<Pagination
current={currentPage}
total={data.totalCount}
pageSize={params.pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
/>
</div>
</>
) : (
<div className="flex flex-col justify-center items-center pt-6">
<Empty
description={
keyword || selectedCategory !== "all"
? "未找到相关信件"
: "暂无信件"
}
/>
</div>
)}
</div>
</div>
);
} }