Files
labelmain-demo/app/management/team/employee/components/ImportModal.tsx
2026-02-27 10:21:43 +08:00

264 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

"use client"
import { 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<File | null>(null)
const [sheetList, setSheetList] = useState<ImportItem[]>([])
const [failedDataTips, setFailedDataTips] = useState<string[]>([])
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<any[]>(ws, { header: 1, raw: false })
const tableData = arr.slice(4).filter((row) => row.length > 0)
if (tableData.length === 0) {
setSheetList([])
return
}
const checkList: Record<number, string> = {
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 (
<Modal
opened={opened}
onClose={() => {
clearState()
onCloseAction()
}}
title="批量导入用户信息"
centered
size={760}
closeOnClickOutside={false}>
<Stack gap="sm">
<Paper
withBorder
p="sm"
radius="sm"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Group gap={8}>
<Text size="sm"></Text>
<Button
variant="subtle"
size="compact-sm"
leftSection={<IconDownload size={14} />}
onClick={exportTemplateFile}>
</Button>
<Text size="sm"></Text>
</Group>
</Paper>
<FileInput
label="上传文件"
description="支持单个 xlsx 文件上传"
size="xs"
radius="xs"
accept=".xlsx"
placeholder="请选择文件"
leftSection={<IconUpload size={16} />}
value={file}
onChange={parseFile}
clearable
/>
<Paper
withBorder
p="xs"
radius="sm"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Text size="sm">{sheetList.length} </Text>
</Paper>
{failedDataTips.length > 0 ? (
<Alert
icon={<IconAlertCircle size={16} />}
color="red"
radius="sm"
title="错误信息提示">
<Stack gap={4} style={{ maxHeight: 180, overflowY: "auto" }}>
{failedDataTips.map((item) => (
<Text key={item} size="sm">
{item}
</Text>
))}
</Stack>
</Alert>
) : null}
<Group justify="flex-end">
<Button
variant="default"
size="xs"
radius="xs"
onClick={() => {
clearState()
onCloseAction()
}}>
</Button>
<Button size="xs" radius="xs" onClick={submit} loading={submitting}>
</Button>
</Group>
</Stack>
</Modal>
)
}