diff --git a/apps/server/src/upload/share-code.service.ts b/apps/server/src/upload/share-code.service.ts index dd1d538..0ecb125 100644 --- a/apps/server/src/upload/share-code.service.ts +++ b/apps/server/src/upload/share-code.service.ts @@ -11,12 +11,15 @@ export class ShareCodeService { // 生成8位分享码,使用易读的字符 private readonly generateCode = customAlphabet( '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', - 8 + 8, ); constructor(private readonly resourceService: ResourceService) {} - async generateShareCode(fileId: string, fileName?: string): Promise { + async generateShareCode( + fileId: string, + fileName?: string, + ): Promise { try { // 检查文件是否存在 const resource = await this.resourceService.findUnique({ @@ -36,7 +39,7 @@ export class ShareCodeService { const existingShareCode = await db.shareCode.findUnique({ where: { fileId }, }); - + if (existingShareCode) { // 更新现有记录,但保留原有文件名 await db.shareCode.update({ @@ -46,9 +49,7 @@ export class ShareCodeService { expiresAt, isUsed: false, // 只在没有现有文件名且提供了新文件名时才更新文件名 - ...(fileName && !existingShareCode.fileName - ? { fileName } - : {}) + ...(fileName && !existingShareCode.fileName ? { fileName } : {}), }, }); } else { @@ -77,7 +78,7 @@ export class ShareCodeService { async validateAndUseCode(code: string): Promise { try { console.log(`尝试验证分享码: ${code}`); - + // 查找有效的分享码 const shareCode = await db.shareCode.findFirst({ where: { @@ -117,10 +118,7 @@ export class ShareCodeService { try { const result = await db.shareCode.deleteMany({ where: { - OR: [ - { expiresAt: { lt: new Date() } }, - { isUsed: true }, - ], + OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }], }, }); @@ -172,4 +170,4 @@ export class ShareCodeService { return []; } } -} \ No newline at end of file +} diff --git a/apps/web/src/app/main/admin/deptsettingpage/page.tsx b/apps/web/src/app/main/admin/deptsettingpage/page.tsx index 868396a..8575fbb 100644 --- a/apps/web/src/app/main/admin/deptsettingpage/page.tsx +++ b/apps/web/src/app/main/admin/deptsettingpage/page.tsx @@ -4,18 +4,18 @@ import { ShareCodeValidator } from "../sharecode/sharecodevalidator"; import { useState, useRef, useCallback } from "react"; import { message, Progress, Button, Tabs, DatePicker } from "antd"; import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons"; -import {env} from '../../../../env' +import { env } from '../../../../env' const { TabPane } = Tabs; export default function DeptSettingPage() { const [uploadedFileId, setUploadedFileId] = useState(''); const [uploadedFileName, setUploadedFileName] = useState(''); const [fileNameMap, setFileNameMap] = useState>({}); - const [uploadedFiles, setUploadedFiles] = useState<{id: string, name: string}[]>([]); + const [uploadedFiles, setUploadedFiles] = useState<{ id: string, name: string }[]>([]); const [isDragging, setIsDragging] = useState(false); const [expireTime, setExpireTime] = useState(null); const dropRef = useRef(null); - + // 使用您的 useTusUpload hook const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({ onSuccess: (result) => { @@ -28,45 +28,56 @@ export default function DeptSettingPage() { } }); + // 清除已上传文件 + const handleClearFile = () => { + setUploadedFileId(''); + setUploadedFileName(''); + setUploadedFiles([]); + setFileNameMap({}); + }; + // 处理文件上传 const handleFileSelect = async (file: File) => { - const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 + // 限制:如果已有上传文件,则提示用户 + if (uploadedFiles.length > 0) { + message.warning('只能上传一个文件,请先删除已上传的文件'); + return; + } + const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识 + handleFileUpload( file, async (result) => { setUploadedFileId(result.fileId); setUploadedFileName(result.fileName); - // 添加到已上传文件列表 - setUploadedFiles(prev => [...prev, {id: result.fileId, name: file.name}]); - + setUploadedFiles([{ id: result.fileId, name: file.name }]); + // 在前端保存文件名映射(用于当前会话) - setFileNameMap(prev => ({ - ...prev, + setFileNameMap({ [result.fileId]: file.name - })); - + }); + // 上传成功后保存原始文件名到数据库 try { console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId); - + const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - fileId: result.fileId, + body: JSON.stringify({ + fileId: result.fileId, fileName: file.name }), }); - const responseText = await response.text(); console.log('保存文件名响应:', response.status, responseText); - + if (!response.ok) { console.error('保存文件名失败:', responseText); message.warning('文件名保存失败,下载时可能无法显示原始文件名'); @@ -77,7 +88,7 @@ export default function DeptSettingPage() { console.error('保存文件名请求失败:', error); message.warning('文件名保存失败,下载时可能无法显示原始文件名'); } - + message.success('文件上传成功'); }, (error) => { @@ -87,11 +98,36 @@ export default function DeptSettingPage() { ); }; - // 处理多个文件上传 + // 处理多个文件上传 - 已移除 // const handleFilesUpload = (file: File) => { // handleFileSelect(file); // }; + // 处理文件删除 + const handleDeleteFile = async (fileId: string) => { + try { + // 可以添加删除文件的API调用 + // const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, { + // method: 'DELETE' + // }); + + // if (!response.ok) { + // throw new Error('删除文件失败'); + // } + + // 无论服务器删除是否成功,前端都需要更新状态 + setUploadedFiles([]); + setUploadedFileId(''); + setUploadedFileName(''); + setFileNameMap({}); + + message.success('文件已删除'); + } catch (error) { + console.error('删除文件错误:', error); + message.error('删除文件失败'); + } + }; + // 拖拽相关处理函数 const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); @@ -115,7 +151,7 @@ export default function DeptSettingPage() { e.preventDefault(); e.stopPropagation(); setIsDragging(false); - + handleFileSelect(e.dataTransfer.files[0]); }, []); @@ -135,22 +171,22 @@ export default function DeptSettingPage() { if (!response.ok) { throw new Error('文件下载失败'); } - + // 创建下载链接 const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - + // 直接使用传入的 fileName link.download = fileName; - + // 触发下载 document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); - + message.success('文件下载开始'); } catch (error) { console.error('下载失败:', error); @@ -162,85 +198,101 @@ export default function DeptSettingPage() { return (

文件分享中心

- + {/* 文件上传区域 */}

第一步:上传文件

-
- -

点击或拖拽文件到此区域进行上传

-

支持单个上传文件

- - { - const file = e.target.files[0]; - if (file) { - handleFileSelect(file); - } - }} - disabled={isUploading} - - /> - -
+ +

