Files
labelmain-demo/app/management/person/collection/page.tsx

630 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import {
getProjectList,
getProjectTypeList,
projectCollect,
} from "@/components/label/api/project"
import { Project } from "@/components/label/api/project/typing"
import {
initAllParams,
initCollectParams,
initDynamicParams,
useAllUserStore,
useProjectStore,
} from "@/components/label/store/auth"
import {
projectLabelTypeOpts,
projectStatusOpts,
STATUS_CODE,
} from "@/components/label/utils/constants"
import {
ActionIcon,
Badge,
Button,
Collapse,
Flex,
Group,
Loader,
Pagination,
Paper,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
UnstyledButton,
} from "@mantine/core"
import { notifications } from "@mantine/notifications"
import {
IconCalendar,
IconChevronDown,
IconChevronUp,
IconRefresh,
IconSearch,
IconStar,
IconStarFilled,
} from "@tabler/icons-react"
import dayjs from "dayjs"
import { User2 } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import { CardCoverIcon } from "./components/ProjectIcon"
type PageType = "all" | "collect" | "dynamic"
function statusBadgeColor(status?: number | null) {
if (!status) return "gray"
if ([191, 199, 700].includes(status)) return "red"
if ([580].includes(status)) return "yellow"
if ([600].includes(status)) return "green"
if ([201, 202, 203].includes(status)) return "cyan"
return "blue"
}
function parseStatusValue(value?: string) {
if (!value) return undefined
if (value.includes(",")) {
return value
.split(",")
.map((v) => Number(v))
.filter((n) => Number.isFinite(n))
}
const n = Number(value)
return Number.isFinite(n) ? [n] : undefined
}
export default function CollectionPage() {
const router = useRouter()
const urlParams = useSearchParams()
const type = (urlParams.get("type") as PageType | null) ?? "collect"
const { userOpts } = useAllUserStore()
const {
allParams,
allTotal,
collectParams,
collectTotal,
dynamicParams,
dynamicTotal,
resetParams,
setSearchParams,
setTotal,
}: any = useProjectStore()
const storeParams = useMemo(() => {
return type === "collect"
? collectParams
: type === "dynamic"
? dynamicParams
: allParams
}, [allParams, collectParams, dynamicParams, type])
const total = useMemo(() => {
return type === "collect"
? collectTotal
: type === "dynamic"
? dynamicTotal
: allTotal
}, [allTotal, collectTotal, dynamicTotal, type])
const [typeOpts, setTypeOpts] = useState<
Array<{ label: string; value: string }>
>([])
const [loading, setLoading] = useState(false)
const [records, setRecords] = useState<Project.DataProps[]>([])
const [searchExpanded, setSearchExpanded] = useState(false)
const [filters, setFilters] = useState(() => {
return {
project_name: "",
project_type: "",
status: "",
owner: "",
label_type: "",
admin_user: "",
start_date: dayjs().subtract(6, "day").format("YYYY-MM-DD"),
end_date: dayjs().format("YYYY-MM-DD"),
}
})
const [appliedFilters, setAppliedFilters] = useState(filters)
const [queryTrigger, setQueryTrigger] = useState(0)
useEffect(() => {
const initFormOpts = async () => {
const res = await getProjectTypeList()
setTypeOpts((res ?? []).map((name) => ({ label: name, value: name })))
}
initFormOpts()
}, [])
const queryParams = useMemo(() => {
const base =
type === "collect"
? { ...initCollectParams }
: type === "dynamic"
? { ...initDynamicParams }
: { ...initAllParams }
const params: Project.ListRequest & any = {
...base,
...storeParams,
page_number: storeParams.page_number ?? 1,
page_size: storeParams.page_size ?? 15,
project_name: appliedFilters.project_name || undefined,
project_type: appliedFilters.project_type || undefined,
owner: appliedFilters.owner || undefined,
admin_user: appliedFilters.admin_user || undefined,
start_date: appliedFilters.start_date || undefined,
end_date: appliedFilters.end_date || undefined,
label_type: appliedFilters.label_type
? Number(appliedFilters.label_type)
: undefined,
status: parseStatusValue(appliedFilters.status),
}
return params
}, [appliedFilters, storeParams, type])
const load = useCallback(async () => {
if (!queryTrigger) return
setLoading(true)
try {
const res = await getProjectList(queryParams)
setRecords(res?.project_list ?? [])
setTotal(res?.total_items ?? 0, type)
} catch (e) {
setRecords([])
setTotal(0, type)
notifications.show({
color: "red",
title: "加载失败",
message: e instanceof Error ? e.message : "请求失败",
})
} finally {
setLoading(false)
}
}, [queryParams, queryTrigger, setTotal, type])
useEffect(() => {
load()
}, [load])
useEffect(() => {
queueMicrotask(() => {
setQueryTrigger((v) => v + 1)
})
}, [])
const currentPage = storeParams.page_number ?? 1
const pageSize = storeParams.page_size ?? 15
const ProjectCard = (record: Project.DataProps) => {
return (
<Paper
key={record.id}
withBorder
radius="md"
shadow="sm"
style={{
borderColor: "rgba(0,0,0,0.06)",
height: 240,
padding: 14,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "white",
}}>
<Group justify="space-between" wrap="nowrap">
<Flex flex={1}>
<CardCoverIcon width={68} height={68} />
<Stack gap={0}>
<Flex align="center">
<UnstyledButton
variant="subtle"
size="sm"
style={{
paddingLeft: 6,
paddingRight: 6,
justifyContent: "flex-start",
width: "100%",
}}
onClick={() => {
const url = type
? `/management/person/collection/detail?id=${record.id}&type=${type}`
: `/management/person/collection/detail?id=${record.id}`
router.push(url)
}}>
<Text
fw={700}
c={"brand"}
style={{
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{record.name}
</Text>
</UnstyledButton>
<Badge
color={statusBadgeColor(record.status)}
style={{
width: "-webkit-fill-available",
}}>
{STATUS_CODE.get(record.status) ?? record.status}
</Badge>
</Flex>
<Flex align="center" px={6} gap={6}>
<User2
color="var(--mantine-color-dimmed)"
width={14}
height={14}
/>
<Text size="sm">{record.owner ?? "-"}</Text>
</Flex>
<Flex align="center" px={6} gap={6}>
<IconCalendar
color="var(--mantine-color-dimmed)"
width={14}
height={14}
/>
<Text size="sm">
{record.create_at ? record.create_at.split(" ")[0] : "-"}
</Text>
</Flex>
</Stack>
</Flex>
<ActionIcon
variant="subtle"
color={record.is_collect ? "yellow" : "gray"}
onClick={async () => {
try {
await projectCollect({
project_id: record.id,
is_collect: !record.is_collect,
})
notifications.show({
color: "green",
title: "操作成功",
message: record.is_collect ? "已取消收藏" : "已收藏",
})
setQueryTrigger((v) => v + 1)
} catch (e) {
notifications.show({
color: "red",
title: "操作失败",
message: e instanceof Error ? e.message : "请求失败",
})
}
}}>
{record.is_collect ? (
<IconStarFilled size={16} />
) : (
<IconStar size={16} />
)}
</ActionIcon>
</Group>
<Stack gap={6} style={{ flex: 1, minHeight: 0, marginTop: 8 }}>
<Text size="sm" c="dimmed">
{record.remarks ? record.remarks : "暂无备注"}
</Text>
</Stack>
</Paper>
)
}
const PaginationBar = (props: {
currentPage: number
pageSize: number
total: number
onChangePage: (page: number) => void
onChangePageSize: (size: number) => void
}) => {
const pageCount = Math.max(
1,
Math.ceil((props.total ?? 0) / props.pageSize)
)
return (
<div
style={{
height: 36,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 12,
paddingLeft: 12,
paddingRight: 12,
}}>
<Select
size="xs"
value={String(props.pageSize)}
data={[15, 45, 99].map((n) => ({
label: `${n}/页`,
value: String(n),
}))}
onChange={(v) => {
const next = Number(v)
if (!Number.isFinite(next)) return
props.onChangePageSize(next)
}}
allowDeselect={false}
style={{ width: 92 }}
/>
<Pagination
size="sm"
withControls
total={pageCount}
value={Math.min(props.currentPage, pageCount)}
onChange={props.onChangePage}
/>
</div>
)
}
return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
<Paper
withBorder
p="md"
radius="md"
style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<Group justify="space-between" wrap="wrap" gap="sm">
{/* <SegmentedControl
value={type}
data={[
{ label: "全部项目", value: "all" },
{ label: "我的收藏", value: "collect" },
{ label: "动态项目", value: "dynamic" },
]}
onChange={(v) => {
const next = v as PageType
router.replace(`/management/person/collection?type=${next}`)
}}
/> */}
<Group gap="xs">
<Text fw={700}></Text>
<Button
size="xs"
variant="transparent"
rightSection={
searchExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)
}
onClick={() => setSearchExpanded((v) => !v)}>
{searchExpanded ? "收起筛选" : "展开筛选"}
</Button>
</Group>
<Group gap="xs">
<Button
size="xs"
radius="xs"
variant="default"
leftSection={<IconRefresh size={16} />}
onClick={() => {
resetParams(type)
const next = {
project_name: "",
project_type: "",
status: "",
owner: "",
label_type: "",
admin_user: "",
start_date: dayjs().subtract(6, "day").format("YYYY-MM-DD"),
end_date: dayjs().format("YYYY-MM-DD"),
}
setFilters(next)
setAppliedFilters(next)
setQueryTrigger((v) => v + 1)
}}>
</Button>
<Button
size="xs"
radius="xs"
leftSection={<IconSearch size={16} />}
onClick={() => {
setSearchParams({ ...storeParams, page_number: 1 }, type)
setAppliedFilters(filters)
setQueryTrigger((v) => v + 1)
}}>
</Button>
</Group>
</Group>
<Collapse
in={searchExpanded}
transitionDuration={250}
transitionTimingFunction="ease"
animateOpacity>
<Stack gap="sm" mt="sm">
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
<TextInput
label="项目名称"
size="xs"
radius="xs"
value={filters.project_name}
onChange={(e) =>
setFilters((s) => ({
...s,
project_name: e.currentTarget.value,
}))
}
/>
<Select
label="所属业务"
size="xs"
radius="xs"
searchable
clearable
data={typeOpts}
value={filters.project_type || null}
onChange={(v) =>
setFilters((s) => ({ ...s, project_type: v ?? "" }))
}
/>
<Select
label="状态"
size="xs"
radius="xs"
searchable
data={projectStatusOpts.map((o) => ({
label: o.label,
value: o.label,
}))}
value={filters.status}
onChange={(v) => setFilters((s) => ({ ...s, status: v ?? "" }))}
/>
<TextInput
label="负责人"
size="xs"
radius="xs"
value={filters.owner}
onChange={(e) =>
setFilters((s) => ({ ...s, owner: e.currentTarget.value }))
}
/>
<Select
label="标注类型"
size="xs"
radius="xs"
searchable
clearable
data={projectLabelTypeOpts.map((o) => ({
label: o.label,
value: String(o.value),
}))}
value={filters.label_type || null}
onChange={(v) =>
setFilters((s) => ({ ...s, label_type: v ?? "" }))
}
/>
<Select
label="标注管理员"
size="xs"
radius="xs"
searchable
clearable
data={userOpts.map((o) => ({
label: o.label,
value: String(o.value),
}))}
value={filters.admin_user || null}
onChange={(v) =>
setFilters((s) => ({ ...s, admin_user: v ?? "" }))
}
/>
<TextInput
label="开始日期"
size="xs"
radius="xs"
type="date"
value={filters.start_date}
onChange={(e) =>
setFilters((s) => ({
...s,
start_date: e.currentTarget.value,
}))
}
/>
<TextInput
label="结束日期"
size="xs"
radius="xs"
type="date"
value={filters.end_date}
onChange={(e) =>
setFilters((s) => ({ ...s, end_date: e.currentTarget.value }))
}
/>
</SimpleGrid>
</Stack>
</Collapse>
</Paper>
<Paper
withBorder
p="md"
radius="md"
style={{
borderColor: "rgba(0,0,0,0.06)",
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
}}>
<div
style={{
flex: 1,
minHeight: 0,
overflowY: "auto",
paddingRight: 2,
}}>
{loading ? (
<div
style={{
height: "100%",
minHeight: 220,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<Loader size="sm" />
</div>
) : records.length ? (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
gap: 16,
}}>
{records.map(ProjectCard)}
</div>
) : (
<div
style={{
height: "100%",
minHeight: 220,
border: "1px solid rgba(0,0,0,0.06)",
borderRadius: 12,
background: "rgba(0,0,0,0.02)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<Text size="lg" c="dimmed" fw={600}>
</Text>
</div>
)}
</div>
<div style={{ height: 36, marginTop: 16 }}>
<PaginationBar
currentPage={currentPage}
pageSize={pageSize}
total={total ?? 0}
onChangePage={(p) => {
setSearchParams({ ...storeParams, page_number: p }, type)
setQueryTrigger((v) => v + 1)
}}
onChangePageSize={(s) => {
setSearchParams(
{ ...storeParams, page_number: 1, page_size: s },
type
)
setQueryTrigger((v) => v + 1)
}}
/>
</div>
</Paper>
</Stack>
)
}