mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +08:00 
			
		
		
		
	feat: 发送短信适配 ContiNew Starter 行为验证码
This commit is contained in:
		| @@ -52,6 +52,12 @@ | |||||||
|             <artifactId>continew-starter-captcha-graphic</artifactId> |             <artifactId>continew-starter-captcha-graphic</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- ContiNew Starter 验证码模块 - 行为验证码 --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>top.charles7c.continew</groupId> | ||||||
|  |             <artifactId>continew-starter-captcha-behavior</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <!-- ContiNew Starter 文件处理模块 - Excel --> |         <!-- ContiNew Starter 文件处理模块 - Excel --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>top.charles7c.continew</groupId> |             <groupId>top.charles7c.continew</groupId> | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import lombok.RequiredArgsConstructor; | |||||||
| /** | /** | ||||||
|  * 消息模板枚举 |  * 消息模板枚举 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:51 |  * @since 2023/10/15 19:51 | ||||||
|  */ |  */ | ||||||
| @Getter | @Getter | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import top.charles7c.continew.starter.data.mybatis.plus.base.BaseMapper; | |||||||
| /** | /** | ||||||
|  * 消息 Mapper |  * 消息 Mapper | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| public interface MessageMapper extends BaseMapper<MessageDO> { | public interface MessageMapper extends BaseMapper<MessageDO> { | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ import top.charles7c.continew.starter.data.mybatis.plus.base.BaseMapper; | |||||||
| /** | /** | ||||||
|  * 消息和用户 Mapper |  * 消息和用户 Mapper | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 20:25 |  * @since 2023/10/15 20:25 | ||||||
|  */ |  */ | ||||||
| public interface MessageUserMapper extends BaseMapper<MessageUserDO> { | public interface MessageUserMapper extends BaseMapper<MessageUserDO> { | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ import top.charles7c.continew.admin.common.enums.MessageTypeEnum; | |||||||
| /** | /** | ||||||
|  * 消息实体 |  * 消息实体 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ import com.baomidou.mybatisplus.annotation.TableName; | |||||||
| /** | /** | ||||||
|  * 消息和用户关联实体 |  * 消息和用户关联实体 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 20:25 |  * @since 2023/10/15 20:25 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ import top.charles7c.continew.starter.data.mybatis.plus.query.QueryType; | |||||||
| /** | /** | ||||||
|  * 消息查询条件 |  * 消息查询条件 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ import top.charles7c.continew.starter.extension.crud.base.BaseReq; | |||||||
| /** | /** | ||||||
|  * 创建消息信息 |  * 创建消息信息 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ import top.charles7c.continew.admin.common.enums.MessageTypeEnum; | |||||||
| /** | /** | ||||||
|  * 消息信息 |  * 消息信息 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp; | |||||||
| /** | /** | ||||||
|  * 消息业务接口 |  * 消息业务接口 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| public interface MessageService { | public interface MessageService { | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ import top.charles7c.continew.admin.system.model.resp.MessageUnreadResp; | |||||||
| /** | /** | ||||||
|  * 消息和用户关联业务接口 |  * 消息和用户关联业务接口 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| public interface MessageUserService { | public interface MessageUserService { | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ import top.charles7c.continew.starter.extension.crud.model.resp.PageDataResp; | |||||||
| /** | /** | ||||||
|  * 消息业务实现 |  * 消息业务实现 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Service | @Service | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ import top.charles7c.continew.starter.core.util.validate.CheckUtils; | |||||||
| /** | /** | ||||||
|  * 消息和用户关联业务实现 |  * 消息和用户关联业务实现 | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Service | @Service | ||||||
|   | |||||||
| @@ -1,11 +1,35 @@ | |||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
|  | import qs from 'query-string'; | ||||||
|  |  | ||||||
| const BASE_URL = '/common/captcha'; | const BASE_URL = '/captcha'; | ||||||
|  |  | ||||||
| export interface ImageCaptchaRes { | export interface ImageCaptchaRes { | ||||||
|   uuid: string; |   uuid: string; | ||||||
|   img: string; |   img: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface BehaviorCaptchaRes { | ||||||
|  |   originalImageBase64: string; | ||||||
|  |   point: { | ||||||
|  |     x: number; | ||||||
|  |     y: number; | ||||||
|  |   }; | ||||||
|  |   jigsawImageBase64: string; | ||||||
|  |   token: string; | ||||||
|  |   secretKey: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface BehaviorCaptchaReq { | ||||||
|  |   captchaType?: string; | ||||||
|  |   captchaVerification?: string; | ||||||
|  |   clientUid?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface CheckBehaviorCaptchaRes { | ||||||
|  |   repCode: string; | ||||||
|  |   repMsg: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export function getImageCaptcha() { | export function getImageCaptcha() { | ||||||
|   return axios.get<ImageCaptchaRes>(`${BASE_URL}/img`); |   return axios.get<ImageCaptchaRes>(`${BASE_URL}/img`); | ||||||
| } | } | ||||||
| @@ -14,6 +38,26 @@ export function getMailCaptcha(email: string) { | |||||||
|   return axios.get(`${BASE_URL}/mail?email=${email}`); |   return axios.get(`${BASE_URL}/mail?email=${email}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getSmsCaptcha(phone: string) { | export function getSmsCaptcha( | ||||||
|   return axios.get(`${BASE_URL}/sms?phone=${phone}`); |   phone: string, | ||||||
|  |   behaviorCaptcha: BehaviorCaptchaReq, | ||||||
|  | ) { | ||||||
|  |   return axios.get( | ||||||
|  |     `${BASE_URL}/sms?phone=${phone}&captchaVerification=${encodeURIComponent( | ||||||
|  |       behaviorCaptcha.captchaVerification || '', | ||||||
|  |     )}`, | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getBehaviorCaptcha(params: any) { | ||||||
|  |   return axios.get<BehaviorCaptchaRes>(`${BASE_URL}/behavior`, { | ||||||
|  |     params, | ||||||
|  |     paramsSerializer: (obj) => { | ||||||
|  |       return qs.stringify(obj); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function checkBehaviorCaptcha(params: any) { | ||||||
|  |   return axios.post<CheckBehaviorCaptchaRes>(`${BASE_URL}/behavior`, params); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import RightToolbar from './right-toolbar/index.vue'; | |||||||
| import SvgIcon from './svg-icon/index.vue'; | import SvgIcon from './svg-icon/index.vue'; | ||||||
| import IconSelect from './icon-select/index.vue'; | import IconSelect from './icon-select/index.vue'; | ||||||
| import download from './crud'; | import download from './crud'; | ||||||
|  | import Verify from './verifition/Verify.vue'; | ||||||
|  |  | ||||||
| // Manually introduce ECharts modules to reduce packing size | // Manually introduce ECharts modules to reduce packing size | ||||||
|  |  | ||||||
| @@ -46,5 +47,6 @@ export default { | |||||||
|     Vue.component('RightToolbar', RightToolbar); |     Vue.component('RightToolbar', RightToolbar); | ||||||
|     Vue.component('SvgIcon', SvgIcon); |     Vue.component('SvgIcon', SvgIcon); | ||||||
|     Vue.component('IconSelect', IconSelect); |     Vue.component('IconSelect', IconSelect); | ||||||
|  |     Vue.component('Verify', Verify); | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										431
									
								
								continew-admin-ui/src/components/verifition/Verify.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										431
									
								
								continew-admin-ui/src/components/verifition/Verify.vue
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1,296 @@ | |||||||
|  | <template> | ||||||
|  |   <div style="position: relative"> | ||||||
|  |     <div class="verify-img-out"> | ||||||
|  |       <div | ||||||
|  |         class="verify-img-panel" | ||||||
|  |         :style="{ | ||||||
|  |           'width': setSize.imgWidth, | ||||||
|  |           'height': setSize.imgHeight, | ||||||
|  |           'background-size': setSize.imgWidth + ' ' + setSize.imgHeight, | ||||||
|  |           'margin-bottom': vSpace + 'px', | ||||||
|  |         }" | ||||||
|  |       > | ||||||
|  |         <div | ||||||
|  |           v-show="showRefresh" | ||||||
|  |           class="verify-refresh" | ||||||
|  |           style="z-index: 3" | ||||||
|  |           @click="refresh" | ||||||
|  |         > | ||||||
|  |           <i class="iconfont icon-refresh"></i> | ||||||
|  |         </div> | ||||||
|  |         <img | ||||||
|  |           ref="canvas" | ||||||
|  |           :src="'data:image/png;base64,' + pointBackImgBase" | ||||||
|  |           alt="" | ||||||
|  |           style="width: 100%; height: 100%; display: block" | ||||||
|  |           @click="bindingClick ? canvasClick($event) : undefined" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <div | ||||||
|  |           v-for="(tempPoint, index) in tempPoints" | ||||||
|  |           :key="index" | ||||||
|  |           class="point-area" | ||||||
|  |           :style="{ | ||||||
|  |             'background-color': '#1abd6c', | ||||||
|  |             'color': '#fff', | ||||||
|  |             'z-index': 9999, | ||||||
|  |             'width': '20px', | ||||||
|  |             'height': '20px', | ||||||
|  |             'text-align': 'center', | ||||||
|  |             'line-height': '20px', | ||||||
|  |             'border-radius': '50%', | ||||||
|  |             'position': 'absolute', | ||||||
|  |             'top': parseInt(tempPoint.y - 10) + 'px', | ||||||
|  |             'left': parseInt(tempPoint.x - 10) + 'px', | ||||||
|  |           }" | ||||||
|  |         > | ||||||
|  |           {{ index + 1 }} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div | ||||||
|  |       class="verify-bar-area" | ||||||
|  |       :style="{ | ||||||
|  |         'width': setSize.imgWidth, | ||||||
|  |         'color': barAreaColor, | ||||||
|  |         'border-color': barAreaBorderColor, | ||||||
|  |         'line-height': barSize.height, | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <span class="verify-msg">{{ text }}</span> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script type="text/babel"> | ||||||
|  |   import { | ||||||
|  |     checkBehaviorCaptcha, | ||||||
|  |     getBehaviorCaptcha, | ||||||
|  |   } from '@/api/common/captcha'; | ||||||
|  |   import { | ||||||
|  |     getCurrentInstance, | ||||||
|  |     nextTick, | ||||||
|  |     onMounted, | ||||||
|  |     reactive, | ||||||
|  |     ref, | ||||||
|  |     toRefs, | ||||||
|  |   } from 'vue'; | ||||||
|  |   import { resetSize } from '../utils/util'; | ||||||
|  |   import { aesEncrypt } from '../utils/ase'; | ||||||
|  |  | ||||||
|  |   export default { | ||||||
|  |     name: 'VerifyPoints', | ||||||
|  |     props: { | ||||||
|  |       // 弹出式pop,固定fixed | ||||||
|  |       mode: { | ||||||
|  |         type: String, | ||||||
|  |         default: '', | ||||||
|  |       }, | ||||||
|  |       captchaType: { | ||||||
|  |         type: String, | ||||||
|  |       }, | ||||||
|  |       // 间隔 | ||||||
|  |       vSpace: { | ||||||
|  |         type: Number, | ||||||
|  |         default: 5, | ||||||
|  |       }, | ||||||
|  |       imgSize: { | ||||||
|  |         type: Object, | ||||||
|  |         default() { | ||||||
|  |           return { | ||||||
|  |             width: '310px', | ||||||
|  |             height: '155px', | ||||||
|  |           }; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       barSize: { | ||||||
|  |         type: Object, | ||||||
|  |         default() { | ||||||
|  |           return { | ||||||
|  |             width: '310px', | ||||||
|  |             height: '40px', | ||||||
|  |           }; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     setup(props) { | ||||||
|  |       const { mode, captchaType } = toRefs(props); | ||||||
|  |       const { proxy } = getCurrentInstance(); | ||||||
|  |       const secretKey = ref(''); // 后端返回的ase加密秘钥 | ||||||
|  |       const checkNum = ref(3); // 默认需要点击的字数 | ||||||
|  |       const fontPos = reactive([]); // 选中的坐标信息 | ||||||
|  |       const checkPosArr = reactive([]); // 用户点击的坐标 | ||||||
|  |       const num = ref(1); // 点击的记数 | ||||||
|  |       const pointBackImgBase = ref(''); // 后端获取到的背景图片 | ||||||
|  |       const poinTextList = reactive([]); // 后端返回的点击字体顺序 | ||||||
|  |       const backToken = ref(''); // 后端返回的token值 | ||||||
|  |       const setSize = reactive({ | ||||||
|  |         imgHeight: 0, | ||||||
|  |         imgWidth: 0, | ||||||
|  |         barHeight: 0, | ||||||
|  |         barWidth: 0, | ||||||
|  |       }); | ||||||
|  |       const tempPoints = reactive([]); | ||||||
|  |       const text = ref(''); | ||||||
|  |       const barAreaColor = ref(undefined); | ||||||
|  |       const barAreaBorderColor = ref(undefined); | ||||||
|  |       const showRefresh = ref(true); | ||||||
|  |       const bindingClick = ref(true); | ||||||
|  |  | ||||||
|  |       // 请求背景图片和验证图片 | ||||||
|  |       function getPictrue() { | ||||||
|  |         const data = { | ||||||
|  |           captchaType: captchaType.value, | ||||||
|  |         }; | ||||||
|  |         getBehaviorCaptcha(data).then((res) => { | ||||||
|  |           pointBackImgBase.value = res.data.originalImageBase64; | ||||||
|  |           backToken.value = res.data.token; | ||||||
|  |           secretKey.value = res.data.secretKey; | ||||||
|  |           poinTextList.value = res.data.wordList; | ||||||
|  |           text.value = `请依次点击【${poinTextList.value.join(',')}】`; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 获取坐标 | ||||||
|  |       const getMousePos = function (obj, e) { | ||||||
|  |         const x = e.offsetX; | ||||||
|  |         const y = e.offsetY; | ||||||
|  |         return { x, y }; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // 创建坐标点 | ||||||
|  |       const createPoint = function (pos) { | ||||||
|  |         tempPoints.push({ ...pos }); | ||||||
|  |         return num.value + 1; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // 坐标转换函数 | ||||||
|  |       const pointTransfrom = function (pointArr, imgSize) { | ||||||
|  |         return pointArr.map((p) => { | ||||||
|  |           const x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth, 10)); | ||||||
|  |           const y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight, 10)); | ||||||
|  |           return { x, y }; | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const init = () => { | ||||||
|  |         // 加载页面 | ||||||
|  |         fontPos.splice(0, fontPos.length); | ||||||
|  |         checkPosArr.splice(0, checkPosArr.length); | ||||||
|  |         num.value = 1; | ||||||
|  |         getPictrue(); | ||||||
|  |         nextTick(() => { | ||||||
|  |           const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy); | ||||||
|  |           setSize.imgHeight = imgHeight; | ||||||
|  |           setSize.imgWidth = imgWidth; | ||||||
|  |           setSize.barHeight = barHeight; | ||||||
|  |           setSize.barWidth = barWidth; | ||||||
|  |           proxy.$parent.$emit('ready', proxy); | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |       onMounted(() => { | ||||||
|  |         // 禁止拖拽 | ||||||
|  |         init(); | ||||||
|  |         proxy.$el.onselectstart = function () { | ||||||
|  |           return false; | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const refresh = function () { | ||||||
|  |         tempPoints.splice(0, tempPoints.length); | ||||||
|  |         barAreaColor.value = '#000'; | ||||||
|  |         barAreaBorderColor.value = '#ddd'; | ||||||
|  |         bindingClick.value = true; | ||||||
|  |         fontPos.splice(0, fontPos.length); | ||||||
|  |         checkPosArr.splice(0, checkPosArr.length); | ||||||
|  |         num.value = 1; | ||||||
|  |         getPictrue(); | ||||||
|  |         text.value = '验证失败'; | ||||||
|  |         showRefresh.value = true; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const canvas = ref(null); | ||||||
|  |       const canvasClick = (e) => { | ||||||
|  |         checkPosArr.push(getMousePos(canvas, e)); | ||||||
|  |         if (num.value === checkNum.value) { | ||||||
|  |           num.value = createPoint(getMousePos(canvas, e)); | ||||||
|  |           // 按比例转换坐标值 | ||||||
|  |           const arr = pointTransfrom(checkPosArr, setSize); | ||||||
|  |           checkPosArr.length = 0; | ||||||
|  |           checkPosArr.push(...arr); | ||||||
|  |           // 等创建坐标执行完 | ||||||
|  |           setTimeout(() => { | ||||||
|  |             // var flag = this.comparePos(this.fontPos, this.checkPosArr); | ||||||
|  |             // 发送后端请求 | ||||||
|  |             const captchaVerification = secretKey.value | ||||||
|  |               ? aesEncrypt( | ||||||
|  |                   `${backToken.value}---${JSON.stringify(checkPosArr)}`, | ||||||
|  |                   secretKey.value, | ||||||
|  |                 ) | ||||||
|  |               : `${backToken.value}---${JSON.stringify(checkPosArr)}`; | ||||||
|  |             const data = { | ||||||
|  |               captchaType: captchaType.value, | ||||||
|  |               pointJson: secretKey.value | ||||||
|  |                 ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value) | ||||||
|  |                 : JSON.stringify(checkPosArr), | ||||||
|  |               token: backToken.value, | ||||||
|  |             }; | ||||||
|  |             checkBehaviorCaptcha(data).then((res) => { | ||||||
|  |               if (res.success && res.data.repCode === '0000') { | ||||||
|  |                 barAreaColor.value = '#4cae4c'; | ||||||
|  |                 barAreaBorderColor.value = '#5cb85c'; | ||||||
|  |                 text.value = '验证成功'; | ||||||
|  |                 bindingClick.value = false; | ||||||
|  |                 if (mode.value === 'pop') { | ||||||
|  |                   setTimeout(() => { | ||||||
|  |                     proxy.$parent.clickShow = false; | ||||||
|  |                     refresh(); | ||||||
|  |                   }, 1500); | ||||||
|  |                 } | ||||||
|  |                 proxy.$parent.$emit('success', { captchaVerification }); | ||||||
|  |               } else { | ||||||
|  |                 proxy.$parent.$emit('error', proxy); | ||||||
|  |                 barAreaColor.value = '#d9534f'; | ||||||
|  |                 barAreaBorderColor.value = '#d9534f'; | ||||||
|  |                 text.value = res.data.repMsg; | ||||||
|  |                 setTimeout(() => { | ||||||
|  |                   refresh(); | ||||||
|  |                 }, 700); | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |           }, 400); | ||||||
|  |         } | ||||||
|  |         if (num.value < checkNum.value) { | ||||||
|  |           num.value = createPoint(getMousePos(canvas, e)); | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         secretKey, | ||||||
|  |         checkNum, | ||||||
|  |         fontPos, | ||||||
|  |         checkPosArr, | ||||||
|  |         num, | ||||||
|  |         pointBackImgBase, | ||||||
|  |         poinTextList, | ||||||
|  |         backToken, | ||||||
|  |         setSize, | ||||||
|  |         tempPoints, | ||||||
|  |         text, | ||||||
|  |         barAreaColor, | ||||||
|  |         barAreaBorderColor, | ||||||
|  |         showRefresh, | ||||||
|  |         bindingClick, | ||||||
|  |         init, | ||||||
|  |         canvas, | ||||||
|  |         canvasClick, | ||||||
|  |         getMousePos, | ||||||
|  |         createPoint, | ||||||
|  |         refresh, | ||||||
|  |         getPictrue, | ||||||
|  |         pointTransfrom, | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
| @@ -0,0 +1,462 @@ | |||||||
|  | <template> | ||||||
|  |   <div style="position: relative"> | ||||||
|  |     <div | ||||||
|  |       v-if="type === '2'" | ||||||
|  |       class="verify-img-out" | ||||||
|  |       :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }" | ||||||
|  |     > | ||||||
|  |       <div | ||||||
|  |         class="verify-img-panel" | ||||||
|  |         :style="{ width: setSize.imgWidth, height: setSize.imgHeight }" | ||||||
|  |       > | ||||||
|  |         <img | ||||||
|  |           :src="'data:image/png;base64,' + backImgBase" | ||||||
|  |           alt="" | ||||||
|  |           style="width: 100%; height: 100%; display: block" | ||||||
|  |         /> | ||||||
|  |         <div v-show="showRefresh" class="verify-refresh" @click="refresh" | ||||||
|  |           ><i class="iconfont icon-refresh"></i | ||||||
|  |         ></div> | ||||||
|  |         <transition name="tips"> | ||||||
|  |           <span | ||||||
|  |             v-if="tipWords" | ||||||
|  |             class="verify-tips" | ||||||
|  |             :class="passFlag ? 'suc-bg' : 'err-bg'" | ||||||
|  |             >{{ tipWords }}</span | ||||||
|  |           > | ||||||
|  |         </transition> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <!-- 公共部分 --> | ||||||
|  |     <div | ||||||
|  |       class="verify-bar-area" | ||||||
|  |       :style="{ | ||||||
|  |         'width': setSize.imgWidth, | ||||||
|  |         'height': barSize.height, | ||||||
|  |         'line-height': barSize.height, | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <span class="verify-msg" v-text="text"></span> | ||||||
|  |       <div | ||||||
|  |         class="verify-left-bar" | ||||||
|  |         :style="{ | ||||||
|  |           'width': leftBarWidth !== undefined ? leftBarWidth : barSize.height, | ||||||
|  |           'height': barSize.height, | ||||||
|  |           'border-color': leftBarBorderColor, | ||||||
|  |           'transaction': transitionWidth, | ||||||
|  |         }" | ||||||
|  |       > | ||||||
|  |         <span class="verify-msg" v-text="finishText"></span> | ||||||
|  |         <div | ||||||
|  |           class="verify-move-block" | ||||||
|  |           :style="{ | ||||||
|  |             'width': barSize.height, | ||||||
|  |             'height': barSize.height, | ||||||
|  |             'background-color': moveBlockBackgroundColor, | ||||||
|  |             'left': moveBlockLeft, | ||||||
|  |             'transition': transitionLeft, | ||||||
|  |           }" | ||||||
|  |           @touchstart="start" | ||||||
|  |           @mousedown="start" | ||||||
|  |         > | ||||||
|  |           <i | ||||||
|  |             :class="['verify-icon iconfont', iconClass]" | ||||||
|  |             :style="{ color: iconColor }" | ||||||
|  |           ></i> | ||||||
|  |           <div | ||||||
|  |             v-if="type === '2'" | ||||||
|  |             class="verify-sub-block" | ||||||
|  |             :style="{ | ||||||
|  |               'width': | ||||||
|  |                 Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px', | ||||||
|  |               'height': setSize.imgHeight, | ||||||
|  |               'top': '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px', | ||||||
|  |               'background-size': setSize.imgWidth + ' ' + setSize.imgHeight, | ||||||
|  |             }" | ||||||
|  |           > | ||||||
|  |             <img | ||||||
|  |               :src="'data:image/png;base64,' + blockBackImgBase" | ||||||
|  |               alt="" | ||||||
|  |               style=" | ||||||
|  |                 width: 100%; | ||||||
|  |                 height: 100%; | ||||||
|  |                 display: block; | ||||||
|  |                 -webkit-user-drag: none; | ||||||
|  |               " | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script type="text/babel"> | ||||||
|  |   import { | ||||||
|  |     computed, | ||||||
|  |     onMounted, | ||||||
|  |     reactive, | ||||||
|  |     ref, | ||||||
|  |     watch, | ||||||
|  |     nextTick, | ||||||
|  |     toRefs, | ||||||
|  |     getCurrentInstance, | ||||||
|  |   } from 'vue'; | ||||||
|  |   import { | ||||||
|  |     checkBehaviorCaptcha, | ||||||
|  |     getBehaviorCaptcha, | ||||||
|  |   } from '@/api/common/captcha'; | ||||||
|  |   import { aesEncrypt } from '../utils/ase'; | ||||||
|  |   import { resetSize } from '../utils/util'; | ||||||
|  |  | ||||||
|  |   export default { | ||||||
|  |     name: 'VerifySlide', | ||||||
|  |     props: { | ||||||
|  |       captchaType: { | ||||||
|  |         type: String, | ||||||
|  |       }, | ||||||
|  |       type: { | ||||||
|  |         type: String, | ||||||
|  |         default: '1', | ||||||
|  |       }, | ||||||
|  |       // 弹出式pop,固定fixed | ||||||
|  |       mode: { | ||||||
|  |         type: String, | ||||||
|  |         default: 'fixed', | ||||||
|  |       }, | ||||||
|  |       vSpace: { | ||||||
|  |         type: Number, | ||||||
|  |         default: 5, | ||||||
|  |       }, | ||||||
|  |       explain: { | ||||||
|  |         type: String, | ||||||
|  |         default: '向右滑动完成验证', | ||||||
|  |       }, | ||||||
|  |       imgSize: { | ||||||
|  |         type: Object, | ||||||
|  |         default() { | ||||||
|  |           return { | ||||||
|  |             width: '310px', | ||||||
|  |             height: '155px', | ||||||
|  |           }; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       blockSize: { | ||||||
|  |         type: Object, | ||||||
|  |         default() { | ||||||
|  |           return { | ||||||
|  |             width: '50px', | ||||||
|  |             height: '50px', | ||||||
|  |           }; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       barSize: { | ||||||
|  |         type: Object, | ||||||
|  |         default() { | ||||||
|  |           return { | ||||||
|  |             width: '310px', | ||||||
|  |             height: '40px', | ||||||
|  |           }; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     setup(props) { | ||||||
|  |       const { mode, captchaType, type, blockSize, explain } = toRefs(props); | ||||||
|  |       const { proxy } = getCurrentInstance(); | ||||||
|  |       const secretKey = ref(''); // 后端返回的ase加密秘钥 | ||||||
|  |       const passFlag = ref(''); // 是否通过的标识 | ||||||
|  |       const backImgBase = ref(''); // 验证码背景图片 | ||||||
|  |       const blockBackImgBase = ref(''); // 验证滑块的背景图片 | ||||||
|  |       const backToken = ref(''); // 后端返回的唯一token值 | ||||||
|  |       const startMoveTime = ref(''); // 移动开始的时间 | ||||||
|  |       const endMovetime = ref(''); // 移动结束的时间 | ||||||
|  |       const tipsBackColor = ref(''); // 提示词的背景颜色 | ||||||
|  |       const tipWords = ref(''); | ||||||
|  |       const text = ref(''); | ||||||
|  |       const finishText = ref(''); | ||||||
|  |       const setSize = reactive({ | ||||||
|  |         imgHeight: 0, | ||||||
|  |         imgWidth: 0, | ||||||
|  |         barHeight: 0, | ||||||
|  |         barWidth: 0, | ||||||
|  |       }); | ||||||
|  |       const top = ref(0); | ||||||
|  |       const left = ref(0); | ||||||
|  |       const moveBlockLeft = ref(undefined); | ||||||
|  |       const leftBarWidth = ref(undefined); | ||||||
|  |       // 移动中样式 | ||||||
|  |       const moveBlockBackgroundColor = ref(undefined); | ||||||
|  |       const leftBarBorderColor = ref('#ddd'); | ||||||
|  |       const iconColor = ref(undefined); | ||||||
|  |       const iconClass = ref('icon-right'); | ||||||
|  |       const status = ref(false); // 鼠标状态 | ||||||
|  |       const isEnd = ref(false); // 是够验证完成 | ||||||
|  |       const showRefresh = ref(true); | ||||||
|  |       const transitionLeft = ref(''); | ||||||
|  |       const transitionWidth = ref(''); | ||||||
|  |       const startLeft = ref(0); | ||||||
|  |  | ||||||
|  |       // 请求背景图片和验证图片 | ||||||
|  |       function getPictrue() { | ||||||
|  |         const data = { | ||||||
|  |           captchaType: captchaType.value, | ||||||
|  |         }; | ||||||
|  |         getBehaviorCaptcha(data).then((res) => { | ||||||
|  |           backImgBase.value = res.data.originalImageBase64; | ||||||
|  |           blockBackImgBase.value = res.data.jigsawImageBase64; | ||||||
|  |           backToken.value = res.data.token; | ||||||
|  |           secretKey.value = res.data.secretKey; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       const barArea = computed(() => { | ||||||
|  |         return proxy.$el.querySelector('.verify-bar-area'); | ||||||
|  |       }); | ||||||
|  |       // 鼠标移动 | ||||||
|  |       function move(e) { | ||||||
|  |         e = e || window.event; | ||||||
|  |         if (status.value && isEnd.value === false) { | ||||||
|  |           let x; | ||||||
|  |           if (!e.touches) { | ||||||
|  |             // 兼容PC端 | ||||||
|  |             x = e.clientX; | ||||||
|  |           } else { | ||||||
|  |             // 兼容移动端 | ||||||
|  |             x = e.touches[0].pageX; | ||||||
|  |           } | ||||||
|  |           const bar_area_left = barArea.value.getBoundingClientRect().left; | ||||||
|  |           let move_block_left = x - bar_area_left; // 小方块相对于父元素的left值 | ||||||
|  |           if ( | ||||||
|  |             move_block_left >= | ||||||
|  |             barArea.value.offsetWidth - | ||||||
|  |               parseInt(parseInt(blockSize.value.width, 10) / 2, 10) - | ||||||
|  |               2 | ||||||
|  |           ) { | ||||||
|  |             move_block_left = | ||||||
|  |               barArea.value.offsetWidth - | ||||||
|  |               parseInt(parseInt(blockSize.value.width, 10) / 2, 10) - | ||||||
|  |               2; | ||||||
|  |           } | ||||||
|  |           if (move_block_left <= 0) { | ||||||
|  |             move_block_left = parseInt( | ||||||
|  |               parseInt(blockSize.value.width, 10) / 2, | ||||||
|  |               10, | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |           // 拖动后小方块的left值 | ||||||
|  |           moveBlockLeft.value = `${move_block_left - startLeft.value}px`; | ||||||
|  |           leftBarWidth.value = `${move_block_left - startLeft.value}px`; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const refresh = () => { | ||||||
|  |         showRefresh.value = true; | ||||||
|  |         finishText.value = ''; | ||||||
|  |  | ||||||
|  |         transitionLeft.value = 'left .3s'; | ||||||
|  |         moveBlockLeft.value = 0; | ||||||
|  |  | ||||||
|  |         leftBarWidth.value = undefined; | ||||||
|  |         transitionWidth.value = 'width .3s'; | ||||||
|  |  | ||||||
|  |         leftBarBorderColor.value = '#ddd'; | ||||||
|  |         moveBlockBackgroundColor.value = '#fff'; | ||||||
|  |         iconColor.value = '#000'; | ||||||
|  |         iconClass.value = 'icon-right'; | ||||||
|  |         isEnd.value = false; | ||||||
|  |  | ||||||
|  |         getPictrue(); | ||||||
|  |         setTimeout(() => { | ||||||
|  |           transitionWidth.value = ''; | ||||||
|  |           transitionLeft.value = ''; | ||||||
|  |           text.value = explain.value; | ||||||
|  |         }, 300); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // 鼠标松开 | ||||||
|  |       function end() { | ||||||
|  |         endMovetime.value = +new Date(); | ||||||
|  |         // 判断是否重合 | ||||||
|  |         if (status.value && isEnd.value === false) { | ||||||
|  |           let moveLeftDistance = parseInt( | ||||||
|  |             (moveBlockLeft.value || '').replace('px', ''), | ||||||
|  |             10, | ||||||
|  |           ); | ||||||
|  |           moveLeftDistance = | ||||||
|  |             (moveLeftDistance * 310) / parseInt(setSize.imgWidth, 10); | ||||||
|  |           const data = { | ||||||
|  |             captchaType: captchaType.value, | ||||||
|  |             pointJson: secretKey.value | ||||||
|  |               ? aesEncrypt( | ||||||
|  |                   JSON.stringify({ x: moveLeftDistance, y: 5.0 }), | ||||||
|  |                   secretKey.value, | ||||||
|  |                 ) | ||||||
|  |               : JSON.stringify({ x: moveLeftDistance, y: 5.0 }), | ||||||
|  |             token: backToken.value, | ||||||
|  |           }; | ||||||
|  |           checkBehaviorCaptcha(data).then((res) => { | ||||||
|  |             if (res.success && res.data.repCode === '0000') { | ||||||
|  |               moveBlockBackgroundColor.value = '#5cb85c'; | ||||||
|  |               leftBarBorderColor.value = '#5cb85c'; | ||||||
|  |               iconColor.value = '#fff'; | ||||||
|  |               iconClass.value = 'icon-check'; | ||||||
|  |               showRefresh.value = false; | ||||||
|  |               isEnd.value = true; | ||||||
|  |               if (mode.value === 'pop') { | ||||||
|  |                 setTimeout(() => { | ||||||
|  |                   proxy.$parent.clickShow = false; | ||||||
|  |                   refresh(); | ||||||
|  |                 }, 1500); | ||||||
|  |               } | ||||||
|  |               passFlag.value = true; | ||||||
|  |               tipWords.value = `${( | ||||||
|  |                 (endMovetime.value - startMoveTime.value) / | ||||||
|  |                 1000 | ||||||
|  |               ).toFixed(2)}s验证成功`; | ||||||
|  |               const captchaVerification = secretKey.value | ||||||
|  |                 ? aesEncrypt( | ||||||
|  |                     `${backToken.value}---${JSON.stringify({ | ||||||
|  |                       x: moveLeftDistance, | ||||||
|  |                       y: 5.0, | ||||||
|  |                     })}`, | ||||||
|  |                     secretKey.value, | ||||||
|  |                   ) | ||||||
|  |                 : `${backToken.value}---${JSON.stringify({ | ||||||
|  |                     x: moveLeftDistance, | ||||||
|  |                     y: 5.0, | ||||||
|  |                   })}`; | ||||||
|  |               setTimeout(() => { | ||||||
|  |                 tipWords.value = ''; | ||||||
|  |                 proxy.$parent.closeBox(); | ||||||
|  |                 proxy.$parent.$emit('success', { captchaVerification }); | ||||||
|  |               }, 1000); | ||||||
|  |             } else { | ||||||
|  |               moveBlockBackgroundColor.value = '#d9534f'; | ||||||
|  |               leftBarBorderColor.value = '#d9534f'; | ||||||
|  |               iconColor.value = '#fff'; | ||||||
|  |               iconClass.value = 'icon-close'; | ||||||
|  |               passFlag.value = false; | ||||||
|  |               setTimeout(function () { | ||||||
|  |                 refresh(); | ||||||
|  |               }, 1000); | ||||||
|  |               proxy.$parent.$emit('error', proxy); | ||||||
|  |               tipWords.value = res.data.repMsg; | ||||||
|  |               setTimeout(() => { | ||||||
|  |                 tipWords.value = ''; | ||||||
|  |               }, 1000); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |           status.value = false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       function init() { | ||||||
|  |         text.value = explain.value; | ||||||
|  |         getPictrue(); | ||||||
|  |         nextTick(() => { | ||||||
|  |           const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy); | ||||||
|  |           setSize.imgHeight = imgHeight; | ||||||
|  |           setSize.imgWidth = imgWidth; | ||||||
|  |           setSize.barHeight = barHeight; | ||||||
|  |           setSize.barWidth = barWidth; | ||||||
|  |           proxy.$parent.$emit('ready', proxy); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         window.removeEventListener('touchmove', function (e) { | ||||||
|  |           move(e); | ||||||
|  |         }); | ||||||
|  |         window.removeEventListener('mousemove', function (e) { | ||||||
|  |           move(e); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // 鼠标松开 | ||||||
|  |         window.removeEventListener('touchend', function () { | ||||||
|  |           end(); | ||||||
|  |         }); | ||||||
|  |         window.removeEventListener('mouseup', function () { | ||||||
|  |           end(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         window.addEventListener('touchmove', function (e) { | ||||||
|  |           move(e); | ||||||
|  |         }); | ||||||
|  |         window.addEventListener('mousemove', function (e) { | ||||||
|  |           move(e); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // 鼠标松开 | ||||||
|  |         window.addEventListener('touchend', function () { | ||||||
|  |           end(); | ||||||
|  |         }); | ||||||
|  |         window.addEventListener('mouseup', function () { | ||||||
|  |           end(); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       watch(type, () => { | ||||||
|  |         init(); | ||||||
|  |       }); | ||||||
|  |       onMounted(() => { | ||||||
|  |         // 禁止拖拽 | ||||||
|  |         init(); | ||||||
|  |         proxy.$el.onselectstart = function () { | ||||||
|  |           return false; | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |       // 鼠标按下 | ||||||
|  |       function start(e) { | ||||||
|  |         e = e || window.event; | ||||||
|  |         let x; | ||||||
|  |         if (!e.touches) { | ||||||
|  |           // 兼容PC端 | ||||||
|  |           x = e.clientX; | ||||||
|  |         } else { | ||||||
|  |           // 兼容移动端 | ||||||
|  |           x = e.touches[0].pageX; | ||||||
|  |         } | ||||||
|  |         startLeft.value = Math.floor( | ||||||
|  |           x - barArea.value.getBoundingClientRect().left, | ||||||
|  |         ); | ||||||
|  |         startMoveTime.value = +new Date(); // 开始滑动的时间 | ||||||
|  |         if (isEnd.value === false) { | ||||||
|  |           text.value = ''; | ||||||
|  |           moveBlockBackgroundColor.value = '#337ab7'; | ||||||
|  |           leftBarBorderColor.value = '#337AB7'; | ||||||
|  |           iconColor.value = '#fff'; | ||||||
|  |           e.stopPropagation(); | ||||||
|  |           status.value = true; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         secretKey, // 后端返回的ase加密秘钥 | ||||||
|  |         passFlag, // 是否通过的标识 | ||||||
|  |         backImgBase, // 验证码背景图片 | ||||||
|  |         blockBackImgBase, // 验证滑块的背景图片 | ||||||
|  |         backToken, // 后端返回的唯一token值 | ||||||
|  |         startMoveTime, // 移动开始的时间 | ||||||
|  |         endMovetime, // 移动结束的时间 | ||||||
|  |         tipsBackColor, // 提示词的背景颜色 | ||||||
|  |         tipWords, | ||||||
|  |         text, | ||||||
|  |         finishText, | ||||||
|  |         setSize, | ||||||
|  |         top, | ||||||
|  |         left, | ||||||
|  |         moveBlockLeft, | ||||||
|  |         leftBarWidth, | ||||||
|  |         // 移动中样式 | ||||||
|  |         moveBlockBackgroundColor, | ||||||
|  |         leftBarBorderColor, | ||||||
|  |         iconColor, | ||||||
|  |         iconClass, | ||||||
|  |         status, // 鼠标状态 | ||||||
|  |         isEnd, // 是够验证完成 | ||||||
|  |         showRefresh, | ||||||
|  |         transitionLeft, | ||||||
|  |         transitionWidth, | ||||||
|  |         barArea, | ||||||
|  |         refresh, | ||||||
|  |         start, | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
							
								
								
									
										14
									
								
								continew-admin-ui/src/components/verifition/utils/ase.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								continew-admin-ui/src/components/verifition/utils/ase.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import CryptoJS from 'crypto-js'; | ||||||
|  | /** | ||||||
|  |  * @word 要加密的内容 | ||||||
|  |  * @keyWord String  服务器随机返回的关键字 | ||||||
|  |  *  */ | ||||||
|  | export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { | ||||||
|  |   const key = CryptoJS.enc.Utf8.parse(keyWord); | ||||||
|  |   const arcs = CryptoJS.enc.Utf8.parse(word); | ||||||
|  |   const encrypted = CryptoJS.AES.encrypt(arcs, key, { | ||||||
|  |     mode: CryptoJS.mode.ECB, | ||||||
|  |     padding: CryptoJS.pad.Pkcs7, | ||||||
|  |   }); | ||||||
|  |   return encrypted.toString(); | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								continew-admin-ui/src/components/verifition/utils/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								continew-admin-ui/src/components/verifition/utils/util.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | export function resetSize(vm) { | ||||||
|  |   let img_width; | ||||||
|  |   let img_height; | ||||||
|  |   let bar_width; | ||||||
|  |   let bar_height; // 图片的宽度、高度,移动条的宽度、高度 | ||||||
|  |  | ||||||
|  |   const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth; | ||||||
|  |   const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight; | ||||||
|  |   if (vm.imgSize.width.indexOf('%') !== -1) { | ||||||
|  |     img_width = `${(parseInt(vm.imgSize.width, 10) / 100) * parentWidth}px`; | ||||||
|  |   } else { | ||||||
|  |     img_width = vm.imgSize.width; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (vm.imgSize.height.indexOf('%') !== -1) { | ||||||
|  |     img_height = `${(parseInt(vm.imgSize.height, 10) / 100) * parentHeight}px`; | ||||||
|  |   } else { | ||||||
|  |     img_height = vm.imgSize.height; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (vm.barSize.width.indexOf('%') !== -1) { | ||||||
|  |     bar_width = `${(parseInt(vm.barSize.width, 10) / 100) * parentWidth}px`; | ||||||
|  |   } else { | ||||||
|  |     bar_width = vm.barSize.width; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (vm.barSize.height.indexOf('%') !== -1) { | ||||||
|  |     bar_height = `${(parseInt(vm.barSize.height, 10) / 100) * parentHeight}px`; | ||||||
|  |   } else { | ||||||
|  |     bar_height = vm.barSize.height; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     imgWidth: img_width, | ||||||
|  |     imgHeight: img_height, | ||||||
|  |     barWidth: bar_width, | ||||||
|  |     barHeight: bar_height, | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -29,7 +29,7 @@ | |||||||
|         class="captcha-btn" |         class="captcha-btn" | ||||||
|         :loading="captchaLoading" |         :loading="captchaLoading" | ||||||
|         :disabled="captchaDisable" |         :disabled="captchaDisable" | ||||||
|         @click="handleSendCaptcha" |         @click="handleOpenBehaviorCaptcha" | ||||||
|       > |       > | ||||||
|         {{ captchaBtnName }} |         {{ captchaBtnName }} | ||||||
|       </a-button> |       </a-button> | ||||||
| @@ -43,6 +43,13 @@ | |||||||
|       >{{ $t('login.button') }}(演示不开放) |       >{{ $t('login.button') }}(演示不开放) | ||||||
|     </a-button> |     </a-button> | ||||||
|   </a-form> |   </a-form> | ||||||
|  |   <Verify | ||||||
|  |     ref="verifyRef" | ||||||
|  |     :mode="captchaMode" | ||||||
|  |     :captcha-type="captchaType" | ||||||
|  |     :img-size="{ width: '330px', height: '155px' }" | ||||||
|  |     @success="handleSendCaptcha" | ||||||
|  |   ></Verify> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| @@ -52,7 +59,7 @@ | |||||||
|   import { ValidatedError } from '@arco-design/web-vue'; |   import { ValidatedError } from '@arco-design/web-vue'; | ||||||
|   import { useUserStore } from '@/store'; |   import { useUserStore } from '@/store'; | ||||||
|   import { PhoneLoginReq } from '@/api/auth'; |   import { PhoneLoginReq } from '@/api/auth'; | ||||||
|   import { getSmsCaptcha } from '@/api/common/captcha'; |   import { BehaviorCaptchaReq, getSmsCaptcha } from '@/api/common/captcha'; | ||||||
|  |  | ||||||
|   const { proxy } = getCurrentInstance() as any; |   const { proxy } = getCurrentInstance() as any; | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
| @@ -63,6 +70,8 @@ | |||||||
|   const captchaDisable = ref(true); |   const captchaDisable = ref(true); | ||||||
|   const captchaTime = ref(60); |   const captchaTime = ref(60); | ||||||
|   const captchaTimer = ref(); |   const captchaTimer = ref(); | ||||||
|  |   const captchaType = ref('blockPuzzle'); | ||||||
|  |   const captchaMode = ref('pop'); | ||||||
|   const captchaBtnNameKey = ref('login.captcha.get'); |   const captchaBtnNameKey = ref('login.captcha.get'); | ||||||
|   const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); |   const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); | ||||||
|   const data = reactive({ |   const data = reactive({ | ||||||
| @@ -85,6 +94,18 @@ | |||||||
|   }); |   }); | ||||||
|   const { form, rules } = toRefs(data); |   const { form, rules } = toRefs(data); | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 弹出行为验证码 | ||||||
|  |    */ | ||||||
|  |   const handleOpenBehaviorCaptcha = () => { | ||||||
|  |     if (captchaLoading.value) return; | ||||||
|  |     proxy.$refs.formRef.validateField('phone', (valid: any) => { | ||||||
|  |       if (!valid) { | ||||||
|  |         proxy.$refs.verifyRef.show(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * 重置验证码 |    * 重置验证码 | ||||||
|    */ |    */ | ||||||
| @@ -98,13 +119,13 @@ | |||||||
|   /** |   /** | ||||||
|    * 发送验证码 |    * 发送验证码 | ||||||
|    */ |    */ | ||||||
|   const handleSendCaptcha = () => { |   const handleSendCaptcha = (captchaParam: BehaviorCaptchaReq) => { | ||||||
|     if (captchaLoading.value) return; |     if (captchaLoading.value) return; | ||||||
|     proxy.$refs.formRef.validateField('phone', (valid: any) => { |     proxy.$refs.formRef.validateField('phone', (valid: any) => { | ||||||
|       if (!valid) { |       if (!valid) { | ||||||
|         captchaLoading.value = true; |         captchaLoading.value = true; | ||||||
|         captchaBtnNameKey.value = 'login.captcha.ing'; |         captchaBtnNameKey.value = 'login.captcha.ing'; | ||||||
|         getSmsCaptcha(form.value.phone) |         getSmsCaptcha(form.value.phone, captchaParam) | ||||||
|           .then((res) => { |           .then((res) => { | ||||||
|             captchaLoading.value = false; |             captchaLoading.value = false; | ||||||
|             captchaDisable.value = true; |             captchaDisable.value = true; | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ | |||||||
|           v-model="form.newPhone" |           v-model="form.newPhone" | ||||||
|           :placeholder=" |           :placeholder=" | ||||||
|             $t( |             $t( | ||||||
|               'userCenter.securitySettings.updatePhone.form.placeholder.newPhone' |               'userCenter.securitySettings.updatePhone.form.placeholder.newPhone', | ||||||
|             ) |             ) | ||||||
|           " |           " | ||||||
|           allow-clear |           allow-clear | ||||||
| @@ -73,7 +73,7 @@ | |||||||
|           type="primary" |           type="primary" | ||||||
|           :disabled="captchaDisable" |           :disabled="captchaDisable" | ||||||
|           class="captcha-btn" |           class="captcha-btn" | ||||||
|           @click="handleSendCaptcha" |           @click="handleOpenBehaviorCaptcha" | ||||||
|         > |         > | ||||||
|           {{ captchaBtnName }} |           {{ captchaBtnName }} | ||||||
|         </a-button> |         </a-button> | ||||||
| @@ -81,7 +81,7 @@ | |||||||
|       <a-form-item |       <a-form-item | ||||||
|         :label=" |         :label=" | ||||||
|           $t( |           $t( | ||||||
|             'userCenter.securitySettings.updatePhone.form.label.currentPassword' |             'userCenter.securitySettings.updatePhone.form.label.currentPassword', | ||||||
|           ) |           ) | ||||||
|         " |         " | ||||||
|         field="currentPassword" |         field="currentPassword" | ||||||
| @@ -90,7 +90,7 @@ | |||||||
|           v-model="form.currentPassword" |           v-model="form.currentPassword" | ||||||
|           :placeholder=" |           :placeholder=" | ||||||
|             $t( |             $t( | ||||||
|               'userCenter.securitySettings.updatePhone.form.placeholder.currentPassword' |               'userCenter.securitySettings.updatePhone.form.placeholder.currentPassword', | ||||||
|             ) |             ) | ||||||
|           " |           " | ||||||
|           :max-length="32" |           :max-length="32" | ||||||
| @@ -98,13 +98,20 @@ | |||||||
|         /> |         /> | ||||||
|       </a-form-item> |       </a-form-item> | ||||||
|     </a-form> |     </a-form> | ||||||
|  |     <Verify | ||||||
|  |       ref="verifyRef" | ||||||
|  |       :mode="captchaMode" | ||||||
|  |       :captcha-type="captchaType" | ||||||
|  |       :img-size="{ width: '330px', height: '155px' }" | ||||||
|  |       @success="handleSendCaptcha" | ||||||
|  |     ></Verify> | ||||||
|   </a-modal> |   </a-modal> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|   import { getCurrentInstance, ref, reactive, computed } from 'vue'; |   import { getCurrentInstance, ref, reactive, computed } from 'vue'; | ||||||
|   import { FieldRule } from '@arco-design/web-vue'; |   import { FieldRule } from '@arco-design/web-vue'; | ||||||
|   import { getSmsCaptcha } from '@/api/common/captcha'; |   import { BehaviorCaptchaReq, getSmsCaptcha } from '@/api/common/captcha'; | ||||||
|   import { UserPhoneUpdateReq, updatePhone } from '@/api/system/user-center'; |   import { UserPhoneUpdateReq, updatePhone } from '@/api/system/user-center'; | ||||||
|   import { useI18n } from 'vue-i18n'; |   import { useI18n } from 'vue-i18n'; | ||||||
|   import { useUserStore } from '@/store'; |   import { useUserStore } from '@/store'; | ||||||
| @@ -117,6 +124,8 @@ | |||||||
|   const captchaTimer = ref(); |   const captchaTimer = ref(); | ||||||
|   const captchaLoading = ref(false); |   const captchaLoading = ref(false); | ||||||
|   const captchaDisable = ref(true); |   const captchaDisable = ref(true); | ||||||
|  |   const captchaType = ref('blockPuzzle'); | ||||||
|  |   const captchaMode = ref('pop'); | ||||||
|   const visible = ref(false); |   const visible = ref(false); | ||||||
|   const captchaBtnNameKey = ref('userCenter.securitySettings.captcha.get'); |   const captchaBtnNameKey = ref('userCenter.securitySettings.captcha.get'); | ||||||
|   const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); |   const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); | ||||||
| @@ -134,13 +143,13 @@ | |||||||
|         { |         { | ||||||
|           required: true, |           required: true, | ||||||
|           message: t( |           message: t( | ||||||
|             'userCenter.securitySettings.updatePhone.form.error.required.newPhone' |             'userCenter.securitySettings.updatePhone.form.error.required.newPhone', | ||||||
|           ), |           ), | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           match: /^1[3-9]\d{9}$/, |           match: /^1[3-9]\d{9}$/, | ||||||
|           message: t( |           message: t( | ||||||
|             'userCenter.securitySettings.updatePhone.form.error.match.newPhone' |             'userCenter.securitySettings.updatePhone.form.error.match.newPhone', | ||||||
|           ), |           ), | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
| @@ -154,7 +163,7 @@ | |||||||
|         { |         { | ||||||
|           required: true, |           required: true, | ||||||
|           message: t( |           message: t( | ||||||
|             'userCenter.securitySettings.updatePhone.form.error.required.currentPassword' |             'userCenter.securitySettings.updatePhone.form.error.required.currentPassword', | ||||||
|           ), |           ), | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
| @@ -171,26 +180,38 @@ | |||||||
|     captchaDisable.value = false; |     captchaDisable.value = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 弹出行为验证码 | ||||||
|  |    */ | ||||||
|  |   const handleOpenBehaviorCaptcha = () => { | ||||||
|  |     if (captchaLoading.value) return; | ||||||
|  |     proxy.$refs.formRef.validateField('newPhone', (valid: any) => { | ||||||
|  |       if (!valid) { | ||||||
|  |         proxy.$refs.verifyRef.show(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * 发送验证码 |    * 发送验证码 | ||||||
|    */ |    */ | ||||||
|   const handleSendCaptcha = () => { |   const handleSendCaptcha = (captchaParam: BehaviorCaptchaReq) => { | ||||||
|     if (captchaLoading.value) return; |     if (captchaLoading.value) return; | ||||||
|     proxy.$refs.formRef.validateField('newPhone', (valid: any) => { |     proxy.$refs.formRef.validateField('newPhone', (valid: any) => { | ||||||
|       if (!valid) { |       if (!valid) { | ||||||
|         captchaLoading.value = true; |         captchaLoading.value = true; | ||||||
|         captchaBtnNameKey.value = 'userCenter.securitySettings.captcha.ing'; |         captchaBtnNameKey.value = 'userCenter.securitySettings.captcha.ing'; | ||||||
|         getSmsCaptcha(form.newPhone) |         getSmsCaptcha(form.newPhone, captchaParam) | ||||||
|           .then((res) => { |           .then((res) => { | ||||||
|             captchaLoading.value = false; |             captchaLoading.value = false; | ||||||
|             captchaDisable.value = true; |             captchaDisable.value = true; | ||||||
|             captchaBtnNameKey.value = `${t( |             captchaBtnNameKey.value = `${t( | ||||||
|               'userCenter.securitySettings.captcha.get' |               'userCenter.securitySettings.captcha.get', | ||||||
|             )}(${(captchaTime.value -= 1)}s)`; |             )}(${(captchaTime.value -= 1)}s)`; | ||||||
|             captchaTimer.value = window.setInterval(() => { |             captchaTimer.value = window.setInterval(() => { | ||||||
|               captchaTime.value -= 1; |               captchaTime.value -= 1; | ||||||
|               captchaBtnNameKey.value = `${t( |               captchaBtnNameKey.value = `${t( | ||||||
|                 'userCenter.securitySettings.captcha.get' |                 'userCenter.securitySettings.captcha.get', | ||||||
|               )}(${captchaTime.value}s)`; |               )}(${captchaTime.value}s)`; | ||||||
|               if (captchaTime.value <= 0) { |               if (captchaTime.value <= 0) { | ||||||
|                 resetCaptcha(); |                 resetCaptcha(); | ||||||
|   | |||||||
| @@ -13,7 +13,8 @@ | |||||||
|       "@/*": ["src/*"] |       "@/*": ["src/*"] | ||||||
|     }, |     }, | ||||||
|     "lib": ["es2020", "dom"], |     "lib": ["es2020", "dom"], | ||||||
|     "skipLibCheck": true |     "skipLibCheck": true, | ||||||
|  |     "allowJs": true | ||||||
|   }, |   }, | ||||||
|   "include": ["src/**/*", "src/**/*.vue"], |   "include": ["src/**/*", "src/**/*.vue"], | ||||||
|   "exclude": ["node_modules"] |   "exclude": ["node_modules"] | ||||||
|   | |||||||
| @@ -35,11 +35,14 @@ import org.dromara.sms4j.api.entity.SmsResponse; | |||||||
| import org.dromara.sms4j.comm.constant.SupplierConstant; | import org.dromara.sms4j.comm.constant.SupplierConstant; | ||||||
| import org.dromara.sms4j.core.factory.SmsFactory; | import org.dromara.sms4j.core.factory.SmsFactory; | ||||||
| import org.redisson.api.RateType; | import org.redisson.api.RateType; | ||||||
|  | import org.springframework.http.HttpHeaders; | ||||||
| import org.springframework.validation.annotation.Validated; | import org.springframework.validation.annotation.Validated; | ||||||
| import org.springframework.web.bind.annotation.GetMapping; | import org.springframework.web.bind.annotation.*; | ||||||
| import org.springframework.web.bind.annotation.RequestMapping; |  | ||||||
| import org.springframework.web.bind.annotation.RestController; |  | ||||||
|  |  | ||||||
|  | import com.anji.captcha.model.common.RepCodeEnum; | ||||||
|  | import com.anji.captcha.model.common.ResponseModel; | ||||||
|  | import com.anji.captcha.model.vo.CaptchaVO; | ||||||
|  | import com.anji.captcha.service.CaptchaService; | ||||||
| import com.wf.captcha.base.Captcha; | import com.wf.captcha.base.Captcha; | ||||||
|  |  | ||||||
| import cn.dev33.satoken.annotation.SaIgnore; | import cn.dev33.satoken.annotation.SaIgnore; | ||||||
| @@ -58,6 +61,7 @@ import top.charles7c.continew.starter.captcha.graphic.autoconfigure.GraphicCaptc | |||||||
| import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties; | import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties; | ||||||
| import top.charles7c.continew.starter.core.util.TemplateUtils; | import top.charles7c.continew.starter.core.util.TemplateUtils; | ||||||
| import top.charles7c.continew.starter.core.util.validate.CheckUtils; | import top.charles7c.continew.starter.core.util.validate.CheckUtils; | ||||||
|  | import top.charles7c.continew.starter.core.util.validate.ValidationUtils; | ||||||
| import top.charles7c.continew.starter.extension.crud.model.resp.R; | import top.charles7c.continew.starter.extension.crud.model.resp.R; | ||||||
| import top.charles7c.continew.starter.messaging.mail.util.MailUtils; | import top.charles7c.continew.starter.messaging.mail.util.MailUtils; | ||||||
|  |  | ||||||
| @@ -72,13 +76,27 @@ import top.charles7c.continew.starter.messaging.mail.util.MailUtils; | |||||||
| @Validated | @Validated | ||||||
| @RestController | @RestController | ||||||
| @RequiredArgsConstructor | @RequiredArgsConstructor | ||||||
| @RequestMapping("/common/captcha") | @RequestMapping("/captcha") | ||||||
| public class CaptchaController { | public class CaptchaController { | ||||||
|  |  | ||||||
|  |     private final CaptchaService captchaService; | ||||||
|     private final CaptchaProperties captchaProperties; |     private final CaptchaProperties captchaProperties; | ||||||
|     private final ProjectProperties projectProperties; |     private final ProjectProperties projectProperties; | ||||||
|     private final GraphicCaptchaProperties graphicCaptchaProperties; |     private final GraphicCaptchaProperties graphicCaptchaProperties; | ||||||
|  |  | ||||||
|  |     @Operation(summary = "获取行为验证码", description = "获取行为验证码(Base64编码)") | ||||||
|  |     @GetMapping("/behavior") | ||||||
|  |     public R<Object> getBehaviorCaptcha(CaptchaVO captchaReq, HttpServletRequest request) { | ||||||
|  |         captchaReq.setBrowserInfo(JakartaServletUtil.getClientIP(request) + request.getHeader(HttpHeaders.USER_AGENT)); | ||||||
|  |         return R.ok(captchaService.get(captchaReq).getRepData()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "校验行为验证码", description = "校验行为验证码") | ||||||
|  |     @PostMapping("/behavior") | ||||||
|  |     public R<Object> checkBehaviorCaptcha(@RequestBody CaptchaVO captchaReq) { | ||||||
|  |         return R.ok(captchaService.check(captchaReq)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Operation(summary = "获取图片验证码", description = "获取图片验证码(Base64编码,带图片格式:data:image/gif;base64)") |     @Operation(summary = "获取图片验证码", description = "获取图片验证码(Base64编码,带图片格式:data:image/gif;base64)") | ||||||
|     @GetMapping("/img") |     @GetMapping("/img") | ||||||
|     public R<CaptchaResp> getImageCaptcha() { |     public R<CaptchaResp> getImageCaptcha() { | ||||||
| @@ -118,7 +136,11 @@ public class CaptchaController { | |||||||
|     @GetMapping("/sms") |     @GetMapping("/sms") | ||||||
|     public R getSmsCaptcha( |     public R getSmsCaptcha( | ||||||
|         @NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式错误") String phone, |         @NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式错误") String phone, | ||||||
|         HttpServletRequest request) { |         CaptchaVO captchaReq, HttpServletRequest request) { | ||||||
|  |         // 行为验证码校验 | ||||||
|  |         ResponseModel verificationRes = captchaService.verification(captchaReq); | ||||||
|  |         ValidationUtils.throwIfNotEqual(verificationRes.getRepCode(), RepCodeEnum.SUCCESS.getCode(), | ||||||
|  |             verificationRes.getRepMsg()); | ||||||
|         CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms(); |         CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms(); | ||||||
|         String templateId = captchaSms.getTemplateId(); |         String templateId = captchaSms.getTemplateId(); | ||||||
|         String limitKeyPrefix = CacheConstants.LIMIT_KEY_PREFIX; |         String limitKeyPrefix = CacheConstants.LIMIT_KEY_PREFIX; | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ import top.charles7c.continew.starter.log.common.annotation.Log; | |||||||
| /** | /** | ||||||
|  * 消息管理 API |  * 消息管理 API | ||||||
|  * |  * | ||||||
|  * @author BULL_BCLS |  * @author Bull-BCLS | ||||||
|  * @since 2023/10/15 19:05 |  * @since 2023/10/15 19:05 | ||||||
|  */ |  */ | ||||||
| @Tag(name = "消息管理 API") | @Tag(name = "消息管理 API") | ||||||
|   | |||||||
| @@ -88,8 +88,13 @@ spring.cache: | |||||||
|     # 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题) |     # 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题) | ||||||
|     cache-null-values: true |     cache-null-values: true | ||||||
|  |  | ||||||
| --- ### 图形验证码配置 | --- ### 验证码配置 | ||||||
| continew-starter.captcha: | continew-starter.captcha: | ||||||
|  |   ## 行为验证码配置 | ||||||
|  |   behavior: | ||||||
|  |     enabled: true | ||||||
|  |     cache-type: REDIS | ||||||
|  |     water-mark: ${project.app-name} | ||||||
|   ## 图形验证码配置 |   ## 图形验证码配置 | ||||||
|   graphic: |   graphic: | ||||||
|     enabled: true |     enabled: true | ||||||
|   | |||||||
| @@ -90,8 +90,13 @@ spring.cache: | |||||||
|     # 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题) |     # 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题) | ||||||
|     cache-null-values: true |     cache-null-values: true | ||||||
|  |  | ||||||
| --- ### 图形验证码配置 | --- ### 验证码配置 | ||||||
| continew-starter.captcha: | continew-starter.captcha: | ||||||
|  |   ## 行为验证码配置 | ||||||
|  |   behavior: | ||||||
|  |     enabled: true | ||||||
|  |     cache-type: REDIS | ||||||
|  |     water-mark: ${project.app-name} | ||||||
|   ## 图形验证码配置 |   ## 图形验证码配置 | ||||||
|   graphic: |   graphic: | ||||||
|     enabled: true |     enabled: true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Bull-BCLS
					Bull-BCLS