This commit is contained in:
Li1304553726 2025-04-02 10:24:12 +08:00
parent 652883d142
commit ee320de2ca
4 changed files with 481 additions and 105 deletions

View File

@ -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<GenerateShareCodeResponse> {
async generateShareCode(
fileId: string,
fileName?: string,
): Promise<GenerateShareCodeResponse> {
try {
// 检查文件是否存在
const resource = await this.resourceService.findUnique({
@ -46,9 +49,7 @@ export class ShareCodeService {
expiresAt,
isUsed: false,
// 只在没有现有文件名且提供了新文件名时才更新文件名
...(fileName && !existingShareCode.fileName
? { fileName }
: {})
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
},
});
} else {
@ -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 }],
},
});

View File

@ -4,14 +4,14 @@ 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<string>('');
const [uploadedFileName, setUploadedFileName] = useState<string>('');
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
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<Date | null>(null);
const dropRef = useRef<HTMLDivElement>(null);
@ -28,8 +28,22 @@ export default function DeptSettingPage() {
}
});
// 清除已上传文件
const handleClearFile = () => {
setUploadedFileId('');
setUploadedFileName('');
setUploadedFiles([]);
setFileNameMap({});
};
// 处理文件上传
const handleFileSelect = async (file: File) => {
// 限制:如果已有上传文件,则提示用户
if (uploadedFiles.length > 0) {
message.warning('只能上传一个文件,请先删除已上传的文件');
return;
}
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
handleFileUpload(
@ -38,15 +52,13 @@ export default function DeptSettingPage() {
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 {
@ -63,7 +75,6 @@ export default function DeptSettingPage() {
}),
});
const responseText = await response.text();
console.log('保存文件名响应:', response.status, responseText);
@ -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<HTMLDivElement>) => {
e.preventDefault();
@ -168,54 +204,71 @@ export default function DeptSettingPage() {
{/* 文件上传区域 */}
<div style={{ marginBottom: '40px' }}>
<h3></h3>
<div
ref={dropRef}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
padding: '20px',
border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`,
borderRadius: '8px',
textAlign: 'center',
backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
transition: 'all 0.3s',
marginBottom: '20px'
}}
>
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
<p></p>
<p style={{ fontSize: '12px', color: '#888' }}></p>
<input
type="file"
id="file-input"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files[0];
if (file) {
handleFileSelect(file);
}
}}
disabled={isUploading}
/>
<label
htmlFor="file-input"
{/* 如果没有已上传文件,显示上传区域 */}
{uploadedFiles.length === 0 ? (
<div
ref={dropRef}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
display: 'inline-block',
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
padding: '20px',
border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`,
borderRadius: '8px',
textAlign: 'center',
backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
transition: 'all 0.3s',
marginBottom: '20px'
}}
>
<UploadOutlined />
</label>
</div>
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
<p></p>
<p style={{ fontSize: '12px', color: '#888' }}></p>
<input
type="file"
id="file-input"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
}}
disabled={isUploading}
/>
<label
htmlFor="file-input"
style={{
display: 'inline-block',
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
}}
>
<UploadOutlined />
</label>
</div>
) : (
<div style={{ marginBottom: '20px' }}>
<div style={{
padding: '10px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
marginBottom: '10px'
}}>
<p style={{ color: '#52c41a', margin: 0 }}>
</p>
</div>
</div>
)}
{/* 已上传文件列表 */}
{uploadedFiles.length > 0 && (
@ -224,13 +277,12 @@ export default function DeptSettingPage() {
borderRadius: '4px',
overflow: 'hidden'
}}>
{uploadedFiles.map((file, index) => (
{uploadedFiles.map((file) => (
<div key={file.id} style={{
display: 'flex',
alignItems: 'center',
padding: '10px 15px',
borderBottom: index < uploadedFiles.length - 1 ? '1px solid #f0f0f0' : 'none',
backgroundColor: index % 2 === 0 ? '#fafafa' : 'white'
backgroundColor: '#fafafa'
}}>
<div style={{
display: 'flex',
@ -254,6 +306,8 @@ export default function DeptSettingPage() {
<Button
type="text"
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
onClick={() => handleDeleteFile(file.id)}
title="删除此文件"
/>
</div>
))}

View File

@ -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<any[]>([]);
const [columnDefs, setColumnDefs] = useState<any[]>([]);
@ -26,6 +46,11 @@ export default function StaffMessage() {
const fields = useCustomFields();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [currentEditStaff, setCurrentEditStaff] = useState<any>(null);
const [gridApi, setGridApi] = useState<any>(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<string, string> = {};
// 检查是否有选中行
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<string, string>);
// 创建工作簿和工作表
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<string, any> = {};
// 基础字段
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 (
<>
<div className="flex justify-between items-center mb-4">
@ -158,6 +417,15 @@ export default function StaffMessage() {
onClick={handleDelete}></Button>
<Button className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
onClick={handleEdit}></Button>
<Button onClick={() => setImportVisible(true)} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600">
Excel
</Button>
<Button onClick={handleExportTemplate} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
</Button>
<Button onClick={handleConfirm} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
</Button>
</div>
<Button className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
onClick={() => {
@ -180,6 +448,7 @@ export default function StaffMessage() {
headerHeight={40}
rowHeight={40}
domLayout="autoHeight"
onGridReady={(params) => setGridApi(params.api)}
/>
</div>
</div>
@ -203,6 +472,61 @@ export default function StaffMessage() {
/>
)}
</Modal>
{/* 导出文件名对话框 */}
<Modal
title="输入文件名"
open={fileNameVisible}
onOk={handleFileNameConfirm}
onCancel={() => setFileNameVisible(false)}
okText="导出"
cancelText="取消"
>
<Input
placeholder={`默认名称: ${defaultFileName}`}
value={fileName}
onChange={(e) => setFileName(e.target.value)}
/>
</Modal>
{/* 导入对话框 */}
<Modal
title="导入员工数据"
open={importVisible}
onCancel={() => setImportVisible(false)}
footer={null}
>
<Upload
accept=".xlsx,.xls"
beforeUpload={file => {
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;
}}
>
<Button icon={<UploadOutlined />}>Excel文件</Button>
</Upload>
<div className="mt-4 text-gray-500 text-sm">
使
</div>
</Modal>
</>
);
}