shish 1 kuukausi sitten
vanhempi
commit
5d49c1781f

+ 254 - 4
ghsApp/src/admin/home/login.vue

@@ -54,8 +54,60 @@
 					</view>
 				</template>
 
+				<template v-if="showSmsLogin">
+					<view class="login-card-head">
+						<text class="login-card-title">验证码登录</text>
+					</view>
+					<view class="login-form">
+						<view class="login-field">
+							<text class="login-label">手机号</text>
+							<input
+								@focus="mobile=''"
+								v-model="mobile"
+								class="login-input"
+								placeholder="请输入手机号"
+								type="number"
+							/>
+						</view>
+						<view class="login-field login-field--last" style="display: flex; align-items: center; justify-content: space-between;">
+							<view style="flex: 1; margin-right: 20upx;">
+								<text class="login-label">验证码</text>
+								<input
+									@focus="smsCode=''"
+									v-model="smsCode"
+									class="login-input"
+									placeholder="请输入验证码"
+									type="number"
+								/>
+							</view>
+							<button 
+								class="admin-button-com middle blue" 
+								:disabled="smsCountdown > 0" 
+								@click="getSmsCode"
+								style="margin-top: 40upx; font-size: 26upx; height: 70upx; line-height: 70upx; padding: 0 20upx; min-width: 180upx;"
+							>
+								{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}
+							</button>
+						</view>
+						<button class="login-btn login-btn--primary" @click="smsLoginFun">登录</button>
+					</view>
+					<view class="login-agreement">
+						<checkbox-group @change="agreeItem">
+							<label class="login-agreement-label">
+								<checkbox value="1" class="login-checkbox" :checked="agree == 1" />
+								<text class="login-agreement-text">
+									我已阅读并同意
+									<text class="login-agreement-link" @click.stop="goToService">《服务协议》</text>
+									和
+									<text class="login-agreement-link" @click.stop="goToPrivacy">《隐私政策》</text>
+								</text>
+							</label>
+						</checkbox-group>
+					</view>
+				</template>
+
 				<!-- #ifdef MP-WEIXIN -->
-				<view v-if="showAccountLogin == false" class="login-quick">
+				<view v-if="showAccountLogin == false && showSmsLogin == false" class="login-quick">
 					<view class="login-card-head">
 						<text class="login-card-title">手机号快捷登录</text>
 					</view>
@@ -75,7 +127,7 @@
 				<!-- #endif -->
 
 				<!-- #ifdef APP-PLUS -->
-				<view v-if="showAccountLogin == false" class="login-quick">
+				<view v-if="showAccountLogin == false && showSmsLogin == false" class="login-quick">
 					<view class="login-card-head">
 						<text class="login-card-title">本机号码快捷登录</text>
 					</view>
@@ -100,27 +152,63 @@
 				<!-- #endif -->
 
 				<view class="login-footer-links">
+					<!-- 如果当前不是账号密码登录,显示切换到账号密码登录的链接 -->
 					<text
 						v-if="showAccountLogin == false"
 						class="login-footer-link"
 						@click="useAccount()"
 					>使用账号密码登录</text>
+					<!-- 如果当前是账号密码登录,显示切换到快捷登录的链接 -->
+					<text
+						v-else
+						class="login-footer-link"
+						@click="useMoibleAccount()"
+					>使用快捷登录</text>
+
+					<text class="login-footer-divider">|</text>
+
+					<!-- 如果当前不是短信登录,显示切换到短信登录的链接 -->
+					<text
+						v-if="showSmsLogin == false"
+						class="login-footer-link"
+						@click="useSms()"
+					>使用短信登录</text>
+					<!-- 如果当前是短信登录,显示切换到快捷登录的链接 -->
 					<text
 						v-else
 						class="login-footer-link"
 						@click="useMoibleAccount()"
-					>使用手机号登录</text>
+					>使用快捷登录</text>
+
 					<text class="login-footer-divider">|</text>
 					<text class="login-footer-link login-footer-link--register" @click="register">注册账号</text>
 				</view>
 			</view>
 		</view>
