slideCode.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <template>
  2. <view class="slide_code_container">
  3. <!-- 背景区域 -->
  4. <view class="slide_bg" :style="{ backgroundImage: sliderBgUrl ? 'url(' + sliderBgUrl + ')' : '' }">
  5. <!-- 拼图缺口示意 -->
  6. <view class="block_bg"
  7. :style="{ left: sliderPositionX + 'px',top:sliderPositionY+'px', backgroundImage: blockBgUrl ? 'url(' + blockBgUrl + ')' : '' }">
  8. </view>
  9. </view>
  10. <!-- 滑块轨道 -->
  11. <view class="slide_track" @mousemove="onMouseMove" @mouseup="onMouseUp" @mouseleave="onMouseUp">
  12. <!-- 已滑过的进度条 -->
  13. <view class="slide_progress" :style="{ width: sliderPositionX + 'px' }"
  14. :class="{ 'success': sliderVerified==1, 'fail': sliderVerified==-1 }">
  15. </view>
  16. <!-- 提示文字 -->
  17. <view class="slide_text" :class="{ 'hide': isDragging || sliderVerified ==1 }">
  18. {{ statusText }}
  19. </view>
  20. <!-- 可拖动滑块 -->
  21. <view class="slide_btn" :style="{ left: sliderPositionX + 'px' }"
  22. :class="{ 'success': sliderVerified==1, 'fail': sliderVerified==-1 }" @touchstart="onTouchStart"
  23. @touchmove="onTouchMove" @touchend="onTouchEnd" @mousedown="onMouseDown">
  24. <text v-if="sliderVerified==0" class="icon">>></text>
  25. <text v-else-if="sliderVerified===1" class="icon">✔</text>
  26. <text v-else class="icon">✖</text>
  27. </view>
  28. </view>
  29. </view>
  30. </template>
  31. <script setup>
  32. import {
  33. ref,
  34. computed,
  35. onMounted,
  36. watch
  37. } from 'vue';
  38. import user from '@/reqApi/user.js';
  39. // --- Props ---
  40. // 1. 显式接收 props 对象
  41. const props = defineProps({
  42. // 背景图片地址
  43. sliderBgUrl: {
  44. type: String,
  45. default: ''
  46. },
  47. blockBgUrl: {
  48. type: String,
  49. default: ''
  50. }, //拼图缺口图片地址
  51. sliderUUID: {
  52. type: String,
  53. default: '',
  54. }, //uuid
  55. // sliderVerified: {
  56. // type: Number,
  57. // default: 0
  58. // },//是否验证通过 0 未验证 1 验证通过 -1 验证失败
  59. sliderPositionY: {
  60. type: Number,
  61. default: 0
  62. },
  63. });
  64. // --- Emits ---
  65. const emit = defineEmits(['success', 'fail', 'reset', 'CheckResult']);
  66. // --- State ---
  67. const sliderPositionX = ref(0); //当前滑块移动的X轴距离 (相对位移)
  68. const sliderVerified = ref(0); //0 未验证 1 验证通过 -1 验证失败
  69. // const sliderPositionY = ref(0); //拼图缺口的Y轴位置 后端返回
  70. const targetX = ref(0); // 拼图缺口的目标X轴位置 (后端返回或随机生成)
  71. const isDragging = ref(false); // 是否正在拖动
  72. const sliderPhone = ref(''); //验证的手机号
  73. const startX = ref(0); //鼠标或者手指按下时的初始x坐标
  74. // --- Computed ---
  75. const statusText = computed(() => {
  76. if (sliderVerified.value == 1) return '验证成功';
  77. if (sliderVerified.value == -1) {
  78. return '验证失败';
  79. }
  80. return '向右滑动填充拼图';
  81. });
  82. watch(() => props.sliderVerified, (newVal) => {
  83. if (newVal === -1) {
  84. // 如果验证失败,重置滑块位置为 0
  85. sliderPositionX.value = 0;
  86. }
  87. });
  88. // --- Methods ---
  89. // 初始化/重置
  90. const Reset = () => {
  91. isDragging.value = false;
  92. // 随机生成目标位置 (模拟)
  93. sliderPositionX.value = 0;
  94. };
  95. // 开始拖动 (Touch)
  96. const onTouchStart = (e) => {
  97. if (props.sliderVerified == 1) return; //验证通过不允许再拖动
  98. isDragging.value = true;
  99. // 记录手指按下的屏幕坐标
  100. startX.value = e.touches[0].clientX;
  101. // 阻止默认行为,防止页面滚动或浏览器手势导航
  102. e.preventDefault();
  103. };
  104. // 拖动中 (Touch)
  105. const onTouchMove = (e) => {
  106. if (!isDragging.value) return;
  107. // 【可选】再次阻止默认行为,确保拖动流畅且不滚动页面
  108. e.preventDefault();
  109. // 计算移动距离 = 当前坐标 - 初始坐标
  110. const currentX = e.touches[0].clientX;
  111. let moveDistance = currentX - startX.value; //相对距离
  112. updatePosition(moveDistance);
  113. };
  114. // 结束拖动 (Touch)
  115. const onTouchEnd = () => {
  116. if (!isDragging.value) return;
  117. isDragging.value = false;
  118. console.log("拖动结束,验证结果");
  119. VerifySlideCaptcha();
  120. // emit('CheckResult',sliderPositionX.value)
  121. };
  122. // 开始拖动 (Mouse - PC端)
  123. const onMouseDown = (e) => {
  124. if (props.sliderVerified == 1) return; // 验证通过不允许再拖动
  125. isDragging.value = true;
  126. // 记录鼠标按下时的屏幕坐标
  127. startX.value = e.clientX;
  128. // 防止文本选中
  129. e.preventDefault();
  130. };
  131. // 拖动中 (Mouse - PC端)
  132. // 注意:这个函数由父容器 .slide_track 触发
  133. const onMouseMove = (e) => {
  134. if (!isDragging.value) return;
  135. // 计算移动距离 = 当前坐标 - 初始坐标
  136. const currentX = e.clientX;
  137. let moveDistance = currentX - startX.value;
  138. updatePosition(moveDistance);
  139. };
  140. // 结束拖动 (Mouse - PC端)
  141. // 注意:这个函数由父容器 .slide_track 触发
  142. const onMouseUp = () => {
  143. if (!isDragging.value) return;
  144. isDragging.value = false;
  145. console.log("鼠标拖动结束,验证结果");
  146. VerifySlideCaptcha();
  147. };
  148. // 更新位置逻辑
  149. const updatePosition = (moveDistance) => {
  150. // 限制移动范围 [0, maxSlideDistance]
  151. if (moveDistance < 0) moveDistance = 0;
  152. if (moveDistance > 250) moveDistance = 250;
  153. sliderPositionX.value = moveDistance;
  154. };
  155. //验证滑动验证码位置是否正确
  156. const VerifySlideCaptcha = () => {
  157. let params = {
  158. sliderUUID: props.sliderUUID,
  159. x: Math.round(sliderPositionX.value), //滑动验证码块切块图位置x坐标
  160. };
  161. user.checkSlideCaptcha(params).then(res => {
  162. console.log("333验证滑动验证码结果", res);
  163. if (res.code == 200) {
  164. //验证成功
  165. sliderVerified.value = 1;
  166. sliderPhone.value = res.data; //手机号
  167. emit('CheckResult', sliderPhone.value)
  168. } else if (res.code == 601) {
  169. //验证成功
  170. sliderVerified.value = 1;
  171. uni.showToast({
  172. title: res.msg || '滑块验证通过,您上次的验证码还在有效期,请输入上次发送的验证码进行登录!',
  173. icon: 'none', // 可选值:'success', 'loading', 'none'
  174. duration: 2000 // 提示持续时间,单位毫秒
  175. });
  176. sliderPhone.value = res.data; //手机号
  177. emit('CheckResult', sliderPhone.value)
  178. } else if (res.code == 602) {
  179. //滑动验证码过期 重新获取验证码
  180. uni.showToast({
  181. title: '验证码已过期,已重新获取,请重新拖动滑块验证',
  182. icon: 'none', // 可选值:'success', 'loading', 'none'
  183. duration: 2000 // 提示持续时间,单位毫秒
  184. });
  185. // GetObtainSliderImg();//重新获取验证码
  186. emit('Refresh')
  187. } else {
  188. sliderVerified.value = 0; //验证失败
  189. sliderPositionX.value = 0; //重置滑块位置
  190. }
  191. });
  192. }
  193. // 验证结果
  194. const checkResult = () => {
  195. };
  196. // 生命周期
  197. onMounted(() => {
  198. Reset();
  199. console.log("打印初始值", props.sliderPositionY);
  200. console.log("打印sliderBgUrl", props.sliderBgUrl);
  201. console.log("打印blockBgUrl", props.blockBgUrl);
  202. });
  203. // 暴露方法给父组件
  204. defineExpose({
  205. Reset
  206. });
  207. </script>
  208. <style scoped>
  209. .slide_code_container {
  210. width: 300px;
  211. /* 可根据需求调整宽度 */
  212. user-select: none;
  213. height: auto;
  214. padding-top: 5px;
  215. box-sizing: border-box;
  216. }
  217. .slide_bg {
  218. width: 100%;
  219. height: 180px;
  220. background-image: url('@/static/image/bg/captcha_bg.png');
  221. background-size: 100% 100%;
  222. background-position: 0 0;
  223. background-repeat: no-repeat;
  224. border-radius: 4px;
  225. position: relative;
  226. }
  227. .block_bg {
  228. width: 50px;
  229. height: 50px;
  230. background-image: url('@/static/image/bg/captcha_block.png');
  231. background-position: 0 0;
  232. background-repeat: no-repeat;
  233. background-size: 100% 100%;
  234. position: absolute;
  235. left: 0;
  236. top: 35px;
  237. border-radius: 5px;
  238. border: 1px solid red;
  239. }
  240. .puzzle_hole {
  241. position: absolute;
  242. top: 30px;
  243. /* 假设缺口高度 */
  244. width: 40px;
  245. height: 40px;
  246. background: rgba(0, 0, 0, 0.5);
  247. box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.8);
  248. border-radius: 4px;
  249. }
  250. /* 滑块轨道 */
  251. .slide_track {
  252. position: relative;
  253. width: 100%;
  254. height: 40px;
  255. background-color: #eef1f8;
  256. border-radius: 4px;
  257. border: 1px solid #dcdcdc;
  258. box-sizing: border-box;
  259. }
  260. /* 已滑过的进度条 */
  261. .slide_progress {
  262. position: absolute;
  263. left: 0;
  264. top: 0;
  265. height: 100%;
  266. background-color: #19be6b;
  267. border-radius: 20px 0 0 20px;
  268. transition: width 0.1s;
  269. }
  270. /* 成功的背景 */
  271. .slide_progress.success {
  272. background-color: #19be6b;
  273. }
  274. .slide_progress.fail {
  275. background-color: #ff4949;
  276. }
  277. /* 提示文字 */
  278. .slide_text {
  279. position: absolute;
  280. width: 100%;
  281. height: 100%;
  282. display: flex;
  283. justify-content: center;
  284. align-items: center;
  285. font-size: 14px;
  286. color: #666;
  287. z-index: 1;
  288. transition: opacity 0.3s;
  289. }
  290. .slide_text.hide {
  291. opacity: 0.7;
  292. text-align: right;
  293. }
  294. /* 滑块的按钮样式 */
  295. .slide_btn {
  296. position: absolute;
  297. left: 0;
  298. top: -1px;
  299. /* 微调对齐 */
  300. width: 50px;
  301. height: 38px;
  302. background-color: #fff;
  303. border: 1px solid #dcdcdc;
  304. border-radius: 5px;
  305. box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
  306. display: flex;
  307. justify-content: center;
  308. align-items: center;
  309. cursor: pointer;
  310. z-index: 2;
  311. transition: left 0.1s;
  312. /* 平滑拖动 */
  313. }
  314. .slide_btn.success {
  315. background-color: #19be6b;
  316. border-color: #19be6b;
  317. color: #fff;
  318. }
  319. .slide_btn.fail {
  320. background-color: #ff4949;
  321. border-color: #ff4949;
  322. color: #fff;
  323. }
  324. .icon {
  325. font-weight: bold;
  326. font-size: 16px;
  327. }
  328. </style>