feat(workload): add workload list
This commit is contained in:
33
app/person/workload/LabelWorkloadTable.tsx
Normal file
33
app/person/workload/LabelWorkloadTable.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { getLabelStatisticsList } from "@/components/label/api/workload"
|
||||||
|
import WorkloadMetricsTable from "./WorkloadMetricsTable"
|
||||||
|
|
||||||
|
export default function LabelWorkloadTable() {
|
||||||
|
return (
|
||||||
|
<WorkloadMetricsTable
|
||||||
|
title="标注工时"
|
||||||
|
sheetName="标注工时数据"
|
||||||
|
fileName="标注工时数据.xlsx"
|
||||||
|
fetchData={getLabelStatisticsList}
|
||||||
|
metrics={[
|
||||||
|
{ key: "work_time", title: "系统工时" },
|
||||||
|
{ key: "object_size", title: "新增对象数" },
|
||||||
|
{ key: "confirmed_prelabel_size", title: "确认预标注对象数" },
|
||||||
|
{ key: "prelabel_object_size", title: "预标注对象数" },
|
||||||
|
{ key: "commit_label_1", title: "提交标注任务数" },
|
||||||
|
{ key: "commit_reviewed_1", title: "被审核任务数" },
|
||||||
|
{ key: "reviewed_size", title: "被审核对象数" },
|
||||||
|
{ key: "commented_size", title: "被批注对象数" },
|
||||||
|
{ key: "rejected_label_1", title: "被驳回任务数" },
|
||||||
|
{ key: "total_reviewed_size", title: "被审核过任务对象总数" },
|
||||||
|
{ key: "dynamic_size", title: "新增动态对象数" },
|
||||||
|
{ key: "static_size", title: "新增静态对象数" },
|
||||||
|
{ key: "key_frame_size", title: "新增关键帧数" },
|
||||||
|
{ key: "question_size", title: "新增问题数" },
|
||||||
|
{ key: "commented_question_size", title: "被批注问题数" },
|
||||||
|
{ key: "reviewed_question_size", title: "被审核问题数" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
app/person/workload/Review1WorkloadTable.tsx
Normal file
35
app/person/workload/Review1WorkloadTable.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { getReview1StatisticsList } from "@/components/label/api/workload"
|
||||||
|
import WorkloadMetricsTable from "./WorkloadMetricsTable"
|
||||||
|
|
||||||
|
export default function Review1WorkloadTable() {
|
||||||
|
return (
|
||||||
|
<WorkloadMetricsTable
|
||||||
|
title="审核工时"
|
||||||
|
sheetName="审核工时数据"
|
||||||
|
fileName="审核工时数据.xlsx"
|
||||||
|
fetchData={getReview1StatisticsList}
|
||||||
|
metrics={[
|
||||||
|
{ key: "work_time", title: "系统工时" },
|
||||||
|
{ key: "object_size", title: "新增对象数" },
|
||||||
|
{ key: "confirmed_prelabel_size", title: "确认预标注对象数" },
|
||||||
|
{ key: "prelabel_object_size", title: "预标注对象数" },
|
||||||
|
{ key: "review_size", title: "审核对象数" },
|
||||||
|
{ key: "comment_size", title: "批注对象数" },
|
||||||
|
{ key: "reviewed_size", title: "被审核对象数" },
|
||||||
|
{ key: "commented_size", title: "被批注对象数" },
|
||||||
|
{ key: "commit_review_1", title: "提交审核任务数" },
|
||||||
|
{ key: "commit_reviewed_2", title: "被复审审核任务数" },
|
||||||
|
{ key: "reject_review_1", title: "审核驳回任务数" },
|
||||||
|
{ key: "rejected_review_1", title: "被驳回的审核任务数" },
|
||||||
|
{ key: "total_review_size", title: "已审核任务对象总数" },
|
||||||
|
{ key: "review_dynamic_size", title: "审核动态对象数" },
|
||||||
|
{ key: "review_static_size", title: "审核静态对象数" },
|
||||||
|
{ key: "total_reviewed_size", title: "已被审核过任务对象总数" },
|
||||||
|
{ key: "review_key_frame_size", title: "审核关键帧数" },
|
||||||
|
{ key: "review_question_size", title: "审核问题数" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
app/person/workload/Review2WorkloadTable.tsx
Normal file
30
app/person/workload/Review2WorkloadTable.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { getReview2StatisticsList } from "@/components/label/api/workload"
|
||||||
|
import WorkloadMetricsTable from "./WorkloadMetricsTable"
|
||||||
|
|
||||||
|
export default function Review2WorkloadTable() {
|
||||||
|
return (
|
||||||
|
<WorkloadMetricsTable
|
||||||
|
title="复审工时"
|
||||||
|
sheetName="复审工时数据"
|
||||||
|
fileName="复审工时数据.xlsx"
|
||||||
|
fetchData={getReview2StatisticsList}
|
||||||
|
metrics={[
|
||||||
|
{ key: "work_time", title: "系统工时" },
|
||||||
|
{ key: "object_size", title: "新增对象数" },
|
||||||
|
{ key: "confirmed_prelabel_size", title: "确认预标注对象数" },
|
||||||
|
{ key: "prelabel_object_size", title: "预标注对象数" },
|
||||||
|
{ key: "review_size", title: "复审对象数" },
|
||||||
|
{ key: "comment_size", title: "批注对象数" },
|
||||||
|
{ key: "commit_review_2", title: "提交复审任务数" },
|
||||||
|
{ key: "reject_review_2", title: "复审驳回任务数" },
|
||||||
|
{ key: "total_review_size", title: "已复审任务对象总数" },
|
||||||
|
{ key: "review_dynamic_size", title: "复审动态对象数" },
|
||||||
|
{ key: "review_static_size", title: "复审静态对象数" },
|
||||||
|
{ key: "review_key_frame_size", title: "复审关键帧数" },
|
||||||
|
{ key: "review_question_size", title: "复审问题数" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
404
app/person/workload/WorkloadMetricsTable.tsx
Normal file
404
app/person/workload/WorkloadMetricsTable.tsx
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { TimeFilter } from "@/components/label/api/workload/typing"
|
||||||
|
import {
|
||||||
|
SettingContentPanel,
|
||||||
|
SettingDataTable,
|
||||||
|
SettingFilterActions,
|
||||||
|
SettingFilterPanel,
|
||||||
|
SettingHeaderActions,
|
||||||
|
SettingInlineFilterField,
|
||||||
|
SettingListHeader,
|
||||||
|
settingSurfaceClassNames,
|
||||||
|
} from "@/components/setting/PageSurface"
|
||||||
|
import { Button, Collapse, Stack, TextInput } from "@mantine/core"
|
||||||
|
import { DateInput } from "@mantine/dates"
|
||||||
|
import { notifications } from "@mantine/notifications"
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronUp,
|
||||||
|
IconDownload,
|
||||||
|
IconRefresh,
|
||||||
|
IconSearch,
|
||||||
|
} from "@tabler/icons-react"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import "dayjs/locale/zh-cn"
|
||||||
|
import { DataTableColumn, DataTableSortStatus } from "mantine-datatable"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import * as XLSX from "xlsx-js-style"
|
||||||
|
import { DimensionType, GroupedData, MetricColumn } from "./config"
|
||||||
|
|
||||||
|
function emptyGroupedData(): GroupedData {
|
||||||
|
return {
|
||||||
|
all: [],
|
||||||
|
order_by_group_name: [],
|
||||||
|
order_by_project_name: [],
|
||||||
|
order_by_project_type: [],
|
||||||
|
order_by_user_name: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialFilters() {
|
||||||
|
const currentDate = dayjs().subtract(1, "day").format("YYYY-MM-DD")
|
||||||
|
return {
|
||||||
|
startDate: currentDate,
|
||||||
|
endDate: currentDate,
|
||||||
|
dimension: "all" as DimensionType,
|
||||||
|
projectName: "",
|
||||||
|
projectType: "",
|
||||||
|
userName: "",
|
||||||
|
groupName: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WorkloadMetricsTable(props: {
|
||||||
|
title: string
|
||||||
|
sheetName: string
|
||||||
|
fileName: string
|
||||||
|
metrics: MetricColumn[]
|
||||||
|
fetchData: (params: TimeFilter) => Promise<GroupedData>
|
||||||
|
}) {
|
||||||
|
const { title, sheetName, fileName, metrics, fetchData } = props
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState(() => createInitialFilters())
|
||||||
|
const [appliedFilters, setAppliedFilters] = useState(() =>
|
||||||
|
createInitialFilters()
|
||||||
|
)
|
||||||
|
const [data, setData] = useState<GroupedData>(emptyGroupedData())
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||||
|
columnAccessor: "work_time",
|
||||||
|
direction: "desc",
|
||||||
|
})
|
||||||
|
|
||||||
|
const refresh = async (nextFilters = appliedFilters) => {
|
||||||
|
if (!nextFilters.startDate || !nextFilters.endDate) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
title: "参数不完整",
|
||||||
|
message: "请选择起止日期",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await fetchData({
|
||||||
|
start_date: nextFilters.startDate,
|
||||||
|
end_date: nextFilters.endDate,
|
||||||
|
})
|
||||||
|
setData({
|
||||||
|
...emptyGroupedData(),
|
||||||
|
...(res ?? {}),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
notifications.show({
|
||||||
|
color: "red",
|
||||||
|
title: "加载失败",
|
||||||
|
message: e instanceof Error ? e.message : "请求失败",
|
||||||
|
})
|
||||||
|
setData(emptyGroupedData())
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialFilters = createInitialFilters()
|
||||||
|
queueMicrotask(() => refresh(initialFilters))
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filteredRecords = useMemo(() => {
|
||||||
|
let records = [...(data[appliedFilters.dimension] ?? [])]
|
||||||
|
if (appliedFilters.projectName.trim()) {
|
||||||
|
records = records.filter((item) =>
|
||||||
|
String(item.project_name ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(appliedFilters.projectName.trim().toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (appliedFilters.projectType.trim()) {
|
||||||
|
records = records.filter((item) =>
|
||||||
|
String(item.project_type ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(appliedFilters.projectType.trim().toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (appliedFilters.userName.trim()) {
|
||||||
|
records = records.filter((item) =>
|
||||||
|
String(item.user_name ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(appliedFilters.userName.trim().toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (appliedFilters.groupName.trim()) {
|
||||||
|
records = records.filter((item) =>
|
||||||
|
String(item.group_name ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(appliedFilters.groupName.trim().toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}, [appliedFilters, data])
|
||||||
|
|
||||||
|
const sortedRecords = useMemo(() => {
|
||||||
|
const list = [...filteredRecords]
|
||||||
|
const accessor = sortStatus.columnAccessor
|
||||||
|
const dir = sortStatus.direction === "asc" ? 1 : -1
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const av = a?.[accessor as string]
|
||||||
|
const bv = b?.[accessor as string]
|
||||||
|
const an = Number(av)
|
||||||
|
const bn = Number(bv)
|
||||||
|
if (Number.isFinite(an) && Number.isFinite(bn)) {
|
||||||
|
return (an - bn) * dir
|
||||||
|
}
|
||||||
|
return String(av ?? "").localeCompare(String(bv ?? "")) * dir
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}, [filteredRecords, sortStatus.columnAccessor, sortStatus.direction])
|
||||||
|
|
||||||
|
const baseColumns = useMemo(() => {
|
||||||
|
const allCols: Record<DimensionType, DataTableColumn<any>[]> = {
|
||||||
|
all: [
|
||||||
|
{ accessor: "project_name", title: "项目名称", width: 180 },
|
||||||
|
{ accessor: "project_type", title: "项目类型", width: 280 },
|
||||||
|
{ accessor: "user_name", title: "用户名", width: 120 },
|
||||||
|
{ accessor: "group_name", title: "用户组", width: 160 },
|
||||||
|
],
|
||||||
|
order_by_group_name: [
|
||||||
|
{ accessor: "group_name", title: "用户组", width: 180 },
|
||||||
|
],
|
||||||
|
order_by_project_name: [
|
||||||
|
{ accessor: "project_name", title: "项目名称", width: 220 },
|
||||||
|
],
|
||||||
|
order_by_project_type: [
|
||||||
|
{ accessor: "project_type", title: "项目类型", width: 320 },
|
||||||
|
],
|
||||||
|
order_by_user_name: [
|
||||||
|
{ accessor: "user_name", title: "用户名", width: 180 },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return allCols[appliedFilters.dimension]
|
||||||
|
}, [appliedFilters.dimension])
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const totalsByKey = Object.fromEntries(
|
||||||
|
metrics.map((metric) => {
|
||||||
|
const sum = sortedRecords.reduce((acc, item) => {
|
||||||
|
const value = Number(item?.[metric.key] ?? 0)
|
||||||
|
return acc + (Number.isFinite(value) ? value : 0)
|
||||||
|
}, 0)
|
||||||
|
return [metric.key, sum]
|
||||||
|
})
|
||||||
|
) as Record<string, number>
|
||||||
|
|
||||||
|
const metricCols = metrics.map((metric) => {
|
||||||
|
return {
|
||||||
|
accessor: metric.key,
|
||||||
|
title: metric.title,
|
||||||
|
width: 140,
|
||||||
|
sortable: true,
|
||||||
|
footer: totalsByKey[metric.key] ?? 0,
|
||||||
|
} as DataTableColumn<any>
|
||||||
|
})
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: "index",
|
||||||
|
title: "序号",
|
||||||
|
width: 80,
|
||||||
|
footer: "总计",
|
||||||
|
render: (_record: any, index: number) => index + 1,
|
||||||
|
} as DataTableColumn<any>,
|
||||||
|
...baseColumns,
|
||||||
|
...metricCols,
|
||||||
|
]
|
||||||
|
}, [baseColumns, metrics, sortedRecords])
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
if (!sortedRecords.length) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
title: "暂无数据",
|
||||||
|
message: "当前筛选条件下无可导出数据",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const baseHeaders = baseColumns.map((c) => String(c.title ?? c.accessor))
|
||||||
|
const metricHeaders = metrics.map((m) => m.title)
|
||||||
|
const header = ["序号", ...baseHeaders, ...metricHeaders]
|
||||||
|
|
||||||
|
const baseAccessors = baseColumns.map((c) => String(c.accessor))
|
||||||
|
const rows = sortedRecords.map((item, index) => {
|
||||||
|
return [
|
||||||
|
index + 1,
|
||||||
|
...baseAccessors.map((key) => item?.[key] ?? "-"),
|
||||||
|
...metrics.map((m) => item?.[m.key] ?? 0),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet([header, ...rows])
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, sheetName)
|
||||||
|
XLSX.writeFile(wb, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKey = useMemo(() => {
|
||||||
|
if (appliedFilters.dimension === "all") {
|
||||||
|
return (record: any) =>
|
||||||
|
`${record.project_id ?? "p"}_${record.uid ?? "u"}_${record.project_name ?? ""}_${record.user_name ?? ""}`
|
||||||
|
}
|
||||||
|
if (appliedFilters.dimension === "order_by_group_name") return "group_name"
|
||||||
|
if (appliedFilters.dimension === "order_by_project_name")
|
||||||
|
return "project_name"
|
||||||
|
if (appliedFilters.dimension === "order_by_project_type")
|
||||||
|
return "project_type"
|
||||||
|
return "user_name"
|
||||||
|
}, [appliedFilters.dimension])
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
setAppliedFilters(filters)
|
||||||
|
void refresh(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
const nextFilters = createInitialFilters()
|
||||||
|
setFilters(nextFilters)
|
||||||
|
setAppliedFilters(nextFilters)
|
||||||
|
void refresh(nextFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [searchExpanded, setSearchExpanded] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack h="100%" gap={12} style={{ minWidth: 0 }}>
|
||||||
|
<SettingFilterPanel>
|
||||||
|
<Stack gap={12}>
|
||||||
|
<div className={settingSurfaceClassNames.filterTopRow}>
|
||||||
|
<SettingInlineFilterField label="开始日期">
|
||||||
|
<DateInput
|
||||||
|
locale="zh-cn"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((s) => ({
|
||||||
|
...s,
|
||||||
|
startDate: dayjs(e).format("YYYY-MM-DD"),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingInlineFilterField>
|
||||||
|
<SettingInlineFilterField label="结束日期">
|
||||||
|
<DateInput
|
||||||
|
locale="zh-cn"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((s) => ({
|
||||||
|
...s,
|
||||||
|
endDate: dayjs(e).format("YYYY-MM-DD"),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingInlineFilterField>
|
||||||
|
<SettingInlineFilterField label="项目名称">
|
||||||
|
<TextInput
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
value={filters.projectName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
projectName: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingInlineFilterField>
|
||||||
|
|
||||||
|
<SettingFilterActions
|
||||||
|
mt={0}
|
||||||
|
className={settingSurfaceClassNames.filterActions}>
|
||||||
|
<Button
|
||||||
|
variant="transparent"
|
||||||
|
className={settingSurfaceClassNames.collapseToggle}
|
||||||
|
rightSection={
|
||||||
|
searchExpanded ? (
|
||||||
|
<IconChevronUp size={16} />
|
||||||
|
) : (
|
||||||
|
<IconChevronDown size={16} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={() => setSearchExpanded((v) => !v)}>
|
||||||
|
{searchExpanded ? "收起" : "展开"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</SettingFilterActions>
|
||||||
|
</div>
|
||||||
|
<Collapse in={searchExpanded} transitionDuration={150} animateOpacity>
|
||||||
|
<div className={settingSurfaceClassNames.filterExpandedGrid}>
|
||||||
|
<SettingInlineFilterField label="项目类型">
|
||||||
|
<TextInput
|
||||||
|
placeholder="请输入项目类型"
|
||||||
|
value={filters.projectType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
projectType: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingInlineFilterField>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
|
</SettingFilterPanel>
|
||||||
|
|
||||||
|
<SettingContentPanel
|
||||||
|
style={{ flex: 1, minHeight: 0, minWidth: 0, display: "flex" }}>
|
||||||
|
<Stack gap={12} style={{ flex: 1, minHeight: 0, minWidth: 0 }}>
|
||||||
|
<SettingListHeader
|
||||||
|
title={title}
|
||||||
|
count={`${sortedRecords.length} 条`}
|
||||||
|
actions={
|
||||||
|
<SettingHeaderActions>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={() => void refresh(appliedFilters)}
|
||||||
|
loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconDownload size={16} />}
|
||||||
|
onClick={handleExport}>
|
||||||
|
下载数据
|
||||||
|
</Button>
|
||||||
|
</SettingHeaderActions>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SettingDataTable<any>
|
||||||
|
fetching={loading}
|
||||||
|
records={sortedRecords}
|
||||||
|
columns={columns}
|
||||||
|
idAccessor={rowKey as any}
|
||||||
|
minHeight={0}
|
||||||
|
scrollAreaProps={{ type: "auto" }}
|
||||||
|
style={{ width: "100%", flex: 1, minWidth: 0, minHeight: 0 }}
|
||||||
|
sortStatus={sortStatus}
|
||||||
|
onSortStatusChange={setSortStatus}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SettingContentPanel>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
app/person/workload/config.ts
Normal file
27
app/person/workload/config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export type DimensionType =
|
||||||
|
| "all"
|
||||||
|
| "order_by_user_name"
|
||||||
|
| "order_by_group_name"
|
||||||
|
| "order_by_project_name"
|
||||||
|
| "order_by_project_type"
|
||||||
|
|
||||||
|
export const dimensionOpts = [
|
||||||
|
{ label: "全部", value: "all" },
|
||||||
|
{ label: "用户名", value: "order_by_user_name" },
|
||||||
|
{ label: "用户组", value: "order_by_group_name" },
|
||||||
|
{ label: "项目名称", value: "order_by_project_name" },
|
||||||
|
{ label: "项目类型", value: "order_by_project_type" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export type MetricColumn = {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GroupedData = {
|
||||||
|
all: any[]
|
||||||
|
order_by_group_name: any[]
|
||||||
|
order_by_project_name: any[]
|
||||||
|
order_by_project_type: any[]
|
||||||
|
order_by_user_name: any[]
|
||||||
|
}
|
||||||
87
app/person/workload/page.tsx
Normal file
87
app/person/workload/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingPage,
|
||||||
|
settingSurfaceClassNames,
|
||||||
|
settingTabsClassNames,
|
||||||
|
settingTabsStyles,
|
||||||
|
} from "@/components/setting/PageSurface"
|
||||||
|
import { Stack, Tabs, Text } from "@mantine/core"
|
||||||
|
import { useState } from "react"
|
||||||
|
import LabelWorkloadTable from "./LabelWorkloadTable"
|
||||||
|
import Review1WorkloadTable from "./Review1WorkloadTable"
|
||||||
|
import Review2WorkloadTable from "./Review2WorkloadTable"
|
||||||
|
|
||||||
|
type WorkloadTab = "label" | "review1" | "review2"
|
||||||
|
|
||||||
|
function createVisitedTabs() {
|
||||||
|
return {
|
||||||
|
label: true,
|
||||||
|
review1: false,
|
||||||
|
review2: false,
|
||||||
|
task: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TeamWorkloadPage() {
|
||||||
|
const moduleTitle = "个人中心"
|
||||||
|
const [activeTab, setActiveTab] = useState<WorkloadTab>("label")
|
||||||
|
const [visitedTabs, setVisitedTabs] = useState(createVisitedTabs)
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
const nextTab = value as WorkloadTab
|
||||||
|
setActiveTab(nextTab)
|
||||||
|
setVisitedTabs((current) =>
|
||||||
|
current[nextTab] ? current : { ...current, [nextTab]: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingPage>
|
||||||
|
<Stack className={settingSurfaceClassNames.pageIntro}>
|
||||||
|
<Text className={settingSurfaceClassNames.breadcrumb}>
|
||||||
|
{moduleTitle} /{" "}
|
||||||
|
<span className={settingSurfaceClassNames.breadcrumbCurrent}>
|
||||||
|
工时列表
|
||||||
|
</span>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
variant="pills"
|
||||||
|
color="#1874FF"
|
||||||
|
autoContrast
|
||||||
|
radius="sm"
|
||||||
|
classNames={settingTabsClassNames}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
...settingTabsStyles,
|
||||||
|
}}>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="label">标注工时</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="review1">审核工时</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="review2">复审工时</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="label">
|
||||||
|
{visitedTabs.label ? <LabelWorkloadTable /> : null}
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="review1">
|
||||||
|
{visitedTabs.review1 ? <Review1WorkloadTable /> : null}
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="review2">
|
||||||
|
{visitedTabs.review2 ? <Review2WorkloadTable /> : null}
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</SettingPage>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
components/label/api/workload/index.ts
Normal file
36
components/label/api/workload/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import httpFetch from "@/api/fetch"
|
||||||
|
import { BASE_LABEL_API } from "../const"
|
||||||
|
import { TimeFilter, WorkLoad } from "./typing"
|
||||||
|
|
||||||
|
// 标注个人工作量统计
|
||||||
|
export const getLabelStatisticsList = (params: TimeFilter) => {
|
||||||
|
return httpFetch<WorkLoad.LabelResponse>({
|
||||||
|
url:
|
||||||
|
BASE_LABEL_API +
|
||||||
|
"/api/v1/label_server/workload/label_statistics/list_by_user",
|
||||||
|
method: "GET",
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核个人工作量统计
|
||||||
|
export const getReview1StatisticsList = (params: TimeFilter) => {
|
||||||
|
return httpFetch<WorkLoad.Review1Response>({
|
||||||
|
url:
|
||||||
|
BASE_LABEL_API +
|
||||||
|
"/api/v1/label_server/workload/review1_statistics/list_by_user",
|
||||||
|
method: "GET",
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复审个人工作量统计
|
||||||
|
export const getReview2StatisticsList = (params: TimeFilter) => {
|
||||||
|
return httpFetch<WorkLoad.Review2Response>({
|
||||||
|
url:
|
||||||
|
BASE_LABEL_API +
|
||||||
|
"/api/v1/label_server/workload/review2_statistics/list_by_user",
|
||||||
|
method: "GET",
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
107
components/label/api/workload/typing.ts
Normal file
107
components/label/api/workload/typing.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
export interface TimeFilter {
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace WorkLoad {
|
||||||
|
export interface LabelData {
|
||||||
|
commented_question_size: number
|
||||||
|
commented_size: number
|
||||||
|
commit_label_1: number
|
||||||
|
commit_reviewed_1: number
|
||||||
|
confirmed_prelabel_size: number
|
||||||
|
dynamic_size: number
|
||||||
|
group_name?: string
|
||||||
|
key_frame_size: number
|
||||||
|
object_size: number
|
||||||
|
prelabel_object_size: number
|
||||||
|
project_id: number
|
||||||
|
project_name: string
|
||||||
|
project_type: string
|
||||||
|
question_size: number
|
||||||
|
rejected_label_1: number
|
||||||
|
reviewed_question_size: number
|
||||||
|
reviewed_size: number
|
||||||
|
static_size: number
|
||||||
|
total_reviewed_size: number
|
||||||
|
uid: number
|
||||||
|
user_name: string
|
||||||
|
work_time: number
|
||||||
|
[property: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelResponse {
|
||||||
|
all: LabelData[]
|
||||||
|
order_by_group_name: any[]
|
||||||
|
order_by_project_name: any[]
|
||||||
|
order_by_project_type: any[]
|
||||||
|
order_by_user_name: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review1Data {
|
||||||
|
comment_size: number
|
||||||
|
commented_size: number
|
||||||
|
commit_review_1: number
|
||||||
|
commit_reviewed_2: number
|
||||||
|
confirmed_prelabel_size: number
|
||||||
|
group_name?: string
|
||||||
|
object_size: number
|
||||||
|
prelabel_object_size: number
|
||||||
|
project_id: number
|
||||||
|
project_name: string
|
||||||
|
project_type: string
|
||||||
|
reject_review_1: number
|
||||||
|
rejected_review_1: number
|
||||||
|
review_dynamic_size: number
|
||||||
|
review_key_frame_size: number
|
||||||
|
review_question_size: number
|
||||||
|
review_size: number
|
||||||
|
review_static_size: number
|
||||||
|
reviewed_size: number
|
||||||
|
total_review_size: number
|
||||||
|
total_reviewed_size: number
|
||||||
|
uid: number
|
||||||
|
user_name: string
|
||||||
|
work_time: number
|
||||||
|
[property: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review1Response {
|
||||||
|
all: Review1Data[]
|
||||||
|
order_by_group_name: any[]
|
||||||
|
order_by_project_name: any[]
|
||||||
|
order_by_project_type: any[]
|
||||||
|
order_by_user_name: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review2Data {
|
||||||
|
comment_size: number
|
||||||
|
commit_review_2: number
|
||||||
|
confirmed_prelabel_size: number
|
||||||
|
group_name?: string
|
||||||
|
object_size: number
|
||||||
|
prelabel_object_size: number
|
||||||
|
project_id: number
|
||||||
|
project_name: string
|
||||||
|
project_type: string
|
||||||
|
reject_review_2: number
|
||||||
|
review_dynamic_size: number
|
||||||
|
review_key_frame_size: number
|
||||||
|
review_question_size: number
|
||||||
|
review_size: number
|
||||||
|
review_static_size: number
|
||||||
|
total_review_size: number
|
||||||
|
uid: number
|
||||||
|
user_name: string
|
||||||
|
work_time: number
|
||||||
|
[property: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review2Response {
|
||||||
|
all: Review2Data[]
|
||||||
|
order_by_group_name: any[]
|
||||||
|
order_by_project_name: any[]
|
||||||
|
order_by_project_type: any[]
|
||||||
|
order_by_user_name: any[]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,11 @@ const currentComponentList: MenuItem[] = [
|
|||||||
url: "report",
|
url: "report",
|
||||||
icon: "ReportIcon",
|
icon: "ReportIcon",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "工时列表",
|
||||||
|
url: "workload",
|
||||||
|
icon: "WorkloadIcon",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
IconHierarchy3,
|
IconHierarchy3,
|
||||||
IconLayoutDashboard,
|
IconLayoutDashboard,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
|
IconReportMedical,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
@@ -26,6 +27,7 @@ const iconMap = {
|
|||||||
ProjectCategoryIcon: IconCategory2,
|
ProjectCategoryIcon: IconCategory2,
|
||||||
ProjectManagementIcon: IconFolders,
|
ProjectManagementIcon: IconFolders,
|
||||||
ReportIcon: IconFileText,
|
ReportIcon: IconFileText,
|
||||||
|
WorkloadIcon: IconReportMedical,
|
||||||
TeamManagementIcon: IconHierarchy3,
|
TeamManagementIcon: IconHierarchy3,
|
||||||
VideoAnnotationIcon: IconVideo,
|
VideoAnnotationIcon: IconVideo,
|
||||||
SystemIcon: IconSettings,
|
SystemIcon: IconSettings,
|
||||||
|
|||||||
@@ -13,6 +13,35 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pageIntro {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #86909c;
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink:hover {
|
||||||
|
color: #1874ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbCurrent {
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
.modalContent {
|
.modalContent {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -84,10 +113,517 @@
|
|||||||
color: #c9cdd4 !important;
|
color: #c9cdd4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modalForm {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalMetaText {
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalActions {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-InputWrapper-label) {
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Switch-label),
|
||||||
|
.modalForm :global(.mantine-Checkbox-label) {
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Input-input),
|
||||||
|
.modalForm :global(.mantine-Select-input),
|
||||||
|
.modalForm :global(.mantine-DateInput-input),
|
||||||
|
.modalForm :global(.mantine-NumberInput-input),
|
||||||
|
.modalForm :global(.mantine-PillsInput-root),
|
||||||
|
.modalForm :global(.mantine-Textarea-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-PillsInput-root) {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-PillsInput-input) {
|
||||||
|
min-height: 30px;
|
||||||
|
background: transparent;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Textarea-input) {
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Input-input::placeholder),
|
||||||
|
.modalForm :global(.mantine-Select-input::placeholder),
|
||||||
|
.modalForm :global(.mantine-DateInput-input::placeholder),
|
||||||
|
.modalForm :global(.mantine-NumberInput-input::placeholder),
|
||||||
|
.modalForm :global(.mantine-PillsInput-input::placeholder),
|
||||||
|
.modalForm :global(.mantine-Textarea-input::placeholder) {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Input-input:focus),
|
||||||
|
.modalForm :global(.mantine-Select-input:focus),
|
||||||
|
.modalForm :global(.mantine-DateInput-input:focus),
|
||||||
|
.modalForm :global(.mantine-NumberInput-input:focus),
|
||||||
|
.modalForm :global(.mantine-PillsInput-root:focus-within),
|
||||||
|
.modalForm :global(.mantine-Textarea-input:focus) {
|
||||||
|
border-color: #1874ff;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Input-input[readonly]),
|
||||||
|
.modalForm :global(.mantine-DateInput-input[readonly]),
|
||||||
|
.modalForm :global(.mantine-NumberInput-input[readonly]),
|
||||||
|
.modalForm :global(.mantine-Textarea-input[readonly]) {
|
||||||
|
background: #f7f8fa;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Input-section),
|
||||||
|
.modalForm :global(.mantine-Select-section),
|
||||||
|
.modalForm :global(.mantine-DateInput-section),
|
||||||
|
.modalForm :global(.mantine-NumberInput-section) {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Switch-root) {
|
||||||
|
--switch-height: 20px;
|
||||||
|
--switch-width: 34px;
|
||||||
|
--switch-thumb-size: 14px;
|
||||||
|
--switch-radius: 999px;
|
||||||
|
--switch-color: #1874ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Checkbox-root) {
|
||||||
|
--checkbox-color: #1874ff;
|
||||||
|
--checkbox-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Checkbox-input) {
|
||||||
|
border-color: #c9cdd4;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Button-root) {
|
||||||
|
min-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Button-root[data-variant="filled"]:hover) {
|
||||||
|
background: #0f6ae8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Button-root[data-variant="default"]) {
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Button-root[data-disabled]),
|
||||||
|
.modalForm :global(.mantine-Button-root:disabled) {
|
||||||
|
border-color: #e5e6eb !important;
|
||||||
|
background: #f2f3f5 !important;
|
||||||
|
color: #c9cdd4 !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalForm :global(.mantine-Button-root[data-disabled]:hover),
|
||||||
|
.modalForm :global(.mantine-Button-root:disabled:hover) {
|
||||||
|
border-color: #e5e6eb !important;
|
||||||
|
background: #f2f3f5 !important;
|
||||||
|
color: #c9cdd4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.filterPanel {
|
.filterPanel {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailContent {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailIntroGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailSection {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailActionBar {
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailScrollArea {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readValue {
|
||||||
|
min-height: 32px;
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readValueText {
|
||||||
|
width: 100%;
|
||||||
|
color: #1d2129;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-InputWrapper-label) {
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Input-input),
|
||||||
|
.detailForm :global(.mantine-Select-input),
|
||||||
|
.detailForm :global(.mantine-ColorInput-input),
|
||||||
|
.detailForm :global(.mantine-NumberInput-input),
|
||||||
|
.detailForm :global(.mantine-PillsInput-root),
|
||||||
|
.detailForm :global(.mantine-Textarea-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-PillsInput-root) {
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-PillsInput-input) {
|
||||||
|
min-height: 30px;
|
||||||
|
background: transparent;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Textarea-input) {
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Input-input::placeholder),
|
||||||
|
.detailForm :global(.mantine-Select-input::placeholder),
|
||||||
|
.detailForm :global(.mantine-ColorInput-input::placeholder),
|
||||||
|
.detailForm :global(.mantine-NumberInput-input::placeholder),
|
||||||
|
.detailForm :global(.mantine-PillsInput-input::placeholder),
|
||||||
|
.detailForm :global(.mantine-Textarea-input::placeholder) {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Input-input:focus),
|
||||||
|
.detailForm :global(.mantine-Select-input:focus),
|
||||||
|
.detailForm :global(.mantine-ColorInput-input:focus),
|
||||||
|
.detailForm :global(.mantine-NumberInput-input:focus),
|
||||||
|
.detailForm :global(.mantine-PillsInput-root:focus-within),
|
||||||
|
.detailForm :global(.mantine-Textarea-input:focus) {
|
||||||
|
border-color: #1874ff;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Input-input[readonly]),
|
||||||
|
.detailForm :global(.mantine-ColorInput-input[readonly]),
|
||||||
|
.detailForm :global(.mantine-NumberInput-input[readonly]),
|
||||||
|
.detailForm :global(.mantine-Textarea-input[readonly]) {
|
||||||
|
background: #f7f8fa;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Input-section),
|
||||||
|
.detailForm :global(.mantine-Select-section),
|
||||||
|
.detailForm :global(.mantine-ColorInput-section),
|
||||||
|
.detailForm :global(.mantine-NumberInput-section) {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Pill-root) {
|
||||||
|
background: #e7efff;
|
||||||
|
color: #1874ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Button-root) {
|
||||||
|
min-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Button-root[data-variant="filled"]:hover) {
|
||||||
|
background: #0f6ae8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Button-root[data-variant="default"]),
|
||||||
|
.detailForm :global(.mantine-Button-root[data-variant="light"]) {
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-ActionIcon-root) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Table-table) {
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
background: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Table-th) {
|
||||||
|
background: #f7f8fa;
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Table-th),
|
||||||
|
.detailForm :global(.mantine-Table-td) {
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailForm :global(.mantine-Table-td) {
|
||||||
|
color: #1d2129;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableViewport {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterStack {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterTopRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterExpandedGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px 20px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterField {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
flex: 0 0 var(--setting-inline-filter-label-width, 80px);
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterControl {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterControl :global(.mantine-InputWrapper-root),
|
||||||
|
.filterControl :global(.mantine-PillsInput-root) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterControl :global(.mantine-InputWrapper-label) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterActionsAlignEnd {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: end;
|
||||||
|
grid-column: -2 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapseToggle {
|
||||||
|
min-width: auto;
|
||||||
|
padding-inline: 0;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationGroup {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationLink {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #1874ff;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationLinkDanger {
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationLinkDisabled {
|
||||||
|
color: #c9cdd4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationDivider {
|
||||||
|
width: 1px;
|
||||||
|
height: 12px;
|
||||||
|
background: #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPositive {
|
||||||
|
background: #e8ffea;
|
||||||
|
color: #00b42a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusNegative {
|
||||||
|
background: #ffece8;
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusWarning {
|
||||||
|
background: #fff7e8;
|
||||||
|
color: #ff7d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusInfo {
|
||||||
|
background: #e8f3ff;
|
||||||
|
color: #1874ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusNeutral {
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
.filterPanel :global(.mantine-InputWrapper-label) {
|
.filterPanel :global(.mantine-InputWrapper-label) {
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -235,6 +771,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contentPanel :global(.mantine-datatable-pagination) {
|
.contentPanel :global(.mantine-datatable-pagination) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
padding: 12px 16px !important;
|
padding: 12px 16px !important;
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
}
|
}
|
||||||
@@ -274,6 +815,21 @@
|
|||||||
--pagination-control-fz: 12px;
|
--pagination-control-fz: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contentPanel :global(.mantine-datatable-pagination .mantine-Select-input) {
|
||||||
|
min-height: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-color: #e5e6eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPanel :global(.mantine-datatable-pagination .mantine-Select-section) {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
.contentPanel :global(.mantine-Pagination-dots) {
|
.contentPanel :global(.mantine-Pagination-dots) {
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
}
|
}
|
||||||
@@ -327,6 +883,19 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listHeaderTitle {
|
||||||
|
color: #1d2129;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeaderCount {
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid #e5e6eb;
|
border-bottom: 1px solid #e5e6eb;
|
||||||
@@ -354,38 +923,42 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listHeaderTitle {
|
|
||||||
color: #1d2129;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listHeaderCount {
|
|
||||||
color: #86909c;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingTabsList {
|
.settingTabsList {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f7f8fa;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
border: 1px solid #eef2f6;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f8f9fb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingTabsTab {
|
.settingTabsTab {
|
||||||
min-height: 36px;
|
min-height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
color: #56606a;
|
padding: 0 12px;
|
||||||
font-weight: 600;
|
color: #4e5969;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingTabsTab[data-active] {
|
.settingTabsTab[data-active],
|
||||||
background: #ffffff;
|
.settingTabsTab[data-active]:hover {
|
||||||
color: #1f2329;
|
background: #1874ff;
|
||||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingTabsTabLabel {
|
||||||
|
color: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settingTabsTab[data-active] .settingTabsTabLabel,
|
||||||
|
.settingTabsTab[data-active]:hover .settingTabsTabLabel {
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingTabsPanel {
|
.settingTabsPanel {
|
||||||
@@ -394,3 +967,39 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.filterTopRow,
|
||||||
|
.filterExpandedGrid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterActionsAlignEnd {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.detailIntroGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterTopRow,
|
||||||
|
.filterExpandedGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterField {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterActionsAlignEnd {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,18 +9,36 @@ import {
|
|||||||
type PaperProps,
|
type PaperProps,
|
||||||
type StackProps,
|
type StackProps,
|
||||||
} from "@mantine/core"
|
} from "@mantine/core"
|
||||||
import type { PropsWithChildren, ReactNode } from "react"
|
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
type DataTableDefaultColumnProps,
|
type DataTableDefaultColumnProps,
|
||||||
type DataTableProps,
|
type DataTableProps,
|
||||||
} from "mantine-datatable"
|
} from "mantine-datatable"
|
||||||
|
import type { CSSProperties, PropsWithChildren, ReactNode } from "react"
|
||||||
import classes from "./PageSurface.module.css"
|
import classes from "./PageSurface.module.css"
|
||||||
|
|
||||||
function mergeClassName(...classNames: Array<string | undefined>) {
|
function mergeClassName(...classNames: Array<string | undefined>) {
|
||||||
return classNames.filter(Boolean).join(" ")
|
return classNames.filter(Boolean).join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const settingModalClassNames = {
|
||||||
|
content: classes.modalContent,
|
||||||
|
header: classes.modalHeader,
|
||||||
|
title: classes.modalTitle,
|
||||||
|
body: classes.modalBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingModalFormClassName = classes.modalForm
|
||||||
|
export const settingModalActionsClassName = classes.modalActions
|
||||||
|
export const settingModalMetaTextClassName = classes.modalMetaText
|
||||||
|
export const settingSurfaceClassNames = classes
|
||||||
|
|
||||||
|
type SettingTableViewportProps = PropsWithChildren<{
|
||||||
|
maxHeight: number | string
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
}>
|
||||||
|
|
||||||
export function SettingPage({
|
export function SettingPage({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
@@ -57,6 +75,21 @@ export function SettingPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingTableViewport({
|
||||||
|
maxHeight,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
}: SettingTableViewportProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ maxHeight, ...style }}
|
||||||
|
className={mergeClassName(classes.tableViewport, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function SettingFilterPanel({
|
export function SettingFilterPanel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
@@ -183,6 +216,56 @@ export function SettingFilterActions({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingFilterStack({
|
||||||
|
className,
|
||||||
|
gap = 12,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<StackProps>) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={gap}
|
||||||
|
className={mergeClassName(classes.filterStack, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingInlineFilterFieldProps = {
|
||||||
|
label: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
labelClassName?: string
|
||||||
|
controlClassName?: string
|
||||||
|
labelWidth?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingInlineFilterField({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
controlClassName,
|
||||||
|
labelWidth = 80,
|
||||||
|
}: SettingInlineFilterFieldProps) {
|
||||||
|
const style = {
|
||||||
|
"--setting-inline-filter-label-width":
|
||||||
|
typeof labelWidth === "number" ? `${labelWidth}px` : labelWidth,
|
||||||
|
} as CSSProperties
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className={mergeClassName(classes.filterField, className)}>
|
||||||
|
<Text className={mergeClassName(classes.filterLabel, labelClassName)}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<div className={mergeClassName(classes.filterControl, controlClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const settingDataTableStyles = {
|
const settingDataTableStyles = {
|
||||||
header: {
|
header: {
|
||||||
backgroundColor: "#F7F8FA",
|
backgroundColor: "#F7F8FA",
|
||||||
@@ -192,7 +275,6 @@ const settingDataTableStyles = {
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
height: "100%",
|
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
@@ -238,12 +320,15 @@ export function SettingDataTable<T = Record<string, unknown>>(
|
|||||||
loadingText,
|
loadingText,
|
||||||
noRecordsText,
|
noRecordsText,
|
||||||
minHeight,
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
height,
|
||||||
page,
|
page,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
recordsPerPage,
|
recordsPerPage,
|
||||||
recordsPerPageLabel,
|
recordsPerPageLabel,
|
||||||
paginationText,
|
paginationText,
|
||||||
|
scrollAreaProps,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
@@ -268,7 +353,7 @@ export function SettingDataTable<T = Record<string, unknown>>(
|
|||||||
from: number
|
from: number
|
||||||
to: number
|
to: number
|
||||||
totalRecords: number
|
totalRecords: number
|
||||||
}) => `${from}-${to} / ${totalRecords}`),
|
}) => `显示 ${from}-${to} 条,共 ${totalRecords} 条记录`),
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
@@ -283,6 +368,12 @@ export function SettingDataTable<T = Record<string, unknown>>(
|
|||||||
loaderBackgroundBlur: loaderBackgroundBlur ?? 0,
|
loaderBackgroundBlur: loaderBackgroundBlur ?? 0,
|
||||||
noRecordsText: noRecordsText ?? "暂无数据",
|
noRecordsText: noRecordsText ?? "暂无数据",
|
||||||
minHeight: minHeight ?? 360,
|
minHeight: minHeight ?? 360,
|
||||||
|
maxHeight,
|
||||||
|
height: height ?? (maxHeight !== undefined ? "auto" : undefined),
|
||||||
|
scrollAreaProps: {
|
||||||
|
type: "auto",
|
||||||
|
...scrollAreaProps,
|
||||||
|
},
|
||||||
styles: styles
|
styles: styles
|
||||||
? { ...settingDataTableStyles, ...styles }
|
? { ...settingDataTableStyles, ...styles }
|
||||||
: settingDataTableStyles,
|
: settingDataTableStyles,
|
||||||
@@ -296,16 +387,23 @@ export function SettingDataTable<T = Record<string, unknown>>(
|
|||||||
export const settingTabsStyles = {
|
export const settingTabsStyles = {
|
||||||
list: {
|
list: {
|
||||||
padding: 4,
|
padding: 4,
|
||||||
backgroundColor: "#F8F9FB",
|
backgroundColor: "#F7F8FA",
|
||||||
border: "1px solid #EEF2F6",
|
border: "1px solid #E5E6EB",
|
||||||
borderRadius: 10,
|
borderRadius: 4,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
tab: {
|
tab: {
|
||||||
minHeight: 36,
|
minHeight: 32,
|
||||||
borderRadius: 8,
|
borderRadius: 4,
|
||||||
color: "#56606A",
|
padding: "0 12px",
|
||||||
fontWeight: 600,
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "20px",
|
||||||
|
},
|
||||||
|
tabLabel: {
|
||||||
|
color: "inherit",
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: "20px",
|
||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -318,5 +416,6 @@ export const settingTabsStyles = {
|
|||||||
export const settingTabsClassNames = {
|
export const settingTabsClassNames = {
|
||||||
list: classes.settingTabsList,
|
list: classes.settingTabsList,
|
||||||
tab: classes.settingTabsTab,
|
tab: classes.settingTabsTab,
|
||||||
|
tabLabel: classes.settingTabsTabLabel,
|
||||||
panel: classes.settingTabsPanel,
|
panel: classes.settingTabsPanel,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
Reference in New Issue
Block a user