- 将多个组件中的 @chageable/components 导入改为从 @/components 导入 - 在 BoxBrandList.tsx、CostList.tsx、ProductDataList.tsx 等文件中更新 ProFormBizSelect 相关组件导入 - 在 CaptchaModal/index.tsx 中将 Captcha 组件导入从 @chageable/components 改为 @/components - 在 ChannelList.tsx、CompanyList.tsx、EmployeeList.tsx 等文件中更新 ProFormUploadMaterial 导入 - 在 MaterialList.tsx 中更新 ProFormBizTreeSelect 和 ProFormUploadOss 导入 - 在 MenuList.tsx、OrderCostList.tsx 中更新 ProFormBizSelect 相关组件导入 - 在 DealerModal.tsx、MaterialModal.tsx、OrderModal.tsx 等文件中更新 SelectModal 导入 - 在 app.tsx 中将 LeftMenu 导入从 @chageable/components 改为 @/components - 新增 UploadMaterial 组件并添加到 components/index.ts 导出 - 在 Captcha 组件中添加滑动验证码功能,包含样式和交互逻辑 - 在 companyPaymentAccount 工具函数中添加 branchName 字段支持
407 lines
12 KiB
TypeScript
407 lines
12 KiB
TypeScript
import {
|
||
CheckCircleOutlined,
|
||
CloseCircleOutlined,
|
||
DoubleRightOutlined,
|
||
LoadingOutlined,
|
||
ReloadOutlined,
|
||
} from '@ant-design/icons';
|
||
import { Modal, Skeleton, message } from 'antd';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import './index.less';
|
||
import { checkCaptchaMock, getPictureMock } from './mock';
|
||
import { AJCaptchaIconProps, AJCaptchaSliderProps, CaptchaRes } from './typing';
|
||
import { aesEncrypt, setStyle, uuid } from './utils';
|
||
|
||
const Scale = {
|
||
default: 1,
|
||
big: 1.4,
|
||
large: 1.8,
|
||
};
|
||
|
||
const AJCaptchaIcon = (props: { icon: AJCaptchaIconProps }) => {
|
||
const iconStyle: React.CSSProperties = {
|
||
fontSize: '18px',
|
||
color: '#999',
|
||
};
|
||
switch (props.icon) {
|
||
case 'right':
|
||
return <DoubleRightOutlined style={iconStyle} />;
|
||
case 'fail':
|
||
return <CloseCircleOutlined style={{ ...iconStyle, color: '#ff4d4f' }} />;
|
||
case 'loading':
|
||
return <LoadingOutlined style={iconStyle} />;
|
||
case 'check':
|
||
return <CheckCircleOutlined style={{ ...iconStyle, color: '#52c41a' }} />;
|
||
}
|
||
};
|
||
|
||
const AJCaptchaSlider: React.FC<AJCaptchaSliderProps> = ({
|
||
show = false,
|
||
title,
|
||
tips,
|
||
refreshText,
|
||
size = 'default',
|
||
vSpace = 18, // 图片与滑块的距离,单位px
|
||
sliderBlockWidth = 45, // 滑块宽度45,单位px
|
||
padding = 16, // 弹框内边距 单位px
|
||
hide,
|
||
onSuccess,
|
||
setSize = {
|
||
imgWidth: 310, // 图片宽度
|
||
imgHeight: 155, // 图片高度
|
||
barHeight: 36, // 滑块框高度
|
||
},
|
||
getPicture = getPictureMock,
|
||
checkCaptcha = checkCaptchaMock,
|
||
}) => {
|
||
const scale = Scale[size];
|
||
const blockWidth = sliderBlockWidth * scale;
|
||
const imgWidth = setSize.imgWidth * scale;
|
||
const imgHeight = setSize.imgHeight * scale;
|
||
|
||
const isSupportTouch = 'ontouchstart' in window;
|
||
const events = isSupportTouch
|
||
? {
|
||
start: 'touchstart',
|
||
move: 'touchmove',
|
||
end: 'touchend',
|
||
}
|
||
: {
|
||
start: 'mousedown',
|
||
move: 'mousemove',
|
||
end: 'mouseup',
|
||
};
|
||
|
||
const [isLoading, setLoading] = useState<boolean>(false); // 是否加载
|
||
const [response, setResponse] = useState<CaptchaRes | null>(null); // token、密钥、图片等数据
|
||
const [icon, setIcon] = useState<AJCaptchaIconProps>('loading'); // 滑块icon
|
||
const [showTips, setTips] = useState<boolean>(true); // 是否展示提示文案
|
||
const [blockPressed, setPressed] = useState<boolean>(false); // 滑块是否被按下
|
||
|
||
const barAreaRef = useRef({
|
||
barAreaLeft: 0,
|
||
barAreaOffsetWidth: 0,
|
||
});
|
||
const leftBarRef = useRef<HTMLDivElement>(null);
|
||
|
||
const isEnd = useRef<boolean>(false);
|
||
const status = useRef<boolean>(false);
|
||
|
||
const closeBox = () => {
|
||
setResponse(null);
|
||
hide?.();
|
||
};
|
||
|
||
const getData = () => {
|
||
setLoading(true);
|
||
setIcon('right');
|
||
getPicture()
|
||
.then((res) => {
|
||
setResponse(res);
|
||
})
|
||
.finally(() => setLoading(false));
|
||
};
|
||
|
||
/**
|
||
* 刷新数据和界面状态的函数
|
||
* 此函数主要用于在不加载中的情况下,重新获取数据并重置鼠标状态、结束状态、提示信息和布局宽度
|
||
* 它确保在界面交互过程中,用户界面始终保持一致和响应性
|
||
*/
|
||
const refresh = () => {
|
||
// 检查数据加载状态,如果正在加载,则不执行后续操作
|
||
if (isLoading) return;
|
||
|
||
// 重新获取数据
|
||
getData();
|
||
|
||
// 重置状态,准备下一次交互
|
||
isEnd.current = false;
|
||
status.current = false;
|
||
|
||
// 设置提示信息,指导用户进行下一步操作
|
||
setTips(true);
|
||
};
|
||
|
||
/**
|
||
* 处理滑动事件的方法,用于更新滑动块的位置和相关的状态
|
||
* @param e React的鼠标点击事件或触摸事件对象
|
||
*/
|
||
const move = (
|
||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||
) => {
|
||
// 如果当前状态不允许滑动或滑动已结束,则直接返回,不执行后续操作
|
||
if (!status.current || isEnd.current) return;
|
||
|
||
// 根据事件类型(触摸或鼠标)获取滑动的x坐标
|
||
const x = 'touches' in e ? e.touches[0].pageX : e.clientX;
|
||
|
||
// 计算滑动块可以移动的最大左边距
|
||
const maxLeft = barAreaRef.current.barAreaOffsetWidth - blockWidth;
|
||
|
||
// 根据滑动位置计算滑动块的实际左边距,确保它在允许的范围内
|
||
const moveBlockLeft = Math.max(
|
||
0,
|
||
Math.min(x - barAreaRef.current.barAreaLeft - blockWidth / 2, maxLeft),
|
||
);
|
||
|
||
// 拖动后小方块的left值
|
||
const left = Math.max(0, moveBlockLeft);
|
||
|
||
// 计算对应leftBar的width值
|
||
const width = `${left + blockWidth}px`;
|
||
|
||
// 设置提示信息为空,表示滑动操作正常进行,无错误或额外信息需要展示
|
||
setTips(false);
|
||
|
||
// 设置leftBar的width值
|
||
setStyle(leftBarRef.current, { width });
|
||
};
|
||
|
||
const end = () => {
|
||
document.removeEventListener(events.move, move as any);
|
||
document.removeEventListener(events.end, end);
|
||
document.removeEventListener('touchcancel', end);
|
||
// 判断是否重合
|
||
if (status.current && !isEnd.current) {
|
||
setIcon('loading');
|
||
|
||
const leftBarWidth = parseInt(
|
||
(leftBarRef.current?.style.width || '').replace('px', ''),
|
||
);
|
||
|
||
const rawPointJson = JSON.stringify({
|
||
x: (leftBarWidth - blockWidth) / scale, // 计算x轴偏移量
|
||
y: 5.0,
|
||
});
|
||
|
||
const data = {
|
||
captchaType: 'blockPuzzle',
|
||
pointJson: response?.secretKey
|
||
? aesEncrypt(rawPointJson, response?.secretKey)
|
||
: rawPointJson,
|
||
token: response?.token || '',
|
||
clientUid: localStorage.getItem('slider')!,
|
||
ts: Date.now(),
|
||
};
|
||
|
||
checkCaptcha(data)
|
||
.then((res) => {
|
||
isEnd.current = true;
|
||
if (res.token) {
|
||
setIcon('check');
|
||
message.success('验证成功!');
|
||
setTimeout(() => {
|
||
const params = `${res.token}---${rawPointJson}`;
|
||
onSuccess(aesEncrypt(params, response?.secretKey));
|
||
closeBox();
|
||
}, 1000);
|
||
} else {
|
||
setIcon('fail');
|
||
message.error('验证失败!');
|
||
setTimeout(() => {
|
||
refresh();
|
||
}, 800);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
isEnd.current = true;
|
||
setIcon('fail');
|
||
message.error('验证失败!');
|
||
setTimeout(() => {
|
||
refresh();
|
||
}, 800);
|
||
});
|
||
status.current = false;
|
||
setPressed(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!localStorage.getItem('slider'))
|
||
localStorage.setItem('slider', `slider-${uuid()}`);
|
||
|
||
// 清理函数
|
||
return () => {
|
||
if (localStorage.getItem('slider')) localStorage.removeItem('slider');
|
||
document.removeEventListener(events.move, move as any);
|
||
document.removeEventListener(events.end, end);
|
||
document.removeEventListener('touchcancel', end);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (show) refresh();
|
||
}, [show]);
|
||
|
||
/**
|
||
* 设置栏区域的左边界和宽度
|
||
* 此函数通过计算给定HTML元素的位置和尺寸来更新栏区域的左边界和宽度
|
||
* @param event HTMLDivElement类型,代表触发事件的HTML元素它用于获取栏区域的位置和宽度信息
|
||
*/
|
||
const setBarArea = (event: HTMLDivElement | null) => {
|
||
if (!event) return;
|
||
// 获取栏区域左边界的坐标
|
||
const newBarAreaLeft = event.getBoundingClientRect().left;
|
||
// 获取栏区域的宽度
|
||
const newBarAreaOffsetWidth = event.offsetWidth;
|
||
// 更新状态
|
||
barAreaRef.current = {
|
||
barAreaLeft: newBarAreaLeft, // 记录栏区域的左边界
|
||
barAreaOffsetWidth: newBarAreaOffsetWidth, // 记录栏区域的宽度
|
||
};
|
||
};
|
||
|
||
const start = (
|
||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||
) => {
|
||
if (isEnd.current) return;
|
||
status.current = true;
|
||
setPressed(true);
|
||
|
||
document.addEventListener(events.move, move as any);
|
||
document.addEventListener(events.end, end);
|
||
document.addEventListener('touchcancel', end);
|
||
e.stopPropagation();
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
title={title}
|
||
className="captcha-modal"
|
||
centered
|
||
open={show}
|
||
maskClosable={false}
|
||
width={imgWidth + 2 * padding}
|
||
styles={{
|
||
content: {
|
||
padding: `16px ${padding}px 10px`,
|
||
userSelect: 'none',
|
||
},
|
||
}}
|
||
footer={null}
|
||
onCancel={closeBox}
|
||
>
|
||
<div className="verifybox">
|
||
{isLoading ? (
|
||
<div
|
||
style={{
|
||
width: imgWidth,
|
||
}}
|
||
>
|
||
<div
|
||
className="verify-img-out"
|
||
style={{ height: imgHeight + vSpace }}
|
||
>
|
||
<Skeleton.Image
|
||
active
|
||
style={{ height: imgHeight, width: imgWidth }}
|
||
/>
|
||
</div>
|
||
<Skeleton.Node
|
||
active
|
||
style={{ height: setSize.barHeight, width: imgWidth }}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<div
|
||
className="verify-img-out"
|
||
style={{ height: imgHeight + vSpace }}
|
||
>
|
||
<div
|
||
className="verify-img-panel"
|
||
style={{
|
||
width: imgWidth,
|
||
height: imgHeight,
|
||
}}
|
||
>
|
||
{response?.originalImageBase64 && (
|
||
<img
|
||
src={
|
||
'data:image/png;base64,' + response?.originalImageBase64
|
||
}
|
||
alt="captcha-image"
|
||
draggable={false}
|
||
className="verify-img"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className="verify-bar-area"
|
||
style={{
|
||
width: imgWidth,
|
||
height: setSize.barHeight,
|
||
}}
|
||
ref={(bar) => setBarArea(bar)}
|
||
>
|
||
<div
|
||
className="verify-msg"
|
||
style={{
|
||
lineHeight: setSize.barHeight + 'px',
|
||
marginLeft: blockWidth + 'px',
|
||
width: imgWidth - blockWidth + 'px',
|
||
display: showTips ? 'block' : 'none',
|
||
}}
|
||
>
|
||
{tips}
|
||
</div>
|
||
<div
|
||
className="verify-left-bar"
|
||
ref={leftBarRef}
|
||
style={{
|
||
width: blockWidth,
|
||
height: setSize.barHeight,
|
||
touchAction: 'pan-y',
|
||
}}
|
||
>
|
||
<div
|
||
className="verify-move-block"
|
||
onMouseDown={start}
|
||
onTouchStart={start}
|
||
style={{
|
||
width: blockWidth,
|
||
backgroundColor: blockPressed ? '#f2f2f2' : '#fff',
|
||
cursor: blockPressed ? 'grab' : 'pointer',
|
||
height: setSize.barHeight - 2,
|
||
}}
|
||
>
|
||
{<AJCaptchaIcon icon={icon} />}
|
||
<div
|
||
className="verify-sub-block"
|
||
style={{
|
||
width: blockWidth,
|
||
height: imgHeight,
|
||
top: `-${imgHeight + vSpace}px`,
|
||
backgroundSize: `${imgWidth} ${imgHeight}`,
|
||
}}
|
||
>
|
||
{response?.jigsawImageBase64 && (
|
||
<img
|
||
src={
|
||
'data:image/png;base64,' + response?.jigsawImageBase64
|
||
}
|
||
alt="blockImage"
|
||
className="verify-img"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="verify-very-bottom">
|
||
<div className="verify-refresh" onClick={refresh}>
|
||
<ReloadOutlined spin={isLoading} />
|
||
<span className="verify-refresh-text">{refreshText}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default AJCaptchaSlider;
|