diff --git a/app/management/team/board/page.tsx b/app/management/team/board/page.tsx
new file mode 100644
index 0000000..74a0f55
--- /dev/null
+++ b/app/management/team/board/page.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { Center, Paper, Stack, Text } from "@mantine/core"
+
+export default function TeamBoardPage() {
+ return (
+
+
+
+ 项目参与
+
+ 该页面已迁移至团队管理模块,后续可在此继续扩展业务视图。
+
+
+
+
+ )
+}
diff --git a/app/management/team/cost/page.tsx b/app/management/team/cost/page.tsx
new file mode 100644
index 0000000..a707664
--- /dev/null
+++ b/app/management/team/cost/page.tsx
@@ -0,0 +1,197 @@
+"use client"
+
+import {
+ downloadSettlementForm,
+ exportSettlementForm,
+ getDownloadLog,
+} from "@/components/label/api/workload"
+import useAuth from "@/components/label/hooks/useAuth"
+import {
+ ActionIcon,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+} from "@mantine/core"
+import { notifications } from "@mantine/notifications"
+import { IconDownload, IconFileExport, IconRefresh } from "@tabler/icons-react"
+import dayjs from "dayjs"
+import { DataTable, DataTableColumn } from "mantine-datatable"
+import { useCallback, useEffect, useMemo, useState } from "react"
+
+function triggerDownload(blob: Blob, fileName: string) {
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.href = url
+ link.download = fileName
+ link.click()
+ window.URL.revokeObjectURL(url)
+}
+
+type LogRecord = {
+ id: number
+ create_at: string
+ file_name: string
+ create_user: string
+}
+
+export default function TeamCostPage() {
+ const { isShow } = useAuth()
+
+ const [records, setRecords] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [exporting, setExporting] = useState(false)
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true)
+ const res = await getDownloadLog()
+ const list = (res ?? []) as LogRecord[]
+ setRecords(list.sort((a, b) => (b.id ?? 0) - (a.id ?? 0)))
+ } catch (e) {
+ setRecords([])
+ notifications.show({
+ color: "red",
+ title: "加载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ load()
+ }, [load])
+
+ const handleExport = async () => {
+ try {
+ setExporting(true)
+ const time = dayjs().subtract(1, "month")
+ const year = time.year()
+ const month = time.month() + 1
+ const blob = await exportSettlementForm({ year, month })
+ const fileName = `结算表格_${year}_${String(month).padStart(2, "0")}.zip`
+ triggerDownload(blob, fileName)
+ notifications.show({
+ color: "green",
+ title: "导出成功",
+ message: "已开始下载结算表格",
+ })
+ load()
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "导出失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setExporting(false)
+ }
+ }
+
+ const columns = useMemo(() => {
+ const cols: DataTableColumn[] = [
+ { accessor: "id", title: "序号", width: 80 },
+ {
+ accessor: "create_at",
+ title: "导出时间",
+ width: 200,
+ render: (record) =>
+ record.create_at ? dayjs(record.create_at).format("YYYY-MM-DD HH:mm:ss") : "-",
+ },
+ { accessor: "file_name", title: "导出文件名", width: 280 },
+ { accessor: "create_user", title: "操作人", width: 160 },
+ {
+ accessor: "operation",
+ title: "操作",
+ width: 90,
+ textAlign: "center",
+ render: (record) => (
+ {
+ try {
+ const blob = await downloadSettlementForm({ id: record.id })
+ const fileName = record.file_name || `结算表格_${record.id}.zip`
+ triggerDownload(blob, fileName)
+ notifications.show({
+ color: "green",
+ title: "下载成功",
+ message: "已开始下载文件",
+ })
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "下载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ }
+ }}>
+
+
+ ),
+ },
+ ]
+ return cols
+ }, [isShow])
+
+ return (
+
+
+
+
+ }
+ onClick={handleExport}
+ loading={exporting}
+ disabled={!isShow("export")}>
+ 结算表格导出
+
+
+
+
+
+
+ 导出记录
+
+
+
+
+
+
+ withTableBorder
+ withRowBorders
+ fetching={loading}
+ records={records}
+ columns={columns}
+ noRecordsText="暂无数据"
+ scrollAreaProps={{ type: "auto" }}
+ style={{ width: "100%" }}
+ />
+
+
+ )
+}
diff --git a/app/management/team/dailypaper/page.tsx b/app/management/team/dailypaper/page.tsx
index c8c76c8..0911345 100644
--- a/app/management/team/dailypaper/page.tsx
+++ b/app/management/team/dailypaper/page.tsx
@@ -1,5 +1,730 @@
"use client"
-export default function DailypaperPage() {
- return <>>
+import {
+ deleteDailyWorkData,
+ getDailyWorkList,
+ lockDailyWorkData,
+} from "@/components/label/api/daily"
+import { Daily } from "@/components/label/api/daily/typing"
+import { getSelfProjectList } from "@/components/label/api/project"
+import useAuth from "@/components/label/hooks/useAuth"
+import { useAllUserStore } from "@/components/label/store/auth"
+import {
+ ActionIcon,
+ Badge,
+ Button,
+ Collapse,
+ Flex,
+ Group,
+ MultiSelect,
+ Paper,
+ SimpleGrid,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core"
+import { modals } from "@mantine/modals"
+import { notifications } from "@mantine/notifications"
+import {
+ IconChevronDown,
+ IconChevronUp,
+ IconEdit,
+ IconLock,
+ IconLockOpen,
+ IconRefresh,
+ IconSearch,
+ IconTrash,
+} from "@tabler/icons-react"
+import dayjs from "dayjs"
+import {
+ DataTable,
+ DataTableColumn,
+ DataTableSortStatus,
+} from "mantine-datatable"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import * as XLSX from "xlsx-js-style"
+import DailyReportModal from "../../person/report/components/DailyReportModal"
+import {
+ accountTypeOpts,
+ actualTypeOpts,
+ objTypeOpts,
+ objTypeTextMap,
+ performanceOpts,
+ performanceTextMap,
+ setTypeOpts,
+ workTypeOpts,
+} from "../../person/report/config"
+import { flattenLeaderTree, toNumberArray, weekDayText } from "../utils"
+
+function toBoolean(value?: string) {
+ if (value === "true") return true
+ if (value === "false") return false
+ return undefined
+}
+
+function toNumber(value?: string) {
+ if (!value) return undefined
+ const n = Number(value)
+ return Number.isFinite(n) ? n : undefined
+}
+
+const setTypeMap = new Map(setTypeOpts.map((i) => [i.value, i.label]))
+const actualTypeMap = new Map(actualTypeOpts.map((i) => [i.value, i.label]))
+const workTypeMap = new Map(workTypeOpts.map((i) => [i.value, i.label]))
+const accountTypeMap = new Map(accountTypeOpts.map((i) => [i.value, i.label]))
+
+export default function TeamDailypaperPage() {
+ const { isShow } = useAuth()
+ const treeData = useAllUserStore((s) => s.treeData)
+
+ const leaderOptions = useMemo(() => flattenLeaderTree(treeData ?? []), [treeData])
+
+ const defaultFilters = useMemo(() => {
+ return {
+ date_start: dayjs().subtract(6, "day").format("YYYY-MM-DD"),
+ date_end: dayjs().format("YYYY-MM-DD"),
+ uid: [] as string[],
+ set_type: "",
+ actual_type: "",
+ leader_uid: [] as string[],
+ work_type: "",
+ accounting_type: "",
+ project_id: [] as string[],
+ performance_accounting: "",
+ obj_type: "",
+ }
+ }, [])
+
+ const [projectOptions, setProjectOptions] = useState<
+ Array<{ label: string; value: string }>
+ >([])
+
+ const [filters, setFilters] = useState(defaultFilters)
+ const [appliedFilters, setAppliedFilters] = useState(defaultFilters)
+ const [queryTrigger, setQueryTrigger] = useState(0)
+
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const [totalItems, setTotalItems] = useState(0)
+ const [loading, setLoading] = useState(false)
+ const [records, setRecords] = useState([])
+ const [summary, setSummary] = useState(null)
+
+ const [sortStatus, setSortStatus] = useState({
+ columnAccessor: "id",
+ direction: "desc",
+ })
+
+ const [modalOpened, setModalOpened] = useState(false)
+ const [editingRecord, setEditingRecord] = useState(null)
+ const [searchExpanded, setSearchExpanded] = useState(false)
+
+ const queryParams = useMemo(() => {
+ const params: Daily.Request = {
+ page_number: page,
+ page_size: pageSize,
+ flag: 1,
+ date_start: appliedFilters.date_start || undefined,
+ date_end: appliedFilters.date_end || undefined,
+ uid: toNumberArray(appliedFilters.uid),
+ set_type: appliedFilters.set_type || undefined,
+ actual_type: appliedFilters.actual_type || undefined,
+ leader_uid: toNumberArray(appliedFilters.leader_uid),
+ work_type: appliedFilters.work_type || undefined,
+ accounting_type: appliedFilters.accounting_type || undefined,
+ project_id: toNumberArray(appliedFilters.project_id),
+ performance_accounting: toBoolean(appliedFilters.performance_accounting),
+ obj_type: toNumber(appliedFilters.obj_type),
+ order_by: `${sortStatus.columnAccessor} ${sortStatus.direction}`,
+ }
+ Object.keys(params).forEach((k) => {
+ const key = k as keyof Daily.Request
+ const value = params[key]
+ if (value === "" || value === null) delete params[key]
+ if (Array.isArray(value) && value.length === 0) delete params[key]
+ if (value !== 0 && value !== false && value !== "" && !value) {
+ delete params[key]
+ }
+ })
+ return params
+ }, [appliedFilters, page, pageSize, sortStatus.columnAccessor, sortStatus.direction])
+
+ const load = useCallback(async () => {
+ if (!queryTrigger) return
+ setLoading(true)
+ try {
+ const res = await getDailyWorkList(queryParams)
+ setRecords(res?.daily_work_list ?? [])
+ setTotalItems(res?.total_items ?? 0)
+ setSummary(res?.total_summary ?? null)
+ } catch (e) {
+ setRecords([])
+ setTotalItems(0)
+ setSummary(null)
+ notifications.show({
+ color: "red",
+ title: "加载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setLoading(false)
+ }
+ }, [queryParams, queryTrigger])
+
+ useEffect(() => {
+ load()
+ }, [load])
+
+ useEffect(() => {
+ queueMicrotask(() => setQueryTrigger((v) => v + 1))
+ }, [])
+
+ useEffect(() => {
+ const run = async () => {
+ const res = await getSelfProjectList()
+ setProjectOptions((res ?? []).map((p) => ({ label: p.label, value: String(p.value) })))
+ }
+ queueMicrotask(run)
+ }, [])
+
+ const handleExportData = () => {
+ if (!records.length) {
+ notifications.show({
+ color: "yellow",
+ title: "暂无数据",
+ message: "当前无可导出日报数据",
+ })
+ return
+ }
+ const exportTableHeader = [
+ "员工姓名",
+ "日期",
+ "星期",
+ "规定类型",
+ "实际类型",
+ "任务组长",
+ "工作类别",
+ "核算方式",
+ "项目名称",
+ "绩效核算",
+ "对象类型",
+ "余量正负",
+ "开始时间",
+ "结束时间",
+ "工时",
+ "额定倍率",
+ "标准量",
+ "完成量",
+ "余量",
+ "余量工时",
+ "备注",
+ ]
+ const exportData = records.map((item) => [
+ item.username ?? "-",
+ item.date ?? "-",
+ item.date ? weekDayText(dayjs(item.date).day()) : "-",
+ setTypeMap.get(item.set_type ?? "") ?? "-",
+ actualTypeMap.get(item.actual_type ?? "") ?? "-",
+ item.leader_name ?? "-",
+ workTypeMap.get(item.work_type ?? "") ?? "-",
+ accountTypeMap.get(item.accounting_type ?? "") ?? "-",
+ item.project_name ?? "-",
+ item.performance_accounting === true
+ ? "是"
+ : item.performance_accounting === false
+ ? "否"
+ : "-",
+ item.obj_type === 0 || item.obj_type === 1
+ ? (objTypeTextMap.get(item.obj_type) ?? "-")
+ : "-",
+ item.residual_pm === true ? "正" : item.residual_pm === false ? "负" : "-",
+ item.start_time ?? "-",
+ item.end_time ?? "-",
+ item.work_time ?? 0,
+ item.rate ?? 0,
+ item.standard_size ?? 0,
+ item.completed_size ?? 0,
+ item.residual_size ?? 0,
+ item.residual_work_time ?? 0,
+ item.remarks ?? "-",
+ ])
+ const wb = XLSX.utils.book_new()
+ const ws = XLSX.utils.aoa_to_sheet([exportTableHeader, ...exportData])
+ XLSX.utils.book_append_sheet(wb, ws, "团队日报数据")
+ XLSX.writeFile(wb, "团队日报数据.xlsx")
+ }
+
+ const columns = useMemo(() => {
+ const cols: DataTableColumn[] = [
+ { accessor: "id", title: "ID", width: 80, sortable: true },
+ {
+ accessor: "username",
+ title: "员工姓名",
+ width: 120,
+ render: (record) => record.username ?? "-",
+ },
+ {
+ accessor: "date",
+ title: "日期",
+ width: 120,
+ sortable: true,
+ render: (record) => record.date ?? "-",
+ },
+ {
+ accessor: "week",
+ title: "星期",
+ width: 90,
+ render: (record) => (record.date ? weekDayText(dayjs(record.date).day()) : "-"),
+ },
+ {
+ accessor: "set_type",
+ title: "规定类型",
+ width: 120,
+ render: (record) => setTypeMap.get(record.set_type ?? "") ?? "-",
+ },
+ {
+ accessor: "actual_type",
+ title: "实际类型",
+ width: 120,
+ render: (record) => actualTypeMap.get(record.actual_type ?? "") ?? "-",
+ },
+ { accessor: "leader_name", title: "任务组长", width: 120 },
+ {
+ accessor: "work_type",
+ title: "工作类别",
+ width: 140,
+ render: (record) => workTypeMap.get(record.work_type ?? "") ?? "-",
+ },
+ {
+ accessor: "accounting_type",
+ title: "核算方式",
+ width: 140,
+ render: (record) => accountTypeMap.get(record.accounting_type ?? "") ?? "-",
+ },
+ {
+ accessor: "project_name",
+ title: "项目名称",
+ width: 220,
+ render: (record) => (
+
+ {record.project_name ?? "-"}
+
+ ),
+ },
+ {
+ accessor: "performance_accounting",
+ title: "绩效核算",
+ width: 110,
+ render: (record) => {
+ if (record.performance_accounting === true) {
+ return {performanceTextMap.get(true)}
+ }
+ if (record.performance_accounting === false) {
+ return {performanceTextMap.get(false)}
+ }
+ return "-"
+ },
+ },
+ {
+ accessor: "obj_type",
+ title: "对象类型",
+ width: 100,
+ render: (record) => {
+ if (record.obj_type === 0 || record.obj_type === 1) {
+ return objTypeTextMap.get(record.obj_type) ?? "-"
+ }
+ return "-"
+ },
+ },
+ {
+ accessor: "residual_pm",
+ title: "余量正负",
+ width: 100,
+ render: (record) =>
+ record.residual_pm === true ? "正" : record.residual_pm === false ? "负" : "-",
+ },
+ { accessor: "start_time", title: "开始时间", width: 120 },
+ { accessor: "end_time", title: "结束时间", width: 120 },
+ { accessor: "work_time", title: "工时", width: 90, sortable: true },
+ { accessor: "rate", title: "额定倍率", width: 90 },
+ { accessor: "standard_size", title: "标准量", width: 90, sortable: true },
+ { accessor: "completed_size", title: "完成量", width: 90, sortable: true },
+ { accessor: "residual_size", title: "余量", width: 90, sortable: true },
+ {
+ accessor: "residual_work_time",
+ title: "余量工时",
+ width: 110,
+ sortable: true,
+ },
+ {
+ accessor: "remarks",
+ title: "备注",
+ width: 200,
+ render: (record) => (
+
+ {record.remarks ?? "-"}
+
+ ),
+ },
+ {
+ accessor: "operation",
+ title: "操作",
+ width: 120,
+ textAlign: "center",
+ render: (record) => {
+ const locked = !!record.lock_status
+ return (
+
+ {isShow("edit") ? (
+ {
+ if (locked) return
+ setEditingRecord(record)
+ setModalOpened(true)
+ }}>
+
+
+ ) : null}
+ {isShow("lock") ? (
+ {
+ if (!record.id) return
+ await lockDailyWorkData(!locked, record.id)
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: locked ? "已解锁日报" : "已锁定日报",
+ })
+ load()
+ }}>
+ {locked ? : }
+
+ ) : null}
+ {isShow("delete") ? (
+ {
+ if (locked || !record.id) return
+ modals.openConfirmModal({
+ title: "删除日报",
+ centered: true,
+ children: (
+ 删除后不可恢复,你确定删除该日报吗?
+ ),
+ labels: { confirm: "删除", cancel: "取消" },
+ confirmProps: { color: "red" },
+ onConfirm: async () => {
+ await deleteDailyWorkData(record.id as number)
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: "已删除日报",
+ })
+ load()
+ },
+ })
+ }}>
+
+
+ ) : null}
+
+ )
+ },
+ },
+ ]
+ return cols
+ }, [isShow, load])
+
+ const summaryText = useMemo(() => {
+ return {
+ total_work_time: summary?.total_work_time ?? 0,
+ total_standard_size: summary?.total_standard_size ?? 0,
+ total_completed_size: summary?.total_completed_size ?? 0,
+ total_residual_size: summary?.total_residual_size ?? 0,
+ total_residual_work_time: summary?.total_residual_work_time ?? 0,
+ }
+ }, [summary])
+
+ return (
+
+
+
+
+
+ 搜索条件
+
+ ) : (
+
+ )
+ }
+ onClick={() => setSearchExpanded((v) => !v)}>
+ {searchExpanded ? "收起" : "展开"}
+
+
+
+
+ }
+ onClick={() => {
+ setFilters(defaultFilters)
+ setAppliedFilters(defaultFilters)
+ setPage(1)
+ setQueryTrigger((v) => v + 1)
+ }}>
+ 重置
+
+ }
+ onClick={() => {
+ setAppliedFilters(filters)
+ setPage(1)
+ setQueryTrigger((v) => v + 1)
+ }}>
+ 查询
+
+
+
+
+
+ setFilters((s) => ({ ...s, date_start: e.target.value }))}
+ />
+ setFilters((s) => ({ ...s, date_end: e.target.value }))}
+ />
+ setFilters((s) => ({ ...s, uid: v }))}
+ />
+ setFilters((s) => ({ ...s, project_id: v }))}
+ />
+ setFilters((s) => ({ ...s, leader_uid: v }))}
+ />
+ setFilters((s) => ({ ...s, set_type: v[0] ?? "" }))}
+ />
+ setFilters((s) => ({ ...s, actual_type: v[0] ?? "" }))}
+ />
+ setFilters((s) => ({ ...s, work_type: v[0] ?? "" }))}
+ />
+
+ setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" }))
+ }
+ />
+
+ setFilters((s) => ({ ...s, performance_accounting: v[0] ?? "" }))
+ }
+ />
+ setFilters((s) => ({ ...s, obj_type: v[0] ?? "" }))}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ width="100%"
+ style={{ width: "100%" }}
+ withTableBorder
+ withRowBorders
+ pinFirstColumn
+ pinLastColumn
+ scrollAreaProps={{ type: "auto" }}
+ fetching={loading}
+ records={records}
+ columns={columns}
+ totalRecords={totalItems}
+ recordsPerPage={pageSize}
+ page={page}
+ onPageChange={setPage}
+ onRecordsPerPageChange={(v) => {
+ setPageSize(v)
+ setPage(1)
+ }}
+ recordsPerPageOptions={[10, 15, 20, 50]}
+ noRecordsText="暂无数据"
+ recordsPerPageLabel="当前页数"
+ sortStatus={sortStatus}
+ onSortStatusChange={(s) => {
+ setSortStatus(s)
+ setPage(1)
+ }}
+ />
+
+
+
+
+ 当前页总计
+
+
+ 工时:{summaryText.total_work_time}
+ 标准量:{summaryText.total_standard_size}
+ 完成量:{summaryText.total_completed_size}
+ 余量:{summaryText.total_residual_size}
+
+ 余量工时:{summaryText.total_residual_work_time}
+
+
+
+
+
+
+
+ {
+ setModalOpened(false)
+ setEditingRecord(null)
+ if (refresh) load()
+ }}
+ />
+
+ )
}
diff --git a/app/management/team/employee/components/CreateUserModal.tsx b/app/management/team/employee/components/CreateUserModal.tsx
new file mode 100644
index 0000000..ed04943
--- /dev/null
+++ b/app/management/team/employee/components/CreateUserModal.tsx
@@ -0,0 +1,131 @@
+"use client"
+
+import { getUserGroupAll, userAdd } from "@/components/label/api/user"
+import {
+ Button,
+ Group,
+ Modal,
+ Select,
+ Stack,
+ TextInput,
+} from "@mantine/core"
+import { useForm } from "@mantine/form"
+import { notifications } from "@mantine/notifications"
+import { useEffect, useState } from "react"
+
+interface ComponentProps {
+ opened: boolean
+ onCloseAction: (refresh?: boolean) => void
+}
+
+export default function CreateUserModal({
+ opened,
+ onCloseAction,
+}: ComponentProps) {
+ const [groupOpts, setGroupOpts] = useState>(
+ []
+ )
+ const [submitting, setSubmitting] = useState(false)
+
+ const form = useForm({
+ initialValues: {
+ user_name: "",
+ group_uuid: "",
+ base_city: "",
+ },
+ validate: {
+ user_name: (v) => (v.trim() ? null : "请输入用户名"),
+ group_uuid: (v) => (v ? null : "请选择组织"),
+ base_city: (v) => (v.trim() ? null : "请输入所在地"),
+ },
+ })
+
+ useEffect(() => {
+ if (!opened) return
+ const run = async () => {
+ const res = await getUserGroupAll()
+ setGroupOpts(res ?? [])
+ }
+ queueMicrotask(run)
+ }, [opened])
+
+ const submit = form.onSubmit(async (values) => {
+ try {
+ setSubmitting(true)
+ await userAdd({
+ user_name: values.user_name.trim(),
+ group_uuid: values.group_uuid,
+ base_city: values.base_city.trim(),
+ })
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: "已新增用户",
+ })
+ form.reset()
+ onCloseAction(true)
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "操作失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ })
+
+ return (
+ onCloseAction()}
+ title="新增用户"
+ centered
+ size={560}
+ closeOnClickOutside={false}>
+
+
+ )
+}
diff --git a/app/management/team/employee/components/EditUserModal.tsx b/app/management/team/employee/components/EditUserModal.tsx
new file mode 100644
index 0000000..22f4053
--- /dev/null
+++ b/app/management/team/employee/components/EditUserModal.tsx
@@ -0,0 +1,168 @@
+"use client"
+
+import { getUserGroupAll, userEditById } from "@/components/label/api/user"
+import { User } from "@/components/label/api/user/typing"
+import {
+ Button,
+ Group,
+ Modal,
+ Select,
+ Stack,
+ TextInput,
+} from "@mantine/core"
+import { useForm } from "@mantine/form"
+import { notifications } from "@mantine/notifications"
+import { useEffect, useMemo, useState } from "react"
+
+interface ComponentProps {
+ opened: boolean
+ data: User.UserInfo | null
+ onCloseAction: (refresh?: boolean) => void
+}
+
+export default function EditUserModal({
+ opened,
+ data,
+ onCloseAction,
+}: ComponentProps) {
+ const [groupOpts, setGroupOpts] = useState>(
+ []
+ )
+ const [submitting, setSubmitting] = useState(false)
+
+ const groupValueByName = useMemo(() => {
+ const map = new Map()
+ groupOpts.forEach((g) => map.set(g.label, g.value))
+ return map
+ }, [groupOpts])
+
+ const form = useForm({
+ initialValues: {
+ name: "",
+ group_uuid: "",
+ base_city: "",
+ work_status: "1",
+ },
+ validate: {
+ name: (v) => (v.trim() ? null : "请输入用户名"),
+ group_uuid: (v) => (v ? null : "请选择组织"),
+ base_city: (v) => (v.trim() ? null : "请输入所在地"),
+ work_status: (v) => (v ? null : "请选择状态"),
+ },
+ })
+
+ useEffect(() => {
+ if (!opened) return
+ const run = async () => {
+ const res = await getUserGroupAll()
+ setGroupOpts(res ?? [])
+ }
+ queueMicrotask(run)
+ }, [opened])
+
+ useEffect(() => {
+ if (!opened || !data) return
+ form.setValues({
+ name: data.name ?? "",
+ group_uuid: groupValueByName.get(data.group_name ?? "") ?? "",
+ base_city: data.base_city ?? "",
+ work_status: data.work_status === 0 ? "0" : "1",
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [opened, data, groupValueByName])
+
+ const submit = form.onSubmit(async (values) => {
+ if (!data?.uid) return
+ try {
+ setSubmitting(true)
+ await userEditById(
+ {
+ user_name: values.name.trim(),
+ group_uuid: values.group_uuid,
+ base_city: values.base_city.trim(),
+ work_status: Number(values.work_status),
+ },
+ data.uid
+ )
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: "已更新用户信息",
+ })
+ onCloseAction(true)
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "操作失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ })
+
+ return (
+ onCloseAction()}
+ title="编辑用户"
+ centered
+ size={560}
+ closeOnClickOutside={false}>
+
+
+ )
+}
diff --git a/app/management/team/employee/components/ImportModal.tsx b/app/management/team/employee/components/ImportModal.tsx
new file mode 100644
index 0000000..86cc387
--- /dev/null
+++ b/app/management/team/employee/components/ImportModal.tsx
@@ -0,0 +1,265 @@
+"use client"
+
+import { userImport } from "@/components/label/api/user"
+import {
+ Alert,
+ Button,
+ FileInput,
+ Group,
+ Modal,
+ Paper,
+ Stack,
+ Text,
+} from "@mantine/core"
+import { notifications } from "@mantine/notifications"
+import { IconAlertCircle, IconDownload, IconUpload } from "@tabler/icons-react"
+import { useState } from "react"
+import * as XLSX from "xlsx-js-style"
+
+interface ComponentProps {
+ opened: boolean
+ onCloseAction: (refresh?: boolean) => void
+}
+
+type ImportItem = {
+ user_name: string
+ group_name: string
+ base_city: string
+}
+
+export default function ImportModal({ opened, onCloseAction }: ComponentProps) {
+ const [file, setFile] = useState(null)
+ const [sheetList, setSheetList] = useState([])
+ const [failedDataTips, setFailedDataTips] = useState([])
+ const [submitting, setSubmitting] = useState(false)
+
+ const clearState = () => {
+ setFile(null)
+ setSheetList([])
+ setFailedDataTips([])
+ }
+
+ const parseFile = async (nextFile: File | null) => {
+ setFile(nextFile)
+ setSheetList([])
+ setFailedDataTips([])
+ if (!nextFile) return
+ if (!nextFile.name.endsWith(".xlsx")) {
+ notifications.show({
+ color: "red",
+ title: "文件格式错误",
+ message: "仅支持 .xlsx 文件",
+ })
+ setFile(null)
+ return
+ }
+
+ try {
+ const data = await nextFile.arrayBuffer()
+ const workbook = XLSX.read(data, { type: "array" })
+ const sheetName = workbook.SheetNames[0]
+ const ws = workbook.Sheets[sheetName]
+ const arr = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false })
+ const tableData = arr.slice(4).filter((row) => row.length > 0)
+
+ if (tableData.length === 0) {
+ setSheetList([])
+ return
+ }
+
+ const checkList: Record = {
+ 0: "用户名",
+ 1: "组织",
+ 2: "所在地",
+ }
+ const tipRows = 4
+ const errors: string[] = []
+
+ const list: ImportItem[] = tableData.map((item: any[], index: number) => {
+ Object.entries(checkList).forEach(([key, fieldName]) => {
+ const value = item[Number(key)]
+ if (!value || (typeof value === "string" && !value.trim())) {
+ errors.push(
+ `第${index + 1 + tipRows}行数据缺少必填项【${fieldName}】或该必填项全为空格`
+ )
+ }
+ })
+
+ return {
+ user_name: item[0] ? String(item[0]).trim() : "",
+ group_name: item[1] ? String(item[1]).trim() : "",
+ base_city: item[2] ? String(item[2]).trim() : "",
+ }
+ })
+
+ if (errors.length > 0) {
+ setFailedDataTips(errors)
+ setSheetList([])
+ return
+ }
+
+ setSheetList(list)
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "解析失败",
+ message: e instanceof Error ? e.message : "读取文件失败",
+ })
+ setFile(null)
+ }
+ }
+
+ const exportTemplateFile = () => {
+ const tips = [
+ [`提示:\n1.下列字段均为必填项\n2.【组织】仅可以填入一个组织名称`],
+ [""],
+ [""],
+ ]
+ const col = [["用户名", "组织", "所在地"]]
+ const wb = XLSX.utils.book_new()
+ const ws = XLSX.utils.aoa_to_sheet([...tips, ...col])
+ XLSX.utils.book_append_sheet(wb, ws, "用户信息")
+ wb.Sheets["用户信息"]["!rows"] = [{ hpx: 20 }, { hpx: 20 }, { hpx: 20 }]
+ wb.Sheets["用户信息"]["!cols"] = [{ wch: 20 }, { wch: 20 }, { wch: 20 }]
+ wb.Sheets["用户信息"]["!merges"] = [{ s: { c: 0, r: 0 }, e: { c: 2, r: 2 } }]
+ if (wb.Sheets["用户信息"]["A1"]) {
+ wb.Sheets["用户信息"]["A1"].s = { alignment: { wrapText: true } }
+ }
+ XLSX.writeFile(wb, "用户信息批量导入模板.xlsx")
+ }
+
+ const submit = async () => {
+ if (sheetList.length === 0) {
+ notifications.show({
+ color: "yellow",
+ title: "无导入数据",
+ message: "请先上传并校验 Excel 文件",
+ })
+ return
+ }
+ try {
+ setSubmitting(true)
+ const res = await userImport({ user_infos: sheetList })
+ if (Array.isArray(res) && res.length > 0) {
+ setFailedDataTips(res)
+ notifications.show({
+ color: "red",
+ title: "导入失败",
+ message: "请根据错误提示修正后重试",
+ })
+ return
+ }
+ notifications.show({
+ color: "green",
+ title: "导入成功",
+ message: "批量新增用户数据成功",
+ })
+ clearState()
+ onCloseAction(true)
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "导入失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+ {
+ clearState()
+ onCloseAction()
+ }}
+ title="批量导入用户信息"
+ centered
+ size={760}
+ closeOnClickOutside={false}>
+
+
+
+ 点击下载
+ }
+ onClick={exportTemplateFile}>
+ 用户信息批量导入模板
+
+ 后,填写上传
+
+
+
+ }
+ value={file}
+ onChange={parseFile}
+ clearable
+ />
+
+
+ 待导入数据:{sheetList.length} 条
+
+
+ {failedDataTips.length > 0 ? (
+ }
+ color="red"
+ radius="sm"
+ title="错误信息提示">
+
+ {failedDataTips.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ ) : null}
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/management/team/employee/page.tsx b/app/management/team/employee/page.tsx
new file mode 100644
index 0000000..0645757
--- /dev/null
+++ b/app/management/team/employee/page.tsx
@@ -0,0 +1,288 @@
+"use client"
+
+import { getUserList, userEditById } from "@/components/label/api/user"
+import { User } from "@/components/label/api/user/typing"
+import useAuth from "@/components/label/hooks/useAuth"
+import { useAllUserStore } from "@/components/label/store/auth"
+import {
+ ActionIcon,
+ Badge,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core"
+import { modals } from "@mantine/modals"
+import { notifications } from "@mantine/notifications"
+import { IconEdit, IconPlus, IconRefresh, IconSearch, IconUpload } from "@tabler/icons-react"
+import { DataTable, DataTableColumn } from "mantine-datatable"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import CreateUserModal from "./components/CreateUserModal"
+import EditUserModal from "./components/EditUserModal"
+import ImportModal from "./components/ImportModal"
+
+export default function TeamEmployeePage() {
+ const { isShow } = useAuth()
+ const setFlag = useAllUserStore((s) => s.setFlag)
+
+ const [name, setName] = useState("")
+ const [appliedName, setAppliedName] = useState("")
+ const [records, setRecords] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [totalItems, setTotalItems] = useState(0)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const [createOpened, setCreateOpened] = useState(false)
+ const [editOpened, setEditOpened] = useState(false)
+ const [importOpened, setImportOpened] = useState(false)
+ const [selectedUser, setSelectedUser] = useState(null)
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true)
+ const params: User.ListRequest = {
+ page_number: page,
+ page_size: pageSize,
+ }
+ if (appliedName.trim()) params.name = appliedName.trim()
+ const res = await getUserList(params)
+ setRecords(res?.user_list ?? [])
+ setTotalItems(res?.total_items ?? 0)
+ } catch (e) {
+ setRecords([])
+ setTotalItems(0)
+ notifications.show({
+ color: "red",
+ title: "加载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setLoading(false)
+ }
+ }, [appliedName, page, pageSize])
+
+ useEffect(() => {
+ queueMicrotask(load)
+ }, [load])
+
+ const columns = useMemo(() => {
+ const cols: DataTableColumn[] = [
+ { accessor: "uid", title: "UID", width: 120, textAlign: "center" },
+ { accessor: "name", title: "姓名", width: 120, textAlign: "center" },
+ {
+ accessor: "work_status",
+ title: "状态",
+ width: 100,
+ textAlign: "center",
+ render: (record) =>
+ record.work_status === 1 ? (
+
+ 在职
+
+ ) : (
+
+ 离职
+
+ ),
+ },
+ {
+ accessor: "group_name",
+ title: "组织",
+ width: 180,
+ textAlign: "center",
+ },
+ {
+ accessor: "base_city",
+ title: "所在地",
+ width: 180,
+ textAlign: "center",
+ },
+ {
+ accessor: "operation",
+ title: "操作",
+ width: 220,
+ textAlign: "center",
+ render: (record) => (
+
+ {isShow("edit") ? (
+
+ ) : null}
+ {isShow("edit") ? (
+ {
+ setSelectedUser(record)
+ setEditOpened(true)
+ }}>
+
+
+ ) : null}
+
+ ),
+ },
+ ]
+ return cols
+ }, [isShow])
+
+ return (
+
+
+
+
+ setName(e.currentTarget.value)}
+ />
+ }
+ onClick={() => {
+ setAppliedName(name)
+ setPage(1)
+ }}>
+ 查询
+
+ }
+ onClick={() => {
+ setName("")
+ setAppliedName("")
+ setPage(1)
+ setPageSize(20)
+ }}>
+ 重置
+
+
+
+ {isShow("add") ? (
+ }
+ onClick={() => setCreateOpened(true)}>
+ 新增用户
+
+ ) : null}
+ {isShow("add") ? (
+ }
+ onClick={() => setImportOpened(true)}>
+ 批量导入
+
+ ) : null}
+
+
+
+
+
+
+ withTableBorder
+ withRowBorders
+ fetching={loading}
+ records={records}
+ columns={columns}
+ noRecordsText="暂无数据"
+ totalRecords={totalItems}
+ recordsPerPage={pageSize}
+ page={page}
+ onPageChange={setPage}
+ onRecordsPerPageChange={(v) => {
+ setPageSize(v)
+ setPage(1)
+ }}
+ recordsPerPageOptions={[10, 20, 50, 100]}
+ scrollAreaProps={{ type: "auto" }}
+ style={{ width: "100%" }}
+ />
+
+
+ {
+ setCreateOpened(false)
+ if (refresh) {
+ setFlag(true)
+ load()
+ }
+ }}
+ />
+
+ {
+ setImportOpened(false)
+ if (refresh) {
+ setFlag(true)
+ load()
+ }
+ }}
+ />
+
+ {
+ setEditOpened(false)
+ setSelectedUser(null)
+ if (refresh) {
+ setFlag(true)
+ load()
+ }
+ }}
+ />
+
+ )
+}
diff --git a/app/management/team/employee/paper.tsx b/app/management/team/employee/paper.tsx
deleted file mode 100644
index 9c3fba7..0000000
--- a/app/management/team/employee/paper.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-"use client"
-
-export default function EmployeePage() {
- return <>>
-}
diff --git a/app/management/team/organization/components/CreateModal.tsx b/app/management/team/organization/components/CreateModal.tsx
new file mode 100644
index 0000000..c413fbd
--- /dev/null
+++ b/app/management/team/organization/components/CreateModal.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import {
+ getUserAll,
+ getUserGroupAll,
+ userGroupAdd,
+} from "@/components/label/api/user"
+import { useAllUserStore } from "@/components/label/store/auth"
+import { Button, Group, Modal, Select, Stack, TextInput } from "@mantine/core"
+import { useForm } from "@mantine/form"
+import { notifications } from "@mantine/notifications"
+import { useEffect, useMemo, useState } from "react"
+
+interface ComponentProps {
+ opened: boolean
+ onCloseAction: (refresh?: boolean) => void
+}
+
+export default function CreateModal({ opened, onCloseAction }: ComponentProps) {
+ const storeUserOpts = useAllUserStore((s) => s.userOpts)
+
+ const [groupOpts, setGroupOpts] = useState>(
+ []
+ )
+ const [userOpts, setUserOpts] = useState>(
+ []
+ )
+ const [submitting, setSubmitting] = useState(false)
+
+ const finalUserOpts = useMemo(() => {
+ return userOpts.length > 0 ? userOpts : (storeUserOpts as any[])
+ }, [storeUserOpts, userOpts])
+
+ const form = useForm({
+ initialValues: {
+ group_name: "",
+ leader_uid: "",
+ parent_group_uuid: "",
+ },
+ validate: {
+ group_name: (v) => (v.trim() ? null : "请输入用户组名称"),
+ leader_uid: (v) => (v ? null : "请选择用户组负责人"),
+ },
+ })
+
+ useEffect(() => {
+ if (!opened) return
+ const run = async () => {
+ const [groupRes, userRes] = await Promise.all([
+ getUserGroupAll(),
+ getUserAll(),
+ ])
+ setGroupOpts(groupRes ?? [])
+ setUserOpts((userRes ?? []).map((u) => ({ label: u.label, value: String(u.value) })))
+ }
+ queueMicrotask(run)
+ }, [opened])
+
+ const submit = form.onSubmit(async (values) => {
+ try {
+ setSubmitting(true)
+ await userGroupAdd({
+ group_name: values.group_name.trim(),
+ leader_uid: Number(values.leader_uid),
+ parent_group_uuid: values.parent_group_uuid,
+ })
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: "已新增用户组",
+ })
+ form.reset()
+ onCloseAction(true)
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "操作失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ })
+
+ return (
+ onCloseAction()}
+ title="新增用户组"
+ centered
+ size={560}
+ closeOnClickOutside={false}>
+
+
+ )
+}
diff --git a/app/management/team/organization/page.tsx b/app/management/team/organization/page.tsx
index 4b5a616..40fec30 100644
--- a/app/management/team/organization/page.tsx
+++ b/app/management/team/organization/page.tsx
@@ -1,5 +1,262 @@
"use client"
-export default function OrganizationPage() {
- return <>>
+import { getUserGroupTree } from "@/components/label/api/user"
+import { Organization } from "@/components/label/api/user/typing"
+import useAuth from "@/components/label/hooks/useAuth"
+import { useAllUserStore } from "@/components/label/store/auth"
+import {
+ ActionIcon,
+ Button,
+ Group,
+ Paper,
+ ScrollArea,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core"
+import { notifications } from "@mantine/notifications"
+import { IconPlus, IconRefresh, IconSearch, IconUserCircle } from "@tabler/icons-react"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import CreateModal from "./components/CreateModal"
+
+type NodeItem = {
+ key: string
+ name: string
+ isUser?: boolean
+ isManager?: boolean
+ children: NodeItem[]
+}
+
+function toNodeTree(data: Organization.Response[]): NodeItem[] {
+ return (data ?? []).map((item) => {
+ const children: NodeItem[] = []
+
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ children.push(...toNodeTree(item.children))
+ }
+
+ if (item.leader_uid && item.leader_name) {
+ children.push({
+ key: `${item.group_uuid}/${item.leader_uid}`,
+ name: item.leader_name,
+ isUser: true,
+ isManager: true,
+ children: [],
+ })
+ }
+
+ if (Array.isArray(item.members) && item.members.length > 0) {
+ item.members.forEach((member) => {
+ if (member.id === item.leader_uid) return
+ children.push({
+ key: `${item.group_uuid}/${member.id}`,
+ name: member.name,
+ isUser: true,
+ children: [],
+ })
+ })
+ }
+
+ return {
+ key: item.group_uuid,
+ name: item.name,
+ children,
+ }
+ })
+}
+
+function filterTree(nodes: NodeItem[], keyword: string): NodeItem[] {
+ if (!keyword.trim()) return nodes
+ const lower = keyword.trim().toLowerCase()
+ return nodes
+ .map((node) => {
+ const matched = node.name.toLowerCase().includes(lower)
+ const children = filterTree(node.children, keyword)
+ if (!matched && children.length === 0) return null
+ return { ...node, children }
+ })
+ .filter((n): n is NodeItem => !!n)
+}
+
+type FlatNode = NodeItem & { depth: number }
+
+function flattenTree(nodes: NodeItem[], depth = 0): FlatNode[] {
+ const list: FlatNode[] = []
+ nodes.forEach((node) => {
+ list.push({ ...node, depth })
+ if (node.children.length > 0) {
+ list.push(...flattenTree(node.children, depth + 1))
+ }
+ })
+ return list
+}
+
+function HighlightText(props: { text: string; keyword: string }) {
+ const { text, keyword } = props
+ if (!keyword.trim()) return <>{text}>
+ const index = text.toLowerCase().indexOf(keyword.trim().toLowerCase())
+ if (index < 0) return <>{text}>
+
+ const before = text.slice(0, index)
+ const match = text.slice(index, index + keyword.length)
+ const after = text.slice(index + keyword.length)
+ return (
+ <>
+ {before}
+
+ {match}
+
+ {after}
+ >
+ )
+}
+
+export default function TeamOrganizationPage() {
+ const { isShow } = useAuth()
+ const setFlag = useAllUserStore((s) => s.setFlag)
+
+ const [open, setOpen] = useState(false)
+ const [groupName, setGroupName] = useState("")
+ const [data, setData] = useState([])
+ const [loading, setLoading] = useState(false)
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true)
+ const res = await getUserGroupTree()
+ setData(toNodeTree(res ?? []))
+ } catch (e) {
+ setData([])
+ notifications.show({
+ color: "red",
+ title: "加载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ queueMicrotask(load)
+ }, [load])
+
+ const rows = useMemo(() => {
+ const filtered = filterTree(data, groupName)
+ return flattenTree(filtered)
+ }, [data, groupName])
+
+ return (
+
+
+
+
+ }
+ value={groupName}
+ onChange={(e) => setGroupName(e.currentTarget.value)}
+ style={{ minWidth: 360 }}
+ />
+
+
+
+
+ {isShow("group_add") ? (
+ }
+ onClick={() => setOpen(true)}>
+ 新增用户组
+
+ ) : null}
+
+
+
+
+
+
+ {rows.map((node) => {
+ return (
+
+
+ {node.isUser ? : null}
+
+
+ {!node.isUser && node.children.length > 0
+ ? `(${node.children.length})`
+ : ""}
+
+ {node.isManager ? (
+
+ 负责人
+
+ ) : null}
+
+
+ )
+ })}
+ {rows.length === 0 ? (
+
+ 暂无匹配数据
+
+ ) : null}
+
+
+
+
+ {
+ setOpen(false)
+ if (refresh) {
+ setFlag(true)
+ load()
+ }
+ }}
+ />
+
+ )
}
diff --git a/app/management/team/page.tsx b/app/management/team/page.tsx
new file mode 100644
index 0000000..206f1cc
--- /dev/null
+++ b/app/management/team/page.tsx
@@ -0,0 +1,14 @@
+"use client"
+
+import { useRouter } from "next/navigation"
+import { useEffect } from "react"
+
+export default function TeamPage() {
+ const router = useRouter()
+
+ useEffect(() => {
+ router.push("/management/team/employee")
+ }, [router])
+
+ return <>>
+}
diff --git a/app/management/team/sync_server/page.tsx b/app/management/team/sync_server/page.tsx
new file mode 100644
index 0000000..d07f773
--- /dev/null
+++ b/app/management/team/sync_server/page.tsx
@@ -0,0 +1,279 @@
+"use client"
+
+import {
+ getDataSyncServerList,
+ updateSyncServerLocation,
+} from "@/components/label/api/sync"
+import { Response } from "@/components/label/api/sync/typing"
+import useAuth from "@/components/label/hooks/useAuth"
+import {
+ ActionIcon,
+ Badge,
+ Button,
+ Group,
+ Modal,
+ Paper,
+ Select,
+ Stack,
+ TextInput,
+} from "@mantine/core"
+import { notifications } from "@mantine/notifications"
+import { IconEdit, IconRefresh, IconSearch } from "@tabler/icons-react"
+import { DataTable, DataTableColumn } from "mantine-datatable"
+import { useCallback, useEffect, useMemo, useState } from "react"
+
+export default function TeamSyncServerPage() {
+ const { isShow } = useAuth()
+
+ const [records, setRecords] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [totalItems, setTotalItems] = useState(0)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const [location, setLocation] = useState("")
+ const [appliedLocation, setAppliedLocation] = useState("")
+ const [online, setOnline] = useState(null)
+ const [appliedOnline, setAppliedOnline] = useState(null)
+
+ const [open, setOpen] = useState(false)
+ const [editingRecord, setEditingRecord] = useState(null)
+ const [editingLocation, setEditingLocation] = useState("")
+ const [submitting, setSubmitting] = useState(false)
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true)
+ const params: any = {
+ page_number: page,
+ page_size: pageSize,
+ }
+ if (appliedLocation.trim()) params.location = appliedLocation.trim()
+ if (appliedOnline === "true") params.is_online = true
+ if (appliedOnline === "false") params.is_online = false
+
+ const res = await getDataSyncServerList(params)
+ setRecords(res?.sync_servers ?? [])
+ setTotalItems(res?.total_items ?? 0)
+ } catch (e) {
+ setRecords([])
+ setTotalItems(0)
+ notifications.show({
+ color: "red",
+ title: "加载失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setLoading(false)
+ }
+ }, [appliedLocation, appliedOnline, page, pageSize])
+
+ useEffect(() => {
+ load()
+ }, [load])
+
+ const columns = useMemo(() => {
+ const cols: DataTableColumn[] = [
+ { accessor: "id", title: "ID", width: 80 },
+ { accessor: "ip", title: "IP", width: 180 },
+ { accessor: "location", title: "IP所在地", width: 180 },
+ {
+ accessor: "is_online",
+ title: "状态",
+ width: 100,
+ render: (record) =>
+ record.is_online ? (
+
+ 在线
+
+ ) : (
+
+ 离线
+
+ ),
+ },
+ { accessor: "create_at", title: "创建时间", width: 180 },
+ { accessor: "update_at", title: "更新时间", width: 180 },
+ {
+ accessor: "operation",
+ title: "操作",
+ width: 100,
+ textAlign: "center",
+ render: (record) =>
+ isShow("server_edit") ? (
+ {
+ setEditingRecord(record)
+ setEditingLocation(record.location ?? "")
+ setOpen(true)
+ }}>
+
+
+ ) : null,
+ },
+ ]
+ return cols
+ }, [isShow])
+
+ const saveLocation = async () => {
+ if (!editingRecord?.id) return
+ try {
+ setSubmitting(true)
+ await updateSyncServerLocation(editingLocation.trim(), editingRecord.id)
+ notifications.show({
+ color: "green",
+ title: "操作成功",
+ message: "已更新同步服务所在地",
+ })
+ setOpen(false)
+ setEditingRecord(null)
+ setEditingLocation("")
+ load()
+ } catch (e) {
+ notifications.show({
+ color: "red",
+ title: "更新失败",
+ message: e instanceof Error ? e.message : "请求失败",
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+
+
+
+ setLocation(e.currentTarget.value)}
+ />
+
+ }
+ onClick={() => {
+ setAppliedLocation(location)
+ setAppliedOnline(online)
+ setPage(1)
+ }}>
+ 查询
+
+ }
+ onClick={() => {
+ setLocation("")
+ setOnline(null)
+ setAppliedLocation("")
+ setAppliedOnline(null)
+ setPage(1)
+ setPageSize(20)
+ }}>
+ 重置
+
+
+
+
+
+
+ withTableBorder
+ withRowBorders
+ fetching={loading}
+ records={records}
+ columns={columns}
+ totalRecords={totalItems}
+ recordsPerPage={pageSize}
+ page={page}
+ onPageChange={setPage}
+ onRecordsPerPageChange={(v) => {
+ setPageSize(v)
+ setPage(1)
+ }}
+ recordsPerPageOptions={[10, 20, 50, 100]}
+ noRecordsText="暂无数据"
+ scrollAreaProps={{ type: "auto" }}
+ style={{ width: "100%" }}
+ />
+
+
+ {
+ setOpen(false)
+ setEditingRecord(null)
+ setEditingLocation("")
+ }}
+ title="更新同步服务所在地"
+ centered
+ size={520}
+ closeOnClickOutside={false}>
+
+ setEditingLocation(e.currentTarget.value)}
+ />
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/management/team/sync_task/page.tsx b/app/management/team/sync_task/page.tsx
new file mode 100644
index 0000000..8f1e28d
--- /dev/null
+++ b/app/management/team/sync_task/page.tsx
@@ -0,0 +1,455 @@
+"use client"
+
+import { getProjectList } from "@/components/label/api/project"
+import {
+ getAllSyncServerList,
+ getDataSyncTaskList,
+ newSyncTask,
+ updateSyncTaskStatus,
+} from "@/components/label/api/sync"
+import { Response } from "@/components/label/api/sync/typing"
+import useAuth from "@/components/label/hooks/useAuth"
+import {
+ ActionIcon,
+ Button,
+ Group,
+ Modal,
+ Paper,
+ Select,
+ Stack,
+ Text,
+} from "@mantine/core"
+import { modals } from "@mantine/modals"
+import { notifications } from "@mantine/notifications"
+import { IconPlayerPause, IconPlayerPlay, IconPlus, IconRefresh, IconSearch, IconTrash, IconX } from "@tabler/icons-react"
+import { DataTable, DataTableColumn } from "mantine-datatable"
+import { useCallback, useEffect, useMemo, useState } from "react"
+
+const StatusEnum = new Map([
+ [1, "待同步"],
+ [2, "同步中"],
+ [3, "同步失败"],
+ [4, "同步成功"],
+ [5, "待删除"],
+ [6, "删除中"],
+ [7, "删除失败"],
+ [8, "删除完成"],
+ [9, "等待暂停"],
+ [10, "已暂停"],
+ [11, "等待关闭"],
+ [12, "已关闭"],
+])
+
+export default function TeamSyncTaskPage() {
+ const { isShow } = useAuth()
+
+ const [records, setRecords] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [totalItems, setTotalItems] = useState(0)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const [status, setStatus] = useState(null)
+ const [projectId, setProjectId] = useState(null)
+ const [serverId, setServerId] = useState(null)
+ const [appliedStatus, setAppliedStatus] = useState(null)
+ const [appliedProjectId, setAppliedProjectId] = useState(null)
+ const [appliedServerId, setAppliedServerId] = useState(null)
+
+ const [projectOpt, setProjectOpt] = useState>([])
+ const [serverAllOpt, setServerAllOpt] = useState>([])
+ const [serverOnlineOpt, setServerOnlineOpt] = useState>(
+ []
+ )
+ const [serverMap, setServerMap] = useState