+
+		<!-- #ifdef APP-PLUS -->
+		<web-view
+			v-if="showAppCaptcha"
+			:src="'/hybrid/html/captcha.html?sceneId=' + captchaSceneId"
+			@message="onAppCaptchaMessage"
+			style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 99999;"
+		></web-view>
+		<!-- #endif -->
+
+		<!-- #ifdef MP-WEIXIN -->
+		<aliyun-captcha
+			v-if="showWxCaptcha"
+			:sceneId="captchaSceneId"
+			@success="onWxCaptchaSuccess"
+			@fail="onWxCaptchaFail"
+		></aliyun-captcha>
+		<!-- #endif -->
 	</view>
 </template>
 <script>
 import { mapActions } from "vuex";
 import { getShopInfo} from "@/utils/auth";
-import {appLogin} from '@/api/common'
+import {appLogin, sendLoginSms, smsLogin} from '@/api/common'
 import { getDictionaries,postWxCode } from '@/api/mini'
 import { phoneLogin,wxMobileLogin } from "@/api/auth";
 
@@ -135,6 +223,13 @@ export default {
 			hasMobileNet:false,
 			agree:0,
 			showAccountLogin:false,
+			showSmsLogin:false,
+			showAppCaptcha: false,
+			showWxCaptcha: false,
+			captchaSceneId: 'YOUR_SCENE_ID', // 阿里云控制台申请的场景ID
+			smsCode:'',
+			smsCountdown: 0,
+			smsTimer: null,
 			statusBarHeight: 0
 		}
 	},
