|
|
@@ -1,11 +1,96 @@
|
|
|
-import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
|
|
-import { Modal, Table, Button, Image, message, Space } from 'antd';
|
|
|
-import { DeleteOutlined, ZoomInOutlined } from '@ant-design/icons';
|
|
|
+import React, { useState, useEffect, useImperativeHandle, forwardRef, useRef } from 'react';
|
|
|
+import { Modal, Table, Button, Image, message, Space, Upload, Input } from 'antd';
|
|
|
+import { DeleteOutlined, ZoomInOutlined, PlusOutlined } from '@ant-design/icons';
|
|
|
import { userCharacteristicApi } from '../../api/user/characteristic';
|
|
|
import { UserCharacteristic } from '../../types';
|
|
|
import { formatDateTimeFull } from '../../utils/formatTime';
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
+import axiosInstance from '../../utils/axios';
|
|
|
+
|
|
|
+/** 带鉴权加载的图片:通过 axios 携带 token 请求,解决 img 直链 401 不显示 */
|
|
|
+const AuthImage = ({
|
|
|
+ src,
|
|
|
+ alt,
|
|
|
+ width = 60,
|
|
|
+ height = 60,
|
|
|
+ style,
|
|
|
+ noImageText,
|
|
|
+}: {
|
|
|
+ src: string;
|
|
|
+ alt: string;
|
|
|
+ width?: number;
|
|
|
+ height?: number;
|
|
|
+ style?: React.CSSProperties;
|
|
|
+ noImageText?: string;
|
|
|
+}) => {
|
|
|
+ const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [error, setError] = useState(false);
|
|
|
+ const blobUrlRef = useRef<string | null>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!src) {
|
|
|
+ setLoading(false);
|
|
|
+ setError(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setLoading(true);
|
|
|
+ setError(false);
|
|
|
+ let cancelled = false;
|
|
|
+ axiosInstance
|
|
|
+ .get(src, { responseType: 'blob' })
|
|
|
+ .then((blob) => {
|
|
|
+ if (cancelled) return;
|
|
|
+ const url = URL.createObjectURL(blob as Blob);
|
|
|
+ blobUrlRef.current = url;
|
|
|
+ setBlobUrl(url);
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ if (!cancelled) setError(true);
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ if (!cancelled) setLoading(false);
|
|
|
+ });
|
|
|
+ return () => {
|
|
|
+ cancelled = true;
|
|
|
+ if (blobUrlRef.current) {
|
|
|
+ URL.revokeObjectURL(blobUrlRef.current);
|
|
|
+ blobUrlRef.current = null;
|
|
|
+ }
|
|
|
+ setBlobUrl(null);
|
|
|
+ };
|
|
|
+ }, [src]);
|
|
|
+
|
|
|
+ const placeholderStyle: React.CSSProperties = {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ borderRadius: 4,
|
|
|
+ border: '1px solid #dcdfe6',
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ color: '#999',
|
|
|
+ fontSize: 12,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (loading) {
|
|
|
+ return <div style={placeholderStyle}>...</div>;
|
|
|
+ }
|
|
|
+ if (error || !blobUrl) {
|
|
|
+ return <div style={placeholderStyle}>{noImageText || '暂无图片'}</div>;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <Image
|
|
|
+ width={width}
|
|
|
+ height={height}
|
|
|
+ src={blobUrl}
|
|
|
+ alt={alt}
|
|
|
+ preview={{ mask: <ZoomInOutlined /> }}
|
|
|
+ style={{ borderRadius: 4, border: '1px solid #dcdfe6', ...style }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
interface FaceOrFingerFormProps {
|
|
|
onSuccess?: () => void;
|
|
|
@@ -30,6 +115,13 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
const [userId, setUserId] = useState<number>();
|
|
|
const [userType, setUserType] = useState<string>();
|
|
|
const [dataType, setDataType] = useState<'finger' | 'face'>('finger');
|
|
|
+ const [currentUserName, setCurrentUserName] = useState('');
|
|
|
+
|
|
|
+ // 新增人脸:上传弹框
|
|
|
+ const [uploadVisible, setUploadVisible] = useState(false);
|
|
|
+ const [uploadFileList, setUploadFileList] = useState<any[]>([]);
|
|
|
+ const [uploadUserName, setUploadUserName] = useState('');
|
|
|
+ const [uploadLoading, setUploadLoading] = useState(false);
|
|
|
|
|
|
// 打开弹窗
|
|
|
const open = async (type: string, id: number, row: any) => {
|
|
|
@@ -39,6 +131,7 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
setDialogTitle(isFinger ? t('form.fingerprintData') : t('form.faceData'));
|
|
|
setUserId(row.id);
|
|
|
setUserType(isFinger ? '1' : '2');
|
|
|
+ setCurrentUserName(row?.username ?? row?.nickname ?? '');
|
|
|
setQueryParams({ pageNo: 1, pageSize: 10 });
|
|
|
setTotal(0);
|
|
|
setSelectedRowKeys([]);
|
|
|
@@ -49,7 +142,7 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
open,
|
|
|
}));
|
|
|
|
|
|
- // 获取指纹或人脸列表
|
|
|
+ // 获取指纹或人脸列表(兼容接口返回 { code, data: { list, total }, msg },axios 会解包为 { list, total })
|
|
|
const getFaceOrFingerList = async () => {
|
|
|
if (!userId || !userType) return;
|
|
|
|
|
|
@@ -61,8 +154,10 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
userId: userId,
|
|
|
type: userType,
|
|
|
});
|
|
|
- setTableData(response.list || []);
|
|
|
- setTotal(response.total || 0);
|
|
|
+ const list = response?.data?.list ?? response?.list ?? [];
|
|
|
+ const totalCount = response?.data?.total ?? response?.total ?? 0;
|
|
|
+ setTableData(Array.isArray(list) ? list : []);
|
|
|
+ setTotal(typeof totalCount === 'number' ? totalCount : 0);
|
|
|
} catch (error: any) {
|
|
|
message.error(error.message || t('form.getDataFailed'));
|
|
|
} finally {
|
|
|
@@ -76,6 +171,37 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
}
|
|
|
}, [queryParams.pageNo, queryParams.pageSize, dialogVisible]);
|
|
|
|
|
|
+ const openUploadModal = () => {
|
|
|
+ setUploadFileList([]);
|
|
|
+ setUploadUserName(currentUserName);
|
|
|
+ setUploadVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUploadSubmit = async () => {
|
|
|
+ const userName = uploadUserName?.trim();
|
|
|
+ if (!userName) {
|
|
|
+ message.warning(t('form.userName') + (t('common.required') ? ` ${t('common.required')}` : '不能为空'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const file = uploadFileList[0]?.originFileObj;
|
|
|
+ if (!file) {
|
|
|
+ message.warning(t('form.pleaseSelectImage'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setUploadLoading(true);
|
|
|
+ try {
|
|
|
+ await userCharacteristicApi.insertUserFace(userName, file);
|
|
|
+ message.success(t('common.uploadSuccess') || '上传成功');
|
|
|
+ setUploadVisible(false);
|
|
|
+ setUploadFileList([]);
|
|
|
+ await getFaceOrFingerList();
|
|
|
+ } catch (error: any) {
|
|
|
+ message.error(error?.message || t('common.uploadFailed') || '上传失败');
|
|
|
+ } finally {
|
|
|
+ setUploadLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 删除指纹或人脸
|
|
|
const handleDelete = async (id?: number) => {
|
|
|
const idsToDelete = id ? [id] : selectedRowKeys.map(key => Number(key));
|
|
|
@@ -120,20 +246,26 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
{
|
|
|
title: dataType === 'finger' ? t('form.fingerprint') : t('form.face'),
|
|
|
align: 'center',
|
|
|
- render: (_: any, record: UserCharacteristic) => (
|
|
|
- <div style={{ position: 'relative', display: 'inline-block' }}>
|
|
|
- <Image
|
|
|
- width={60}
|
|
|
- height={60}
|
|
|
- src={record.imageUrl}
|
|
|
- alt={dataType === 'finger' ? t('form.fingerprint') : t('form.face')}
|
|
|
- preview={{
|
|
|
- mask: <ZoomInOutlined />,
|
|
|
- }}
|
|
|
- style={{ borderRadius: 4, border: '1px solid #dcdfe6' }}
|
|
|
- />
|
|
|
- </div>
|
|
|
- ),
|
|
|
+ render: (_: any, record: UserCharacteristic) => {
|
|
|
+ const imgSrc = record.imageUrl || record.imagePath;
|
|
|
+ return (
|
|
|
+ <div style={{ position: 'relative', display: 'inline-block' }}>
|
|
|
+ {imgSrc ? (
|
|
|
+ <AuthImage
|
|
|
+ src={imgSrc}
|
|
|
+ alt={dataType === 'finger' ? t('form.fingerprint') : t('form.face')}
|
|
|
+ width={60}
|
|
|
+ height={60}
|
|
|
+ noImageText={t('form.noImage') || '暂无图片'}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div style={{ width: 60, height: 60, borderRadius: 4, border: '1px solid #dcdfe6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', fontSize: 12 }}>
|
|
|
+ {t('form.noImage') || '暂无图片'}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
},
|
|
|
{
|
|
|
title: t('table.createTime'),
|
|
|
@@ -179,7 +311,17 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
width={800}
|
|
|
destroyOnClose
|
|
|
>
|
|
|
- <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
|
|
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
|
+ {dataType === 'face' && (
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={openUploadModal}
|
|
|
+ disabled={formLoading}
|
|
|
+ >
|
|
|
+ {t('common.add')}
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
<Button
|
|
|
type="primary"
|
|
|
danger
|
|
|
@@ -202,6 +344,48 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
|
|
|
bordered
|
|
|
/>
|
|
|
|
|
|
+ {/* 新增人脸:图片上传弹框(含 userName 输入框,中英文用 form.uploadFace / form.userName / form.selectImage / form.pleaseSelectImage) */}
|
|
|
+ <Modal
|
|
|
+ title={t('form.uploadFace')}
|
|
|
+ open={uploadVisible}
|
|
|
+ onCancel={() => { setUploadVisible(false); setUploadFileList([]); }}
|
|
|
+ onOk={handleUploadSubmit}
|
|
|
+ confirmLoading={uploadLoading}
|
|
|
+ okText={t('common.confirm')}
|
|
|
+ cancelText={t('common.cancel')}
|
|
|
+ destroyOnClose
|
|
|
+ >
|
|
|
+ <div style={{ marginBottom: 16 }}>
|
|
|
+ <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
|
|
+ {t('form.userName')}
|
|
|
+ </label>
|
|
|
+ <Input
|
|
|
+ value={uploadUserName}
|
|
|
+ onChange={(e) => setUploadUserName(e.target.value)}
|
|
|
+ placeholder={t('form.userName')}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
|
|
+ {t('form.selectImage')}
|
|
|
+ </label>
|
|
|
+ <Upload
|
|
|
+ accept="image/*"
|
|
|
+ fileList={uploadFileList}
|
|
|
+ listType="picture-card"
|
|
|
+ maxCount={1}
|
|
|
+ beforeUpload={(file) => {
|
|
|
+ setUploadFileList([{ uid: file.uid, name: file.name, status: 'done', originFileObj: file }]);
|
|
|
+ return false;
|
|
|
+ }}
|
|
|
+ onRemove={() => setUploadFileList([])}
|
|
|
+ >
|
|
|
+ {uploadFileList.length === 0 && t('form.selectImage')}
|
|
|
+ </Upload>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+
|
|
|
{/* 分页 */}
|
|
|
{total > 0 && (
|
|
|
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|