"use client" import { TimeFilter } from "@/components/label/api/workload/typing" import { Button, Group, Paper, Select, SimpleGrid, Stack, Text, TextInput, } from "@mantine/core" import { notifications } from "@mantine/notifications" import { IconDownload, IconRefresh } from "@tabler/icons-react" import dayjs from "dayjs" import { DataTable, DataTableColumn, DataTableSortStatus, } from "mantine-datatable" import { useEffect, useMemo, useState } from "react" import * as XLSX from "xlsx-js-style" import { DimensionType, GroupedData, MetricColumn, dimensionOpts } from "./config" function emptyGroupedData(): GroupedData { return { all: [], order_by_group_name: [], order_by_project_name: [], order_by_project_type: [], order_by_user_name: [], } } export default function WorkloadMetricsTable(props: { title: string sheetName: string fileName: string metrics: MetricColumn[] fetchData: (params: TimeFilter) => Promise }) { const { title, sheetName, fileName, metrics, fetchData } = props const [startDate, setStartDate] = useState(dayjs().subtract(1, "day").format("YYYY-MM-DD")) const [endDate, setEndDate] = useState(dayjs().subtract(1, "day").format("YYYY-MM-DD")) const [dimension, setDimension] = useState("all") const [data, setData] = useState(emptyGroupedData()) const [loading, setLoading] = useState(false) const [projectName, setProjectName] = useState("") const [projectType, setProjectType] = useState("") const [userName, setUserName] = useState("") const [groupName, setGroupName] = useState("") const [sortStatus, setSortStatus] = useState({ columnAccessor: "work_time", direction: "desc", }) const refresh = async () => { if (!startDate || !endDate) { notifications.show({ color: "yellow", title: "参数不完整", message: "请选择起止日期", }) return } try { setLoading(true) const res = await fetchData({ start_date: startDate, end_date: endDate, }) setData({ ...emptyGroupedData(), ...(res ?? {}), }) } catch (e) { notifications.show({ color: "red", title: "加载失败", message: e instanceof Error ? e.message : "请求失败", }) setData(emptyGroupedData()) } finally { setLoading(false) } } useEffect(() => { queueMicrotask(refresh) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const filteredRecords = useMemo(() => { let records = [...(data[dimension] ?? [])] if (projectName.trim()) { records = records.filter((item) => String(item.project_name ?? "") .toLowerCase() .includes(projectName.trim().toLowerCase()) ) } if (projectType.trim()) { records = records.filter((item) => String(item.project_type ?? "") .toLowerCase() .includes(projectType.trim().toLowerCase()) ) } if (userName.trim()) { records = records.filter((item) => String(item.user_name ?? "") .toLowerCase() .includes(userName.trim().toLowerCase()) ) } if (groupName.trim()) { records = records.filter((item) => String(item.group_name ?? "") .toLowerCase() .includes(groupName.trim().toLowerCase()) ) } return records }, [data, dimension, groupName, projectName, projectType, userName]) 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[]> = { all: [ { accessor: "project_name", title: "项目名称", width: 180 }, { accessor: "project_type", title: "项目类型", width: 180 }, { 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: 220 }, ], order_by_user_name: [{ accessor: "user_name", title: "用户名", width: 180 }], } return allCols[dimension] }, [dimension]) const columns = useMemo(() => { const metricCols = metrics.map((metric) => { return { accessor: metric.key, title: metric.title, width: 140, sortable: true, } as DataTableColumn }) return [ { accessor: "index", title: "序号", width: 80, render: (_record: any, index: number) => index + 1, } as DataTableColumn, ...baseColumns, ...metricCols, ] }, [baseColumns, metrics]) const totals = useMemo(() => { return 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 { title: metric.title, value: sum } }) }, [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 (dimension === "all") { return (record: any) => `${record.project_id ?? "p"}_${record.uid ?? "u"}_${record.project_name ?? ""}_${record.user_name ?? ""}` } if (dimension === "order_by_group_name") return "group_name" if (dimension === "order_by_project_name") return "project_name" if (dimension === "order_by_project_type") return "project_type" return "user_name" }, [dimension]) return ( {title} setStartDate(e.currentTarget.value)} /> setEndDate(e.currentTarget.value)} />