perf(style): codex-skills/cowa-admin-list-ui
This commit is contained in:
@@ -3,24 +3,20 @@
|
||||
import { getPersonProjectDashBoard } from "@/components/label/api/project"
|
||||
import { Project } from "@/components/label/api/project/typing"
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core"
|
||||
SettingDataTable,
|
||||
SettingListHeader,
|
||||
} from "@/components/setting/PageSurface"
|
||||
import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { DatePickerInput, type DatesRangeValue } from "@mantine/dates"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
|
||||
import { IconRefresh } from "@tabler/icons-react"
|
||||
import { IconRefresh, IconSearch } from "@tabler/icons-react"
|
||||
import dayjs from "dayjs"
|
||||
import "dayjs/locale/zh-cn"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DataTableColumn } from "mantine-datatable"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import classes from "../page.module.css"
|
||||
|
||||
dayjs.extend(duration)
|
||||
|
||||
@@ -51,14 +47,13 @@ function createInitialFilters(): FilterFormRecord {
|
||||
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50]
|
||||
|
||||
export default function PersonalProjectTable() {
|
||||
const router = useRouter()
|
||||
const [records, setRecords] = useState<
|
||||
Project.PersonProjectDashboardResponseItem[]
|
||||
>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [recordsPerPageOptions, setRecordsPerPageOptions] = useState<number[]>(
|
||||
() => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS]
|
||||
)
|
||||
@@ -123,12 +118,18 @@ export default function PersonalProjectTable() {
|
||||
setQueryTrigger((v) => v + 1)
|
||||
}
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
const next = createInitialFilters()
|
||||
setFilters(next)
|
||||
setForm(next)
|
||||
setPage(1)
|
||||
setQueryTrigger((v) => v + 1)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (page > totalPages) setPage(totalPages)
|
||||
}, [page, totalPages])
|
||||
|
||||
const [currentRowId, setCurrentRowId] = useState<number>(0)
|
||||
|
||||
const openCustomPageSizeModal = useCallback(() => {
|
||||
setCustomPageSizeModalOpened(true)
|
||||
}, [])
|
||||
@@ -186,22 +187,8 @@ export default function PersonalProjectTable() {
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return (
|
||||
<Text
|
||||
px={6}
|
||||
variant="transparent"
|
||||
onClick={async () => {
|
||||
false && setCurrentRowId(record.project_id)
|
||||
false &&
|
||||
router.push(`/project/info/detail?id=${record.project_id}`)
|
||||
}}
|
||||
title={record.project_id.toString()}
|
||||
// c={"brand"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
className={classes.ellipsisText}>
|
||||
{record.project_id}
|
||||
</Text>
|
||||
)
|
||||
@@ -215,13 +202,7 @@ export default function PersonalProjectTable() {
|
||||
return (
|
||||
<Text
|
||||
title={record.project_name ?? "-"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
className={classes.ellipsisText}>
|
||||
{record.project_name ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
@@ -235,13 +216,7 @@ export default function PersonalProjectTable() {
|
||||
return (
|
||||
<Text
|
||||
title={record.project_id.toString()}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
className={classes.ellipsisText}>
|
||||
{record.project_type ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
@@ -252,7 +227,11 @@ export default function PersonalProjectTable() {
|
||||
title: "任务耗时",
|
||||
width: 140,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return formatDurationSeconds(record.work_time)
|
||||
return (
|
||||
<Text className={classes.tableCellText}>
|
||||
{formatDurationSeconds(record.work_time)}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -260,74 +239,63 @@ export default function PersonalProjectTable() {
|
||||
title: "标注类别",
|
||||
width: 160,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return record.project_label_type ?? "-"
|
||||
return (
|
||||
<Text className={classes.tableCellText}>
|
||||
{record.project_label_type ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
return column
|
||||
}, [router])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
h="100%"
|
||||
align="stretch"
|
||||
style={{ flexDirection: "column" }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Flex align={"center"} gap={8}>
|
||||
<Text fw={700} fz="lg">
|
||||
参与项目
|
||||
</Text>
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
label=""
|
||||
placeholder="请选择时间范围,默认一周"
|
||||
miw={208}
|
||||
valueFormat="YYYY-MM-DD"
|
||||
locale="zh-cn"
|
||||
clearable
|
||||
value={filters.query_date}
|
||||
onChange={(value: DatesRangeValue) =>
|
||||
setFilters((s) => ({
|
||||
...s,
|
||||
query_date: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
size={"xs"}
|
||||
fz={"sm"}
|
||||
fw={500}
|
||||
radius="sm"
|
||||
onClick={handleSearch}
|
||||
loading={loading}
|
||||
leftSection={<IconRefresh size={16} />}>
|
||||
更新
|
||||
</Button>
|
||||
</Group>
|
||||
<Group
|
||||
gap={0}
|
||||
align="stretch"
|
||||
style={{ flex: 1, minHeight: 0, width: "100%" }}>
|
||||
<DataTable<Project.PersonProjectDashboardResponseItem>
|
||||
<Stack className={classes.dashboardSection}>
|
||||
<SettingListHeader title="参与项目" count={`共 ${totalItems} 条记录`} />
|
||||
<div className={classes.dashboardFilterBar}>
|
||||
<div className={classes.dashboardFilterField}>
|
||||
<Text className={classes.dashboardFilterLabel}>统计周期</Text>
|
||||
<div className={classes.dashboardFilterControl}>
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
placeholder="请选择时间范围,默认一周"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
locale="zh-cn"
|
||||
clearable
|
||||
value={filters.query_date}
|
||||
onChange={(value: DatesRangeValue) =>
|
||||
setFilters((s) => ({
|
||||
...s,
|
||||
query_date: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.dashboardFilterActions}>
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
color="#1874ff"
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onClick={handleSearch}
|
||||
disabled={loading}>
|
||||
查询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.dashboardTableArea}>
|
||||
<SettingDataTable<Project.PersonProjectDashboardResponseItem>
|
||||
width="100%"
|
||||
style={{ width: "100%" }}
|
||||
styles={{
|
||||
header: {
|
||||
color: "var(--mantine-color-grey-text)",
|
||||
backgroundColor: "var(--mantine-datatable-striped-color)",
|
||||
},
|
||||
}}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
minHeight={0}
|
||||
style={{ width: "100%", flex: 1 }}
|
||||
pinFirstColumn
|
||||
rowBackgroundColor={(record) => {
|
||||
if (record.project_id === currentRowId)
|
||||
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
|
||||
}}
|
||||
idAccessor="project_id"
|
||||
fetching={loading}
|
||||
scrollAreaProps={{ type: "auto" }}
|
||||
records={pageRecords}
|
||||
onRecordsPerPageChange={setPageSize}
|
||||
@@ -338,7 +306,10 @@ export default function PersonalProjectTable() {
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
noRecordsText="暂无数据"
|
||||
recordsPerPageLabel="当前页数"
|
||||
recordsPerPageLabel="每页条数"
|
||||
paginationText={({ from, to, totalRecords }) =>
|
||||
`显示 ${from}-${to} 条,共 ${totalRecords} 条记录`
|
||||
}
|
||||
renderPagination={({ Controls }) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
@@ -350,9 +321,8 @@ export default function PersonalProjectTable() {
|
||||
<Controls.Text />
|
||||
<Controls.PageSizeSelector />
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="xs"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={openCustomPageSizeModal}>
|
||||
自定义每页条数
|
||||
</Button>
|
||||
@@ -361,7 +331,7 @@ export default function PersonalProjectTable() {
|
||||
</Group>
|
||||
)}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
<Modal
|
||||
opened={customPageSizeModalOpened}
|
||||
onClose={closeCustomPageSizeModal}
|
||||
@@ -382,10 +352,12 @@ export default function PersonalProjectTable() {
|
||||
<Button variant="default" onClick={closeCustomPageSizeModal}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAddCustomPageSize}>添加</Button>
|
||||
<Button color="#1874ff" onClick={handleAddCustomPageSize}>
|
||||
添加
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Group>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
import { getUserTaskList } from "@/components/label/api/task"
|
||||
import { Task } from "@/components/label/api/task/typing"
|
||||
import TaskStatusTag from "@/components/label/components/TaskStatusTag"
|
||||
import {
|
||||
useBackUrlStore,
|
||||
usePermissionStore,
|
||||
} from "@/components/label/store/auth"
|
||||
import { TaskStatusEnum } from "@/components/label/utils/constants"
|
||||
import {
|
||||
Badge,
|
||||
SettingDataTable,
|
||||
SettingHeaderActions,
|
||||
SettingListHeader,
|
||||
} from "@/components/setting/PageSurface"
|
||||
import {
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
@@ -19,25 +23,9 @@ import {
|
||||
} from "@mantine/core"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
import { IconRefresh } from "@tabler/icons-react"
|
||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
||||
import { DataTableColumn } from "mantine-datatable"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
function getStatusColor(status?: number | null) {
|
||||
switch (status) {
|
||||
case 2:
|
||||
return "blue"
|
||||
case 4:
|
||||
return "yellow"
|
||||
case 6:
|
||||
return "orange"
|
||||
case 7:
|
||||
return "green"
|
||||
case 8:
|
||||
return "red"
|
||||
default:
|
||||
return "gray"
|
||||
}
|
||||
}
|
||||
import classes from "../page.module.css"
|
||||
|
||||
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50]
|
||||
|
||||
@@ -46,7 +34,7 @@ export default function PersonalTaskTable() {
|
||||
const setBackProps = useBackUrlStore((s) => s.setBackProps)
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [recordsPerPageOptions, setRecordsPerPageOptions] = useState<number[]>(
|
||||
() => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS]
|
||||
)
|
||||
@@ -148,13 +136,7 @@ export default function PersonalTaskTable() {
|
||||
return (
|
||||
<Text
|
||||
title={record.project_name ?? "-"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
className={classes.ellipsisText}>
|
||||
{record.project_name ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
@@ -167,8 +149,6 @@ export default function PersonalTaskTable() {
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return (
|
||||
<UnstyledButton
|
||||
px={6}
|
||||
variant="transparent"
|
||||
onClick={async () => {
|
||||
setCurrentRowId(record.id)
|
||||
if ([4, 5, 6, 7].includes(record.label_type)) {
|
||||
@@ -193,14 +173,7 @@ export default function PersonalTaskTable() {
|
||||
}
|
||||
}}
|
||||
title={record.id.toString()}
|
||||
c={"brand"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
className={classes.tableLink}>
|
||||
{record.id}
|
||||
</UnstyledButton>
|
||||
)
|
||||
@@ -211,86 +184,84 @@ export default function PersonalTaskTable() {
|
||||
title: "对象数",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return <>{record.label_object_size?.object_size || "-"}</>
|
||||
return (
|
||||
<Text className={classes.tableCellText}>
|
||||
{record.label_object_size?.object_size || "-"}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "label_status",
|
||||
title: "状态",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return (
|
||||
<Badge
|
||||
color={getStatusColor(record.label_status)}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "2px 6px",
|
||||
}}>
|
||||
{TaskStatusEnum.get(record.label_status)}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
width: 160,
|
||||
render: (record) => (
|
||||
<TaskStatusTag
|
||||
status={record.label_status}
|
||||
rejected={record.rejected}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "label_username",
|
||||
title: "标注员",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => (
|
||||
<Text className={classes.tableCellText}>
|
||||
{record.label_username || "-"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "review1_username",
|
||||
title: "审核员",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => (
|
||||
<Text className={classes.tableCellText}>
|
||||
{record.review1_username || "-"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "review2_username",
|
||||
title: "复审员",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => (
|
||||
<Text className={classes.tableCellText}>
|
||||
{record.review2_username || "-"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
return column
|
||||
}, [setBackProps])
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
h="100%"
|
||||
align="stretch"
|
||||
style={{ flexDirection: "column" }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={700} fz="lg" style={{ marginBottom: 8 }}>
|
||||
我的任务
|
||||
</Text>
|
||||
<Button
|
||||
size={"xs"}
|
||||
fz={"sm"}
|
||||
fw={500}
|
||||
radius="sm"
|
||||
onClick={resetData}
|
||||
loading={loading}
|
||||
leftSection={<IconRefresh size={16} />}>
|
||||
更新
|
||||
</Button>
|
||||
</Group>
|
||||
<Group
|
||||
gap={0}
|
||||
align="stretch"
|
||||
style={{ flex: 1, minHeight: 0, width: "100%" }}>
|
||||
<DataTable<Task.SimpleTaskItem>
|
||||
<Stack className={classes.dashboardSection}>
|
||||
<SettingListHeader
|
||||
title="我的任务"
|
||||
count={`共 ${totalItems} 条记录`}
|
||||
actions={
|
||||
<SettingHeaderActions>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={resetData}
|
||||
disabled={loading}
|
||||
leftSection={<IconRefresh size={16} />}>
|
||||
刷新
|
||||
</Button>
|
||||
</SettingHeaderActions>
|
||||
}
|
||||
/>
|
||||
<div className={classes.dashboardTableArea}>
|
||||
<SettingDataTable<Task.SimpleTaskItem>
|
||||
width="100%"
|
||||
style={{ width: "100%" }}
|
||||
styles={{
|
||||
header: {
|
||||
color: "var(--mantine-color-grey-text)",
|
||||
backgroundColor: "var(--mantine-datatable-striped-color)",
|
||||
},
|
||||
}}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
minHeight={0}
|
||||
style={{ width: "100%", flex: 1 }}
|
||||
pinFirstColumn
|
||||
pinLastColumn
|
||||
fetching={loading}
|
||||
rowBackgroundColor={({ id }) => {
|
||||
if (id === currentRowId)
|
||||
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
|
||||
if (id === currentRowId) return "#F7FBFF"
|
||||
}}
|
||||
scrollAreaProps={{ type: "auto" }}
|
||||
records={records}
|
||||
@@ -302,7 +273,10 @@ export default function PersonalTaskTable() {
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
noRecordsText="暂无数据"
|
||||
recordsPerPageLabel="当前页数"
|
||||
recordsPerPageLabel="每页条数"
|
||||
paginationText={({ from, to, totalRecords }) =>
|
||||
`显示 ${from}-${to} 条,共 ${totalRecords} 条记录`
|
||||
}
|
||||
renderPagination={({ Controls }) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
@@ -314,9 +288,8 @@ export default function PersonalTaskTable() {
|
||||
<Controls.Text />
|
||||
<Controls.PageSizeSelector />
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
size="xs"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={openCustomPageSizeModal}>
|
||||
自定义每页条数
|
||||
</Button>
|
||||
@@ -325,7 +298,7 @@ export default function PersonalTaskTable() {
|
||||
</Group>
|
||||
)}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
<Modal
|
||||
opened={customPageSizeModalOpened}
|
||||
onClose={closeCustomPageSizeModal}
|
||||
@@ -346,10 +319,12 @@ export default function PersonalTaskTable() {
|
||||
<Button variant="default" onClick={closeCustomPageSizeModal}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAddCustomPageSize}>添加</Button>
|
||||
<Button color="#1874ff" onClick={handleAddCustomPageSize}>
|
||||
添加
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Group>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
328
app/person/dashboard/page.module.css
Normal file
328
app/person/dashboard/page.module.css
Normal file
@@ -0,0 +1,328 @@
|
||||
.pageIntro {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.breadcrumbCurrent {
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.summaryGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 180px repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summaryDateCell,
|
||||
.summaryMetricCell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 116px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.summaryDateCell,
|
||||
.summaryMetricCell:not(:last-child) {
|
||||
border-right: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.summaryDateCell {
|
||||
justify-content: center;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.summaryDateLabel,
|
||||
.summaryMetricLabel {
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.summaryDateValue {
|
||||
color: #1d2129;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.summaryDateMeta {
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.summaryMetricCell {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.summaryMetricValue {
|
||||
color: #1d2129;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.summaryMetricMeta {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
background: #f7f8fa;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.summaryMetricMetaLabel {
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.summaryMetricMetaValue {
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tableGrid {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboardPanel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboardPanelBody {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboardSection {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboardTableArea {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboardFilterBar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.dashboardFilterField {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dashboardFilterLabel {
|
||||
flex: 0 0 80px;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.dashboardFilterControl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboardFilterControl :global(.mantine-InputWrapper-root) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboardFilterBar :global(.mantine-Input-input),
|
||||
.dashboardFilterBar :global(.mantine-DateInput-input) {
|
||||
min-height: 32px;
|
||||
border-color: #e5e6eb;
|
||||
border-radius: 4px;
|
||||
background: #f7f8fa;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
box-shadow: none;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboardFilterBar :global(.mantine-Input-input::placeholder),
|
||||
.dashboardFilterBar :global(.mantine-DateInput-input::placeholder) {
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.dashboardFilterBar :global(.mantine-Input-input:focus),
|
||||
.dashboardFilterBar :global(.mantine-DateInput-input:focus) {
|
||||
border-color: #1874ff;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12);
|
||||
}
|
||||
|
||||
.dashboardFilterBar :global(.mantine-Input-section),
|
||||
.dashboardFilterBar :global(.mantine-DateInput-section) {
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.dashboardFilterActions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dashboardFilterActions :global(.mantine-Button-root) {
|
||||
min-height: 32px;
|
||||
border-radius: 4px;
|
||||
padding-inline: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dashboardFilterActions :global(.mantine-Button-root[data-variant="filled"]) {
|
||||
--button-bg: #1874ff;
|
||||
--button-hover: #0f6ae8;
|
||||
--button-color: #ffffff;
|
||||
--button-bd: transparent;
|
||||
background: #1874ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dashboardFilterActions
|
||||
:global(.mantine-Button-root[data-variant="filled"]:hover) {
|
||||
background: #0f6ae8 !important;
|
||||
}
|
||||
|
||||
.dashboardFilterActions :global(.mantine-Button-root[data-variant="default"]) {
|
||||
border-color: #e5e6eb;
|
||||
background: #ffffff;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.tableLink {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #1874ff;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableLink:disabled {
|
||||
color: #c9cdd4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tableCellText {
|
||||
display: block;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ellipsisText {
|
||||
display: block;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
min-height: 24px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.summaryGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summaryMetricCell:nth-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.summaryDateCell,
|
||||
.summaryMetricCell {
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.summaryMetricCell:nth-last-child(-n + 2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.tableGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.summaryGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.summaryDateCell,
|
||||
.summaryMetricCell {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.summaryMetricCell:not(:last-child) {
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.dashboardFilterBar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboardFilterField {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dashboardFilterLabel {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.dashboardFilterActions {
|
||||
justify-self: stretch;
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,17 @@
|
||||
|
||||
import { getCurrentPersonData } from "@/components/label/api/project"
|
||||
import {
|
||||
BackgroundImage,
|
||||
Center,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core"
|
||||
SettingContentPanel,
|
||||
SettingPage,
|
||||
SettingPanel,
|
||||
} from "@/components/setting/PageSurface"
|
||||
import { Stack, Text } from "@mantine/core"
|
||||
import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import { JSX, useEffect, useMemo, useState } from "react"
|
||||
import { DataIcon, HourIcon, ReviewIcon } from "./components/Icon"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import PersonalProjectTable from "./components/PersonalProjectTable"
|
||||
import PersonalTaskTable from "./components/PersonalTaskTable"
|
||||
import DataImage from "./resource/images/data.png"
|
||||
import DataLogoImage from "./resource/images/data_logo.png"
|
||||
import HourImage from "./resource/images/hour.png"
|
||||
import HourLogoImage from "./resource/images/hour_logo.png"
|
||||
import ReviewImage from "./resource/images/review.png"
|
||||
import ReviewLogoImage from "./resource/images/review_logo.png"
|
||||
import classes from "./page.module.css"
|
||||
|
||||
dayjs.extend(duration)
|
||||
|
||||
@@ -56,95 +46,6 @@ function formatDurationSeconds(seconds?: number | null) {
|
||||
return "-"
|
||||
}
|
||||
|
||||
function StatCard(props: {
|
||||
title: string
|
||||
timeText: string
|
||||
sizeTitle: string
|
||||
sizeValue: number
|
||||
sizeColor: string
|
||||
background: string
|
||||
backgroundLogo: string
|
||||
Icon: () => JSX.Element
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
timeText,
|
||||
sizeTitle,
|
||||
sizeValue,
|
||||
sizeColor,
|
||||
background,
|
||||
backgroundLogo,
|
||||
Icon,
|
||||
} = props
|
||||
return (
|
||||
<Paper
|
||||
shadow="xs"
|
||||
p="md"
|
||||
withBorder
|
||||
radius={"md"}
|
||||
pos="relative"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
}}>
|
||||
<BackgroundImage
|
||||
src={background}
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{ position: "absolute", top: 0, left: 0, zIndex: 0 }}
|
||||
/>
|
||||
<BackgroundImage
|
||||
src={backgroundLogo}
|
||||
w={108}
|
||||
h={108}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 8,
|
||||
zIndex: 0,
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
<Group gap="md" align="flex-start" style={{ flex: 1, zIndex: 1 }}>
|
||||
<div style={{ width: 32 }}>
|
||||
<Icon />
|
||||
</div>
|
||||
<Stack gap={6} style={{ flex: 1 }}>
|
||||
<div>
|
||||
<Text fw={700} fz="lg" lh={1.4}>
|
||||
{title}
|
||||
</Text>
|
||||
<Title order={3} style={{ marginTop: 6 }}>
|
||||
{timeText}
|
||||
</Title>
|
||||
</div>
|
||||
<Stack gap={6}>
|
||||
<Text fw={700} fz="lg">
|
||||
{sizeTitle}
|
||||
</Text>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
px={8}
|
||||
py={4}
|
||||
radius="sm"
|
||||
style={{
|
||||
background: "#fff",
|
||||
width: "fit-content",
|
||||
color: sizeColor,
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
}}>
|
||||
{sizeValue}
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PersonDashboardPage() {
|
||||
const [data, setData] = useState<{
|
||||
label_data_size: number
|
||||
@@ -173,116 +74,86 @@ export default function PersonDashboardPage() {
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const summaryItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "标注工时",
|
||||
value: dataText.label,
|
||||
metaLabel: "标注数据量",
|
||||
metaValue: data?.label_data_size ?? 0,
|
||||
},
|
||||
{
|
||||
title: "审核工时",
|
||||
value: dataText.review1,
|
||||
metaLabel: "审核数据量",
|
||||
metaValue: data?.review1_data_size ?? 0,
|
||||
},
|
||||
{
|
||||
title: "复审工时",
|
||||
value: dataText.review2,
|
||||
metaLabel: "复审数据量",
|
||||
metaValue: data?.review2_data_size ?? 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
data?.label_data_size,
|
||||
data?.review1_data_size,
|
||||
data?.review2_data_size,
|
||||
dataText,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
||||
<Group align="stretch" gap="md" wrap="nowrap" h={180}>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
width: 160,
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}>
|
||||
<Text fz="lg" fw={700}>
|
||||
今日
|
||||
</Text>
|
||||
<Center>
|
||||
<Paper
|
||||
radius="md"
|
||||
p="sm"
|
||||
style={{
|
||||
width: 72,
|
||||
height: 72,
|
||||
background: "rgba(0, 161, 255, 0.08)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 4,
|
||||
}}>
|
||||
<Text fz={24} fw={700} lh={1}>
|
||||
{dayjs().format("DD")}
|
||||
</Text>
|
||||
<Text fz={16}>{monthText(dayjs().format("MM"))}</Text>
|
||||
</Paper>
|
||||
</Center>
|
||||
</Paper>
|
||||
<SettingPage>
|
||||
<Stack className={classes.pageIntro}>
|
||||
<Text className={classes.breadcrumb}>
|
||||
个人工作台 /{" "}
|
||||
<span className={classes.breadcrumbCurrent}>个人看板</span>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" style={{ flex: 1 }}>
|
||||
<StatCard
|
||||
title="标注工时"
|
||||
timeText={dataText.label}
|
||||
sizeTitle="标注数据量"
|
||||
sizeValue={data?.label_data_size ?? 0}
|
||||
sizeColor="#E03131"
|
||||
background={DataImage.src}
|
||||
backgroundLogo={DataLogoImage.src}
|
||||
Icon={DataIcon}
|
||||
/>
|
||||
<StatCard
|
||||
title="审核工时"
|
||||
timeText={dataText.review1}
|
||||
sizeTitle="审核数据量"
|
||||
sizeValue={data?.review1_data_size ?? 0}
|
||||
sizeColor="#2F9E44"
|
||||
background={HourImage.src}
|
||||
backgroundLogo={HourLogoImage.src}
|
||||
Icon={HourIcon}
|
||||
/>
|
||||
<StatCard
|
||||
title="复审工时"
|
||||
timeText={dataText.review2}
|
||||
sizeTitle="复审数据量"
|
||||
sizeValue={data?.review2_data_size ?? 0}
|
||||
sizeColor="#F08C00"
|
||||
background={ReviewImage.src}
|
||||
backgroundLogo={ReviewLogoImage.src}
|
||||
Icon={ReviewIcon}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Group>
|
||||
<SettingPanel p={0}>
|
||||
<div className={classes.summaryGrid}>
|
||||
<div className={classes.summaryDateCell}>
|
||||
<Text className={classes.summaryDateLabel}>今日</Text>
|
||||
<Text className={classes.summaryDateValue}>
|
||||
{dayjs().format("DD")}
|
||||
</Text>
|
||||
<Text className={classes.summaryDateMeta}>
|
||||
{monthText(dayjs().format("MM"))} · {dayjs().format("YYYY")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, lg: 2 }}
|
||||
spacing="md"
|
||||
style={{ flex: 1, maxHeight: "calc(100% - 194px)" }}>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
{summaryItems.map((item) => (
|
||||
<div key={item.title} className={classes.summaryMetricCell}>
|
||||
<Text className={classes.summaryMetricLabel}>{item.title}</Text>
|
||||
<Text className={classes.summaryMetricValue}>{item.value}</Text>
|
||||
<div className={classes.summaryMetricMeta}>
|
||||
<Text className={classes.summaryMetricMetaLabel}>
|
||||
{item.metaLabel}
|
||||
</Text>
|
||||
<Text className={classes.summaryMetricMetaValue}>
|
||||
{item.metaValue}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingPanel>
|
||||
|
||||
<div className={classes.tableGrid}>
|
||||
<SettingContentPanel p={0} className={classes.dashboardPanel}>
|
||||
<div className={classes.dashboardPanelBody}>
|
||||
<PersonalTaskTable />
|
||||
</div>
|
||||
</Paper>
|
||||
</SettingContentPanel>
|
||||
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<SettingContentPanel p={0} className={classes.dashboardPanel}>
|
||||
<div className={classes.dashboardPanelBody}>
|
||||
<PersonalProjectTable />
|
||||
</div>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</SettingContentPanel>
|
||||
</div>
|
||||
</SettingPage>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user