@@ -142,6 +237,7 @@ export default {
 	},
 	onLoad(){
 		this.setStatusBarHeight()
+		this.initSmsCountdown()
 		let that = this
 		uni.getNetworkType({
 			success: function (res) {
@@ -175,6 +271,9 @@ export default {
 	},
 	onUnload(){
 		uni.offNetworkStatusChange(this.dealLogin)
+		if (this.smsTimer) {
+			clearInterval(this.smsTimer)
+		}
 	},
 	methods: {
 		...mapActions(['setUserShopAll']),
@@ -194,14 +293,165 @@ export default {
 		},
 		useMoibleAccount(){
 			this.showAccountLogin = false
+			this.showSmsLogin = false
 			this.agree = 0
 			this.$forceUpdate()
 		},
 		useAccount(){
 			this.showAccountLogin = true
+			this.showSmsLogin = false
 			this.agree = 0
 			this.$forceUpdate()
 		},
+		useSms(){
+			this.showAccountLogin = false
+			this.showSmsLogin = true
+			this.agree = 0
+			this.$forceUpdate()
+		},
+		// 从 storage 恢复短信倒计时,防止刷新或重新打开页面绕过限制
+		initSmsCountdown() {
+			const lastSent = uni.getStorageSync('last_sms_sent_time_login')
+			if (lastSent) {
+				const passed = Math.floor((Date.now() - lastSent) / 1000)
+				if (passed < 60) {
+					this.startCountdown(60 - passed)
+				}
+			}
+		},
+		// 启动短信倒计时定时器
+		startCountdown(seconds) {
+			this.smsCountdown = seconds
+			if (this.smsTimer) {
+				clearInterval(this.smsTimer)
+			}
+			this.smsTimer = setInterval(() => {
+				this.smsCountdown--
+				if (this.smsCountdown <= 0) {
+					clearInterval(this.smsTimer)
+				}
+			}, 1000)
+		},
+		// 获取短信验证码(先触发人机验证)
+		getSmsCode() {
+			let mobile = this.mobile
+			if (this.$util.isEmpty(mobile)) {
+				this.$msg('请输入手机号')
+				return false
+			}
+			if (this.$util.checkMobile(mobile) == false) {
+				this.$msg('请输入正确手机号')
+				return false
+			}
+			
+			// 触发人机验证
+			this.triggerCaptcha()
+		},
+		// 唤起人机验证组件
+		triggerCaptcha() {
+			// #ifdef APP-PLUS
+			this.showAppCaptcha = true
+			// #endif
+
+			// #ifdef MP-WEIXIN
+			this.showWxCaptcha = true
+			// #endif
+
+			// #ifndef APP-PLUS || MP-WEIXIN
+			// 其他平台(如H5或调试环境)若未集成,可直接调用发送接口
+			this.sendSmsWithCaptcha('')
+			// #endif
+		},
+		// 携带人机验证凭证请求发送短信
+		sendSmsWithCaptcha(captchaVerifyParam) {
+			let that = this
+			let mobile = this.mobile
+			uni.showLoading({ mask: true })
+			sendLoginSms({ 
+				mobile: mobile,
+				captchaVerifyParam: captchaVerifyParam
+			}).then(res => {
+				uni.hideLoading()
+				if (res.code == 1) {
+					this.$msg('验证码已发送')
+					uni.setStorageSync('last_sms_sent_time_login', Date.now())
+					this.startCountdown(60)
+				}
+			}).catch(() => {
+				uni.hideLoading()
+			})
+		},
+		// App 端 WebView 消息监听
+		onAppCaptchaMessage(e) {
+			const data = e.detail.data[0]
+			if (data && data.action === 'captcha_success') {
+				this.showAppCaptcha = false
+				this.sendSmsWithCaptcha(data.captchaVerifyParam)
+			} else if (data && data.action === 'captcha_cancel') {
+				this.showAppCaptcha = false
+				this.$msg('验证已取消')
+			}
+		},
+		// 微信小程序端人机验证成功
+		onWxCaptchaSuccess(e) {
+			this.showWxCaptcha = false
+			const param = e.detail || e
+			this.sendSmsWithCaptcha(param)
+		},
+		// 微信小程序端人机验证失败
+		onWxCaptchaFail(e) {
+			this.showWxCaptcha = false
+			this.$msg('验证失败,请重试')
+		},
+		// 短信验证码登录提交
+		smsLoginFun() {
+			let that = this
+			let mobile = this.mobile
+			let code = this.smsCode
+			if (this.$util.isEmpty(mobile)) {
+				this.$msg('请输入手机号')
+				return false
+			}
+			if (this.$util.checkMobile(mobile) == false) {
+				this.$msg('请输入正确手机号')
+				return false
+			}
+			if (this.$util.isEmpty(code)) {
+				this.$msg('请输入验证码')
+				return false
+			}
+
+			this.checkAgreement(function() {
+				uni.showLoading({ mask: true })
+				smsLogin({ mobile: mobile, code: code, deviceId: uni.getStorageSync('deviceId') }).then(subRes => {
+					uni.hideLoading()
+					if (subRes.code == 1) {
+						if (that.$util.isEmpty(subRes)) {
+							return false
+						}
+						uni.setStorageSync('token', subRes.data.token)
+						uni.setStorageSync('shopId', subRes.data.admin.currentGhsShopId)
+						getDictionaries({ token: subRes.data.token }).then(res => {
+							if (that.$util.isEmpty(res)) {
+								return false
+							}
+							that.$store.commit('setDictionariesInfo', { ...res.data.dict, ...res.data })
+							that.$store.commit('setMyShopInfo', { ...res.data.shop, ...res.data })
+						})
+						that.$store.commit('setLoginInfo', { ...subRes.data.admin, ...subRes.data })
+						getShopInfo(true).then(res => { uni.setNavigationBarTitle({ title: res.name }) })
+						that.setUserShopAll()
+						uni.navigateBack()
+
+						// #ifdef APP-PLUS
+						that.$util.afterAppLoginSuccess({ auto: 0 })
+						// #endif
+					}
+				}).catch(() => {
+					uni.hideLoading()
+				})
+			})
+		},
 		goToService(){
 			this.$util.pageTo({ url: "/admin/home/register"})
 		},

+ 6 - 0
ghsApp/src/api/common/index.js

@@ -49,5 +49,11 @@ export const initSocket = async clientId => {
 //app登陆
 export const appLogin = data => https.post("/auth/app-login", data);
 
+// 发送登录验证码
+export const sendLoginSms = data => https.post("/auth/send-login-sms", data);
+
+// 短信验证码登录
+export const smsLogin = data => https.post("/auth/sms-login", data);
+
 //app重新获取登陆信息
 export const hasTokenAutoLogin = data => https.get("/admin/has-token-auto-login", data);

+ 132 - 0
ghsApp/src/hybrid/html/captcha.html

@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+	<title>安全人机验证</title>
+	<!-- 引入阿里云验证码 2.0 Web SDK -->
+	<script src="https://g.alicdn.com/gcaptchas/kidd/1.0.0/index.js"></script>
+	<!-- 引入 uni-app SDK 用于向 App 发送消息 -->
+	<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
+	<style>
+		html, body {
+			margin: 0;
+			padding: 0;
+			width: 100%;
+			height: 100%;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			background-color: rgba(0, 0, 0, 0.4);
+			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+		}
+		.captcha-card {
+			width: 85%;
+			max-width: 340px;
+			background: #ffffff;
+			border-radius: 12px;
+			box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
+			overflow: hidden;
+			box-sizing: border-box;
+			padding: 20px;
+			position: relative;
+		}
+		.captcha-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			margin-bottom: 20px;
+		}
+		.captcha-title {
+			font-size: 16px;
+			font-weight: bold;
+			color: #333333;
+		}
+		.captcha-close {
+			font-size: 20px;
+			color: #999999;
+			cursor: pointer;
+			border: none;
+			background: none;
+			padding: 0 5px;
+			line-height: 1;
+		}
+		#ali-captcha {
+			width: 100%;
+			min-height: 100px;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+		.loading-tip {
+			color: #666666;
+			font-size: 14px;
+		}
+	</style>
+</head>
+<body>
+	<div class="captcha-card">
+		<div class="captcha-header">
+			<span class="captcha-title">安全验证</span>
+			<button class="captcha-close" onclick="cancelCaptcha()">×</button>
+		</div>
+		<div id="ali-captcha">
+			<div class="loading-tip">正在加载验证组件...</div>
+		</div>
+	</div>
+
+	<script>
+		// 解析 URL 参数
+		function getQueryParam(name) {
+			var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
+			var r = window.location.search.substr(1).match(reg);
+			if (r != null) return decodeURIComponent(r[2]);
+			return null;
+		}
+
+		var sceneId = getQueryParam('sceneId') || 'YOUR_SCENE_ID'; // 默认场景ID
+
+		function cancelCaptcha() {
+			if (window.uni) {
+				uni.postMessage({
+					data: {
+						action: 'captcha_cancel'
+					}
+				});
+				// 延迟一小会儿后尝试后退,确保消息发出
+				setTimeout(function() {
+					uni.navigateBack();
+				}, 100);
+			}
+		}
+
+		document.addEventListener('UniAppJSBridgeReady', function() {
+			// 清空加载提示
+			var container = document.getElementById('ali-captcha');
+			container.innerHTML = '';
+
+			// 初始化阿里云验证码 2.0
+			initAliyunCaptcha({
+				SceneId: sceneId,
+				prefix: 'ali-captcha',
+				mode: 'embed',
+				element: '#ali-captcha',
+				button: null, // 嵌入式,不需要绑定外部按钮
+				captchaVerifyCallback: function(captchaVerifyParam) {
+					// 验证成功,向 App 发送凭证
+					uni.postMessage({
+						data: {
+							action: 'captcha_success',
+							captchaVerifyParam: captchaVerifyParam
+						}
+					});
+					return Promise.resolve();
+				},
+				onBizResultCallback: function(bizResult) {
+					// 业务校验回调
+				}
+			});
+		});
+	</script>
+</body>
+</html>

+ 6 - 0
ghsApp/src/manifest.json

@@ -226,6 +226,12 @@
             "urlCheck" : false,
             "minified" : true
         },
+        "plugins": {
+            "AliyunCaptcha": {
+                "version": "3.0.0",
+                "provider": "wxbe275ff84246f1a4"
+            }
+        },
         "usingComponents" : true,
         "permission" : {
             "scope.userLocation" : {

+ 1 - 1
ghsApp/src/pages.json

@@ -1,7 +1,7 @@
 {
 	"pages": [
 		{"path": "admin/home/workbench","style": {"navigationBarTitleText": "店铺","enablePullDownRefresh": true}},
-		{"path": "admin/home/login","style": {"navigationBarTitleText": "","enablePullDownRefresh": false}},
+		{"path": "admin/home/login","style": {"navigationBarTitleText": "","enablePullDownRefresh": false, "usingComponents": {"aliyun-captcha": "plugin://AliyunCaptcha/aliyun-captcha"}}},
 		{"path": "admin/home/smsLogin","style": {"navigationBarTitleText": "","enablePullDownRefresh": false}},
 		{"path": "admin/home/pwdLogin","style": {"navigationBarTitleText": "","enablePullDownRefresh": false}},
 		{"path": "pages/captcha-demo","style": {"navigationBarTitleText": "验证码演示","enablePullDownRefresh": false}},