点击或拖拽文件到此区域进行上传

+

只能上传单个文件

+ + { + const file = e.target.files?.[0]; + if (file) { + handleFileSelect(file); + } + }} + disabled={isUploading} + /> + +
+ ) : ( +
+
+

+ 您已上传文件,请继续下一步生成分享码 +

+
+
+ )} {/* 已上传文件列表 */} {uploadedFiles.length > 0 && ( -
- {uploadedFiles.map((file, index) => ( -
( +
-
-
{file.name}
-
))} @@ -262,9 +316,9 @@ export default function DeptSettingPage() { {isUploading && (
-
)} @@ -280,7 +334,7 @@ export default function DeptSettingPage() { {uploadedFileId && (

第二步:生成分享码

- +

使用分享码下载文件

-
diff --git a/apps/web/src/app/main/plan/page.tsx b/apps/web/src/app/main/plan/page.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx b/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx index 0b57f6e..547d7bc 100644 --- a/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx +++ b/apps/web/src/app/main/staffinfo_show/staffmessage_page.tsx @@ -1,9 +1,11 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { AgGridReact } from 'ag-grid-react'; import { api, useStaff } from "@nice/client"; -import { Button, CascaderProps, message, Modal } from 'antd'; +import { Button, CascaderProps, message, Modal, Input, Upload } from 'antd'; import { areaOptions } from '@web/src/app/main/staffinfo_write/area-options'; import StaffInfoWrite from '@web/src/app/main/staffinfo_write/staffinfo_write.page'; +import { utils, writeFile, read } from 'xlsx'; +import { UploadOutlined } from '@ant-design/icons'; function getAreaName(codes: string[], level?: number): string { const result: string[] = []; @@ -18,6 +20,24 @@ function getAreaName(codes: string[], level?: number): string { return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/'); } +// 添加表头提取工具函数 +function extractHeaders(columns: any[]): string[] { + const result: string[] = []; + + const extractHeadersRecursive = (cols: any[]) => { + cols.forEach(col => { + if ('children' in col && col.children) { + extractHeadersRecursive(col.children); + } else if (col.headerName) { + result.push(col.headerName); + } + }); + }; + + extractHeadersRecursive(columns); + return result; +} + export default function StaffMessage() { const [rowData, setRowData] = useState([]); const [columnDefs, setColumnDefs] = useState([]); @@ -26,6 +46,11 @@ export default function StaffMessage() { const fields = useCustomFields(); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [currentEditStaff, setCurrentEditStaff] = useState(null); + const [gridApi, setGridApi] = useState(null); + const [fileNameVisible, setFileNameVisible] = useState(false); + const [fileName, setFileName] = useState(''); + const [defaultFileName] = useState(`员工数据_${new Date().toISOString().slice(0, 10)}`); + const [importVisible, setImportVisible] = useState(false); // 获取数据 const { data: staffData } = api.staff.findMany.useQuery({ @@ -61,7 +86,6 @@ export default function StaffMessage() { setCurrentEditStaff(selectedRows[0]); setIsEditModalVisible(true); }, [selectedRows]); - console.log('选中行',currentEditStaff); // 处理编辑完成 const handleEditComplete = useCallback(() => { setIsEditModalVisible(false); @@ -150,6 +174,241 @@ export default function StaffMessage() { staffData && setRowData(staffData); }, [staffData]); + // 修改导出模板处理函数 + const handleExportTemplate = useCallback(() => { + const headerNames = extractHeaders(columnDefs); + + // 创建示例数据行 + let exampleRow: Record = {}; + + // 检查是否有选中行 + if (selectedRows.length > 0) { + // 使用第一条选中的记录作为模板数据 + const templateData = selectedRows[0]; + + // 基础字段 + exampleRow['姓名'] = templateData.showname || ''; + exampleRow['所属部门'] = templateData.department?.name || ''; + + // 处理自定义字段 + const fieldsList = Array.isArray(fields?.data) ? fields.data : []; + fieldsList.forEach((field: any) => { + const fieldValue = templateData.fieldValues?.find( + (fv: any) => fv.fieldId === field.id + )?.value; + + let displayValue = fieldValue; + + // 根据字段类型处理值 + if (field.type === 'cascader' && fieldValue) { + displayValue = getAreaName(fieldValue.split('/')); + } else if (field.type === 'date' && fieldValue) { + displayValue = new Date(fieldValue).toLocaleDateString(); + } else if (field.type === 'textarea' && fieldValue) { + displayValue = fieldValue.replace(/,/g, '\n'); + } + + exampleRow[field.label || field.name] = displayValue || ''; + }); + } else { + // 如果没有选中行,使用默认示例数据 + exampleRow['姓名'] = '张三'; + exampleRow['所属部门'] = '技术部'; + + // 添加所有自定义字段的空值 + fieldsList.forEach((field: any) => { + exampleRow[field.label || field.name] = ''; + }); + } + + // 创建空白行供用户填写 + const emptyRow = headerNames.reduce((obj, header) => { + obj[header] = ''; + return obj; + }, {} as Record); + + // 创建工作簿和工作表 + const wb = utils.book_new(); + const ws = utils.json_to_sheet([exampleRow], { header: headerNames }); + + // 设置列宽 + const colWidth = headerNames.map(() => ({ wch: 20 })); + ws['!cols'] = colWidth; + + // 在第二行添加提示文字 + const rowIdx = 2; // 第二行索引 + const cellRef = utils.encode_cell({ r: rowIdx, c: 0 }); // A3单元格 + + const tipText = selectedRows.length > 0 + ? '以上为选中人员数据,请在下方行填写实际数据' + : '以上为示例数据,请在下方行填写实际数据'; + + ws[cellRef] = { t: 's', v: tipText }; + + // 手动添加空白行 + utils.sheet_add_json(ws, [emptyRow], { skipHeader: true, origin: rowIdx + 1 }); + + // 合并提示文字单元格 + if (!ws['!merges']) ws['!merges'] = []; + ws['!merges'].push({ + s: { r: rowIdx, c: 0 }, // 起始单元格 A3 + e: { r: rowIdx, c: Math.min(5, headerNames.length - 1) } // 结束单元格,跨越多列 + }); + + utils.book_append_sheet(wb, ws, "员工模板"); + writeFile(wb, `员工数据模板_${new Date().toISOString().slice(0, 10)}.xlsx`); + }, [columnDefs, selectedRows, fields.data]); + + // 导出数据处理函数 + const handleConfirm = useCallback(() => { + setFileNameVisible(true); + }, []); + + // 处理文件名确认 + const handleFileNameConfirm = useCallback(() => { + setFileNameVisible(false); + if (!gridApi) { + console.error('Grid API 未正确初始化'); + return; + } + + try { + // 获取所有数据 + const allData = selectedRows.length > 0 ? selectedRows : rowData; + + // 格式化数据 + const exportData = allData.map(row => { + const formattedRow: Record = {}; + + // 基础字段 + formattedRow['姓名'] = row.showname || ''; + formattedRow['所属部门'] = row.department?.name || ''; + + // 动态字段 + const fieldsList = Array.isArray(fields?.data) ? fields.data : []; + fieldsList.forEach((field: any) => { + const fieldValue = row.fieldValues?.find((fv: any) => fv.fieldId === field.id)?.value; + let displayValue = fieldValue; + + // 根据字段类型处理值 + if (field.type === 'cascader' && fieldValue) { + displayValue = getAreaName(fieldValue.split('/')); + } else if (field.type === 'date' && fieldValue) { + displayValue = new Date(fieldValue).toLocaleDateString(); + } else if (field.type === 'textarea' && fieldValue) { + displayValue = fieldValue.replace(/,/g, '\n'); + } + + formattedRow[field.label || field.name] = displayValue || ''; + }); + + return formattedRow; + }); + + // 生成工作表 + const ws = utils.json_to_sheet(exportData); + const wb = utils.book_new(); + utils.book_append_sheet(wb, ws, "员工数据"); + + // 生成文件名 + const finalFileName = fileName || defaultFileName; + writeFile(wb, `${finalFileName}.xlsx`); + } catch (error) { + console.error('导出失败:', error); + message.error('导出失败,请稍后重试'); + } + }, [fileName, defaultFileName, rowData, selectedRows, fields.data, gridApi]); + + // 处理导入数据 + const createMany = api.staff.create.useMutation({ + onSuccess: () => { + message.success('员工数据导入成功'); + // 刷新数据 + api.staff.findMany.useQuery(); + setImportVisible(false); + }, + onError: (error) => { + message.error(`导入失败: ${error.message}`); + } + }); + + // 处理Excel导入数据 + const handleImportData = useCallback((excelData: any[]) => { + if (excelData.length === 0) { + message.warning('没有可导入的数据'); + return; + } + + try { + // 将Excel数据转换为API需要的格式 + const staffData = excelData.map(row => { + const staff: any = { fieldValues: [] }; + + // 处理基础字段 + if (row['姓名']) staff.showname = row['姓名']; + + // 处理部门 + if (row['所属部门']) { + // 简单存储部门名称,后续可能需要查询匹配 + staff.departmentName = row['所属部门']; + } + + // 处理自定义字段 + const fieldsList = Array.isArray(fields?.data) ? fields.data : []; + fieldsList.forEach((field: any) => { + let value = row[field.label || field.name]; + + // 跳过空值 + if (value === undefined || value === '') return; + + // 根据字段类型处理输入值 + switch (field.type) { + case 'cascader': + // 级联选择器可能需要将显示名称转回代码 + // 这里简单保留原值,实际可能需要查询转换 + break; + + case 'date': + // 尝试将日期字符串转换为ISO格式 + try { + const dateObj = new Date(value); + if (!isNaN(dateObj.getTime())) { + value = dateObj.toISOString(); + } + } catch (e) { + console.error(`日期格式转换错误: ${value}`); + } + break; + + case 'textarea': + // 将换行符替换回逗号进行存储 + if (typeof value === 'string') { + value = value.replace(/\n/g, ','); + } + break; + + // 可以根据需要添加其他字段类型的处理 + } + + // 添加到fieldValues数组 + staff.fieldValues.push({ + fieldId: field.id, + value: String(value) + }); + }); + + return staff; + }); + + // 提交数据 + createMany.mutate({ data: staffData }); + message.info(`正在导入${staffData.length}条员工数据...`); + } catch (error) { + console.error('处理导入数据失败:', error); + message.error('数据格式错误,导入失败'); + } + }, [fields.data, createMany]); + return ( <>
@@ -158,6 +417,15 @@ export default function StaffMessage() { onClick={handleDelete}>批量删除 + + +
- + {/* 编辑弹窗 */} {currentEditStaff && ( - )} + + {/* 导出文件名对话框 */} + setFileNameVisible(false)} + okText="导出" + cancelText="取消" + > + setFileName(e.target.value)} + /> + + + {/* 导入对话框 */} + setImportVisible(false)} + footer={null} + > + { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const wb = read(e.target?.result); + const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + + if (data.length === 0) { + message.warning('Excel文件中没有数据'); + return; + } + + message.info(`读取到${data.length}条数据,正在处理...`); + handleImportData(data); + } catch (error) { + console.error('解析Excel文件失败:', error); + message.error('Excel文件格式错误,请确保使用正确的模板'); + } + }; + reader.readAsArrayBuffer(file); + return false; + }} + > + + +
+ 提示:请使用导出模板功能获取标准模板,按格式填写数据后导入 +
+
); } \ No newline at end of file