更新go结构和uniapp
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ maxmemory = 10485760
|
||||
# MySQL - 远程连接配置
|
||||
mysqluser = go-platform
|
||||
mysqlpass = FSmJCSJ5wk8pjjDC
|
||||
mysqlurls = 212.64.112.158:3388
|
||||
mysqlurls = 10.31.100.3:3306
|
||||
mysqldb = go-platform
|
||||
|
||||
# ORM配置
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AppAuthController App移动端认证控制器
|
||||
type AppAuthController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AppAuthController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// LoginBackend App端登录(需要租户)
|
||||
func (c *AppAuthController) LoginBackend() {
|
||||
var req struct {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(body) == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
req.TenantName = strings.TrimSpace(req.TenantName)
|
||||
req.Account = strings.TrimSpace(req.Account)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
if req.TenantName == "" || req.Account == "" || req.Password == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "租户名称、用户名或密码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
token, loginUser, err := services.BackendLogin(req.TenantName, req.Account, req.Password)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"data": map[string]interface{}{
|
||||
"token": token,
|
||||
"user": map[string]interface{}{
|
||||
"id": loginUser.ID,
|
||||
"account": loginUser.Account,
|
||||
"name": loginUser.Name,
|
||||
"tid": loginUser.Tid,
|
||||
"rid": loginUser.Rid,
|
||||
"avatar": loginUser.Avatar,
|
||||
"role_name": loginUser.RoleName,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetCurrentUser App端当前登录用户信息,需 Bearer Token
|
||||
func (c *AppAuthController) GetCurrentUser() {
|
||||
authHeader := c.Ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(authParts) != 2 || authParts[0] != "Bearer" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "认证信息格式错误"})
|
||||
return
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "无效的token"})
|
||||
return
|
||||
}
|
||||
if claims.UserType != "backend" && claims.UserType != "app" {
|
||||
c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权访问"})
|
||||
return
|
||||
}
|
||||
|
||||
var tenantUser models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", claims.UserID).
|
||||
Filter("tid", claims.TenantId).
|
||||
One(&tenantUser)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "用户不存在"})
|
||||
return
|
||||
}
|
||||
if tenantUser.Status == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "账号已禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
account := ""
|
||||
if tenantUser.Account != nil {
|
||||
account = strings.TrimSpace(*tenantUser.Account)
|
||||
}
|
||||
name := ""
|
||||
if tenantUser.Name != nil {
|
||||
name = strings.TrimSpace(*tenantUser.Name)
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"id": tenantUser.Uid,
|
||||
"account": account,
|
||||
"name": name,
|
||||
"tid": tenantUser.Tid,
|
||||
"rid": 0,
|
||||
"avatar": "",
|
||||
"role_name": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendLoginCode 发送App端登录验证码
|
||||
func (c *AppAuthController) SendLoginCode() {
|
||||
var req struct {
|
||||
Account string `json:"account"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.OpenVerifyEnabled != 1 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "当前未开启验证"})
|
||||
return
|
||||
}
|
||||
channel := strings.TrimSpace(req.Channel)
|
||||
if channel == "" {
|
||||
channel = cfg.VerifyType
|
||||
}
|
||||
if channel != "sms" && channel != "email" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "仅支持短信/邮箱验证码"})
|
||||
return
|
||||
}
|
||||
if err := services.SendBackendLoginCode(req.TenantName, req.Account, channel); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "验证码已发送"})
|
||||
}
|
||||
|
||||
// LoginBySms App端手机号验证码登录(占位实现)
|
||||
func (c *AppAuthController) LoginBySms() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "手机号验证码登录暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// Logout App端退出登录(当前为无状态直接返回成功)
|
||||
func (c *AppAuthController) Logout() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "退出成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetGeetest3Infos 获取App端极验3.0配置
|
||||
func (c *AppAuthController) GetGeetest3Infos() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.Geetest3ID == nil || cfg.Geetest3Key == nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验3参数"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"captcha_id": *cfg.Geetest3ID,
|
||||
"captcha_key": *cfg.Geetest3Key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetGeetest4Infos 获取App端极验4.0配置
|
||||
func (c *AppAuthController) GetGeetest4Infos() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.Geetest4ID == nil || cfg.Geetest4Key == nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验4参数"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"captcha_id": *cfg.Geetest4ID,
|
||||
"captcha_key": *cfg.Geetest4Key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetOpenVerify 判断是否开启App端登录验证
|
||||
func (c *AppAuthController) GetOpenVerify() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
openVerify := "0"
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
openVerify = "1"
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"data": []map[string]string{
|
||||
{
|
||||
"label": "openVerify",
|
||||
"value": openVerify,
|
||||
},
|
||||
{
|
||||
"label": "verifyType",
|
||||
"value": cfg.VerifyType,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyAccount 验证租户和账号是否存在(忘记密码第一步)
|
||||
func (c *AppAuthController) VerifyAccount() {
|
||||
var req struct {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
phone, email, err := services.VerifyTenantAccount(req.TenantName, req.Account)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "验证成功",
|
||||
"data": map[string]interface{}{
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendResetCode 发送找回密码验证码(忘记密码第二步)
|
||||
func (c *AppAuthController) SendResetCode() {
|
||||
var req struct {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Account string `json:"account"`
|
||||
Phone string `json:"phone"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
channel := strings.TrimSpace(req.Channel)
|
||||
if channel == "" {
|
||||
channel = "sms"
|
||||
}
|
||||
|
||||
if err := services.SendResetCode(req.TenantName, req.Account, req.Phone, channel); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword 重置密码(忘记密码第三步)
|
||||
func (c *AppAuthController) ResetPassword() {
|
||||
var req struct {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Account string `json:"account"`
|
||||
Phone string `json:"phone"`
|
||||
SmsCode string `json:"sms_code"`
|
||||
NewPassword string `json:"new_password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.ResetPassword(req.TenantName, req.Account, req.Phone, req.SmsCode, req.NewPassword, req.ConfirmPassword); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "密码重置成功",
|
||||
})
|
||||
}
|
||||
|
||||
// Register App端注册(占位实现)
|
||||
func (c *AppAuthController) Register() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "注册暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// SendRegisterCode 发送App端注册验证码(占位实现)
|
||||
func (c *AppAuthController) SendRegisterCode() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "发送注册验证码暂未实现",
|
||||
})
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (c *BackendAuthController) LoginBackend() {
|
||||
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
if cfg.VerifyType == "geetest4" {
|
||||
if cfg.VerifyType == "geetest4" || cfg.VerifyType == "geetest" {
|
||||
if req.LotNumber == "" || req.PassToken == "" || req.GenTime == "" || req.CaptchaOutput == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请完成人机验证"})
|
||||
return
|
||||
|
||||
+15
-14
@@ -8,25 +8,26 @@ import (
|
||||
|
||||
// Register 注册移动端(app)路由。
|
||||
func Register() {
|
||||
// 登录相关(复用 BackendAuthController)
|
||||
beego.Router("/app/login", &controllers.BackendAuthController{}, "post:LoginBackend")
|
||||
beego.Router("/app/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode")
|
||||
beego.Router("/app/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms")
|
||||
beego.Router("/app/logout", &controllers.BackendAuthController{}, "post:Logout")
|
||||
// 登录相关(使用 AppAuthController)
|
||||
beego.Router("/app/login", &controllers.AppAuthController{}, "post:LoginBackend")
|
||||
beego.Router("/app/sendLoginCode", &controllers.AppAuthController{}, "post:SendLoginCode")
|
||||
beego.Router("/app/loginBySms", &controllers.AppAuthController{}, "post:LoginBySms")
|
||||
beego.Router("/app/logout", &controllers.AppAuthController{}, "post:Logout")
|
||||
|
||||
// 当前用户信息
|
||||
beego.Router("/app/currentUser", &controllers.BackendAuthController{}, "get:GetCurrentUser")
|
||||
beego.Router("/app/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser")
|
||||
|
||||
// 极验与登录验证配置
|
||||
beego.Router("/app/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
|
||||
beego.Router("/app/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/app/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
|
||||
beego.Router("/app/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos")
|
||||
beego.Router("/app/login/getGeetest4Infos", &controllers.AppAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/app/login/getOpenVerify", &controllers.AppAuthController{}, "get:GetOpenVerify")
|
||||
|
||||
// 找回密码
|
||||
beego.Router("/app/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword")
|
||||
beego.Router("/app/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode")
|
||||
// 找回密码(三步流程)
|
||||
beego.Router("/app/verifyAccount", &controllers.AppAuthController{}, "post:VerifyAccount")
|
||||
beego.Router("/app/sendResetCode", &controllers.AppAuthController{}, "post:SendResetCode")
|
||||
beego.Router("/app/resetPassword", &controllers.AppAuthController{}, "post:ResetPassword")
|
||||
|
||||
// 注册
|
||||
beego.Router("/app/register", &controllers.BackendAuthController{}, "post:Register")
|
||||
beego.Router("/app/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode")
|
||||
beego.Router("/app/register", &controllers.AppAuthController{}, "post:Register")
|
||||
beego.Router("/app/sendRegisterCode", &controllers.AppAuthController{}, "post:SendRegisterCode")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
)
|
||||
|
||||
// resetCodeItem 存储找回密码的验证码
|
||||
type resetCodeItem struct {
|
||||
Code string
|
||||
Channel string
|
||||
ExpiredAt time.Time
|
||||
}
|
||||
|
||||
var resetCodeStore sync.Map
|
||||
|
||||
// resetCodeKey 生成密码重置验证码的存储key
|
||||
func resetCodeKey(tenantName, account, phone, channel string) string {
|
||||
key := strings.ToLower(strings.TrimSpace(tenantName)) + "|" +
|
||||
strings.ToLower(strings.TrimSpace(account)) + "|" +
|
||||
strings.ToLower(strings.TrimSpace(phone)) + "|" +
|
||||
strings.TrimSpace(channel)
|
||||
return key
|
||||
}
|
||||
|
||||
// VerifyTenantAccount 第一步:验证租户和账号是否存在
|
||||
// 返回该账号关联的手机号和邮箱(用于第二步选择验证方式)
|
||||
func VerifyTenantAccount(tenantName, account string) (phone, email string, err error) {
|
||||
tenantName = strings.TrimSpace(tenantName)
|
||||
account = strings.TrimSpace(account)
|
||||
|
||||
if tenantName == "" || account == "" {
|
||||
return "", "", errors.New("租户名称和账号不能为空")
|
||||
}
|
||||
|
||||
// 验证租户是否存在
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("tenant_name", tenantName).
|
||||
One(&tenant); err != nil {
|
||||
return "", "", errors.New("租户不存在")
|
||||
}
|
||||
if tenant.Status != 1 {
|
||||
return "", "", errors.New("租户已停用")
|
||||
}
|
||||
|
||||
// 验证该租户下的账号是否存在
|
||||
var tenantUser models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("account", account).
|
||||
One(&tenantUser); err != nil {
|
||||
return "", "", errors.New("账号不存在")
|
||||
}
|
||||
if tenantUser.Status == 0 {
|
||||
return "", "", errors.New("账号已禁用")
|
||||
}
|
||||
|
||||
// 返回该账号的手机号和邮箱
|
||||
phoneStr := ""
|
||||
if tenantUser.Phone != nil {
|
||||
phoneStr = strings.TrimSpace(*tenantUser.Phone)
|
||||
}
|
||||
emailStr := ""
|
||||
if tenantUser.Email != nil {
|
||||
emailStr = strings.TrimSpace(*tenantUser.Email)
|
||||
}
|
||||
|
||||
if phoneStr == "" && emailStr == "" {
|
||||
return "", "", errors.New("账号未绑定手机号或邮箱,无法重置密码")
|
||||
}
|
||||
|
||||
return phoneStr, emailStr, nil
|
||||
}
|
||||
|
||||
// SendResetCode 第二步:验证手机号并发送验证码
|
||||
func SendResetCode(tenantName, account, phone, channel string) error {
|
||||
tenantName = strings.TrimSpace(tenantName)
|
||||
account = strings.TrimSpace(account)
|
||||
phone = strings.TrimSpace(phone)
|
||||
channel = strings.TrimSpace(channel)
|
||||
|
||||
if tenantName == "" || account == "" || phone == "" {
|
||||
return errors.New("租户名称、账号和手机号不能为空")
|
||||
}
|
||||
if channel != "sms" && channel != "email" {
|
||||
return errors.New("仅支持短信或邮箱验证码")
|
||||
}
|
||||
|
||||
// 验证租户是否存在
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("tenant_name", tenantName).
|
||||
One(&tenant); err != nil {
|
||||
return errors.New("租户不存在")
|
||||
}
|
||||
|
||||
// 验证该租户下的账号和手机号是否匹配
|
||||
var tenantUser models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("account", account).
|
||||
One(&tenantUser); err != nil {
|
||||
return errors.New("账号不存在")
|
||||
}
|
||||
if tenantUser.Status == 0 {
|
||||
return errors.New("账号已禁用")
|
||||
}
|
||||
|
||||
// 根据验证渠道验证用户信息
|
||||
if channel == "sms" {
|
||||
if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone {
|
||||
return errors.New("手机号不匹配,请确认您输入的手机号正确")
|
||||
}
|
||||
} else if channel == "email" {
|
||||
if tenantUser.Email == nil || strings.TrimSpace(*tenantUser.Email) != phone {
|
||||
return errors.New("邮箱不匹配,请确认您输入的邮箱正确")
|
||||
}
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
|
||||
// 发送验证码
|
||||
if channel == "sms" {
|
||||
content := "密码重置验证码:" + code
|
||||
if err := enqueueSMSTaskForPasswordReset(tenant.ID, phone, content, code); err != nil {
|
||||
return errors.New("短信发送失败,请重试")
|
||||
}
|
||||
}
|
||||
// TODO: 实现邮箱验证码发送逻辑
|
||||
|
||||
// 存储验证码(5分钟有效期)
|
||||
resetCodeStore.Store(resetCodeKey(tenantName, account, phone, channel), resetCodeItem{
|
||||
Code: code,
|
||||
Channel: channel,
|
||||
ExpiredAt: time.Now().Add(5 * time.Minute),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyResetCode 验证重置密码的验证码
|
||||
func VerifyResetCode(tenantName, account, phone, channel, code string) error {
|
||||
tenantName = strings.TrimSpace(tenantName)
|
||||
account = strings.TrimSpace(account)
|
||||
phone = strings.TrimSpace(phone)
|
||||
channel = strings.TrimSpace(channel)
|
||||
code = strings.TrimSpace(code)
|
||||
|
||||
if code == "" {
|
||||
return errors.New("验证码不能为空")
|
||||
}
|
||||
|
||||
key := resetCodeKey(tenantName, account, phone, channel)
|
||||
val, ok := resetCodeStore.Load(key)
|
||||
if !ok {
|
||||
return errors.New("验证码不存在或已失效")
|
||||
}
|
||||
|
||||
item, ok := val.(resetCodeItem)
|
||||
if !ok {
|
||||
return errors.New("验证码状态异常")
|
||||
}
|
||||
|
||||
if time.Now().After(item.ExpiredAt) {
|
||||
resetCodeStore.Delete(key)
|
||||
return errors.New("验证码已过期")
|
||||
}
|
||||
|
||||
if item.Code != code {
|
||||
return errors.New("验证码错误")
|
||||
}
|
||||
|
||||
// 验证通过后删除验证码
|
||||
resetCodeStore.Delete(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword 第三步:重置密码
|
||||
func ResetPassword(tenantName, account, phone, smsCode, newPassword, confirmPassword string) error {
|
||||
tenantName = strings.TrimSpace(tenantName)
|
||||
account = strings.TrimSpace(account)
|
||||
phone = strings.TrimSpace(phone)
|
||||
smsCode = strings.TrimSpace(smsCode)
|
||||
newPassword = strings.TrimSpace(newPassword)
|
||||
confirmPassword = strings.TrimSpace(confirmPassword)
|
||||
|
||||
if tenantName == "" || account == "" || phone == "" {
|
||||
return errors.New("租户名称、账号和手机号不能为空")
|
||||
}
|
||||
if newPassword == "" {
|
||||
return errors.New("新密码不能为空")
|
||||
}
|
||||
if newPassword != confirmPassword {
|
||||
return errors.New("两次密码不一致")
|
||||
}
|
||||
if len(newPassword) < 6 {
|
||||
return errors.New("密码长度不能少于6个字符")
|
||||
}
|
||||
|
||||
// 验证验证码(验证码验证后会被删除)
|
||||
if err := VerifyResetCode(tenantName, account, phone, "sms", smsCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证租户
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("tenant_name", tenantName).
|
||||
One(&tenant); err != nil {
|
||||
return errors.New("租户不存在")
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
var tenantUser models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("account", account).
|
||||
One(&tenantUser); err != nil {
|
||||
return errors.New("账号不存在")
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
if tenantUser.Phone == nil || strings.TrimSpace(*tenantUser.Phone) != phone {
|
||||
return errors.New("手机号不匹配")
|
||||
}
|
||||
|
||||
// 哈希新密码
|
||||
hashedPassword, err := passwordutil.Hash(newPassword)
|
||||
if err != nil {
|
||||
return errors.New("密码处理失败")
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", tenantUser.ID).
|
||||
Update(map[string]interface{}{
|
||||
"password": hashedPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("密码更新失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enqueueSMSTaskForPasswordReset 发送密码重置短信任务
|
||||
func enqueueSMSTaskForPasswordReset(tid uint64, phone, content, code string) error {
|
||||
// 重用已有的短信发送逻辑
|
||||
return enqueueSMSTaskForLogin(tid, phone, content, code)
|
||||
}
|
||||
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import { getBaseURL } from '@/api/request.js'
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
console.log('App Launch')
|
||||
console.log('App Launch, API:', getBaseURL())
|
||||
},
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 移动端认证接口 — 对接 Go /app/*
|
||||
*/
|
||||
import { request, requestRaw } from '@/api/request.js'
|
||||
|
||||
/** 账号密码登录 */
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/app/login',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送登录验证码(账号二次校验用,需后台开启验证) */
|
||||
export function sendLoginCode(data) {
|
||||
return request({
|
||||
url: '/app/sendLoginCode',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 手机号验证码登录 */
|
||||
export function loginBySms(data) {
|
||||
return request({
|
||||
url: '/app/loginBySms',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 退出登录(后端无状态,失败可忽略;短超时避免卡住) */
|
||||
export function logoutApi(data = {}) {
|
||||
return request({
|
||||
url: '/app/logout',
|
||||
method: 'POST',
|
||||
data,
|
||||
silent: true,
|
||||
timeout: 5000
|
||||
})
|
||||
}
|
||||
|
||||
/** 当前用户信息 */
|
||||
export function getCurrentUser() {
|
||||
return request({
|
||||
url: '/app/currentUser',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
/** 是否开启登录人机/验证码校验 */
|
||||
export function getOpenVerify() {
|
||||
return request({
|
||||
url: '/app/login/getOpenVerify',
|
||||
method: 'GET',
|
||||
silent: true,
|
||||
timeout: 8000
|
||||
})
|
||||
}
|
||||
|
||||
/** 极验 3 配置 */
|
||||
export function getGeetest3Infos() {
|
||||
return request({
|
||||
url: '/app/login/getGeetest3Infos',
|
||||
method: 'GET',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
|
||||
/** 极验 4 配置 */
|
||||
export function getGeetest4Infos() {
|
||||
return request({
|
||||
url: '/app/login/getGeetest4Infos',
|
||||
method: 'GET',
|
||||
silent: true,
|
||||
timeout: 8000
|
||||
})
|
||||
}
|
||||
|
||||
/** 注册 */
|
||||
export function register(data) {
|
||||
return request({
|
||||
url: '/app/register',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送注册验证码 */
|
||||
export function sendRegisterCode(data) {
|
||||
return request({
|
||||
url: '/app/sendRegisterCode',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置密码 */
|
||||
export function resetPassword(data) {
|
||||
return request({
|
||||
url: '/app/resetPassword',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 验证租户和账号(找回密码第一步) */
|
||||
export function verifyAccount(data) {
|
||||
return request({
|
||||
url: '/app/verifyAccount',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送找回密码验证码(找回密码第二步) */
|
||||
export function sendResetCode(data) {
|
||||
return request({
|
||||
url: '/app/sendResetCode',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 getOpenVerify 返回的 label/value 列表
|
||||
* @returns {Promise<{ openVerify: boolean, verifyType: string }>}
|
||||
*/
|
||||
export async function fetchVerifyConfig() {
|
||||
try {
|
||||
const data = await getOpenVerify()
|
||||
const list = Array.isArray(data) ? data : []
|
||||
const map = {}
|
||||
list.forEach((item) => {
|
||||
if (item && item.label) map[item.label] = item.value
|
||||
})
|
||||
return {
|
||||
openVerify: map.openVerify === '1' || map.openVerify === 1,
|
||||
verifyType: map.verifyType || ''
|
||||
}
|
||||
} catch {
|
||||
return { openVerify: false, verifyType: '' }
|
||||
}
|
||||
}
|
||||
|
||||
export { requestRaw }
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 从项目根目录 .env 读取接口地址。
|
||||
* 变量名:VITE_API_BASE_URL
|
||||
* 调试默认 localhost;正式构建前在 .env 里改成 https://api.yunzer.cn 即可。
|
||||
*/
|
||||
|
||||
/** 极验 4.0 captcha_id(前端展示用;KEY 仅在后端校验) */
|
||||
export const GEETEST4_CAPTCHA_ID = '75e8a175c43b9ecfa15372c658be05e5'
|
||||
|
||||
/**
|
||||
* @returns {string} 去掉末尾斜杠的 baseURL
|
||||
*/
|
||||
export function resolveBaseURL() {
|
||||
let fromEnv = ''
|
||||
try {
|
||||
fromEnv =
|
||||
(typeof import.meta !== 'undefined' &&
|
||||
import.meta.env &&
|
||||
import.meta.env.VITE_API_BASE_URL) ||
|
||||
''
|
||||
} catch (e) {
|
||||
fromEnv = ''
|
||||
}
|
||||
|
||||
if (fromEnv && fromEnv !== 'undefined' && fromEnv !== 'null') {
|
||||
return String(fromEnv).replace(/\/$/, '')
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// H5 开发未配置 env 时走 vite 代理(前缀勿用 /api,会与 api/ 源码目录冲突)
|
||||
if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV) {
|
||||
return '/proxy-api'
|
||||
}
|
||||
// #endif
|
||||
|
||||
// App / 真机:localhost 指向设备自身,需用 .env.development 配置局域网 IP
|
||||
return 'http://localhost:9000'
|
||||
}
|
||||
|
||||
/** 与 resolveBaseURL 相同,便于业务侧统一引用 */
|
||||
export function getBaseURL() {
|
||||
return resolveBaseURL()
|
||||
}
|
||||
+115
-18
@@ -1,23 +1,120 @@
|
||||
/**
|
||||
* 统一请求封装,接入后端时在此配置 baseURL、token、错误处理。
|
||||
*
|
||||
* 示例:
|
||||
* export function request({ url, method = 'GET', data }) {
|
||||
* return new Promise((resolve, reject) => {
|
||||
* uni.request({
|
||||
* url: BASE_URL + url,
|
||||
* method,
|
||||
* data,
|
||||
* header: { Authorization: 'Bearer ' + getToken() },
|
||||
* success: (res) => {
|
||||
* if (res.statusCode >= 200 && res.statusCode < 300) resolve(res.data)
|
||||
* else reject(res)
|
||||
* },
|
||||
* fail: reject
|
||||
* })
|
||||
* })
|
||||
* }
|
||||
* 统一请求封装:baseURL、Bearer Token、业务 code 处理。
|
||||
* 接口地址统一从 .env 的 VITE_API_BASE_URL 读取(见 api/config.js)。
|
||||
*/
|
||||
import { getToken, logout } from '@/utils/auth.js'
|
||||
import { resolveBaseURL, getBaseURL } from '@/api/config.js'
|
||||
|
||||
const BASE_URL = resolveBaseURL()
|
||||
|
||||
export { getBaseURL }
|
||||
|
||||
/**
|
||||
* @param {{ url: string, method?: string, data?: object, header?: object, silent?: boolean }} options
|
||||
* @returns {Promise<any>} 业务 data 字段(若无 data 则返回整包)
|
||||
*/
|
||||
export function request(options = {}) {
|
||||
const { url, method = 'GET', data, header = {}, silent = false, timeout = 30000 } = options
|
||||
const token = getToken()
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...header
|
||||
}
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const fullUrl = BASE_URL.replace(/\/$/, '') + url
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: fullUrl,
|
||||
method: method.toUpperCase(),
|
||||
data: data || {},
|
||||
header: headers,
|
||||
timeout,
|
||||
success(res) {
|
||||
const status = res.statusCode
|
||||
const body = res.data || {}
|
||||
|
||||
if (status === 401 || body.code === 401) {
|
||||
logout()
|
||||
if (!silent) {
|
||||
uni.showToast({ title: body.msg || '请重新登录', icon: 'none' })
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}, 400)
|
||||
reject(new Error(body.msg || '未授权'))
|
||||
return
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
const msg = body.msg || `请求失败(${status})`
|
||||
if (!silent) uni.showToast({ title: msg, icon: 'none' })
|
||||
reject(new Error(msg))
|
||||
return
|
||||
}
|
||||
|
||||
// 统一后端 { code, msg, data }
|
||||
if (typeof body.code !== 'undefined' && body.code !== 200) {
|
||||
const msg = body.msg || '操作失败'
|
||||
if (!silent) uni.showToast({ title: msg, icon: 'none' })
|
||||
const err = new Error(msg)
|
||||
err.code = body.code
|
||||
err.response = body
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
resolve(typeof body.data !== 'undefined' ? body.data : body)
|
||||
},
|
||||
fail(err) {
|
||||
const detail = (err && err.errMsg) || ''
|
||||
console.error('[request fail]', fullUrl, detail)
|
||||
if (!silent) {
|
||||
uni.showToast({
|
||||
title: detail.includes('timeout') ? '请求超时' : '网络异常,请检查接口地址',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
reject(new Error(detail || '网络异常'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 返回完整响应体(含 code/msg),用于需要自行判断 code 的场景 */
|
||||
export function requestRaw(options = {}) {
|
||||
const { url, method = 'GET', data, header = {} } = options
|
||||
const token = getToken()
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...header
|
||||
}
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const fullUrl = BASE_URL.replace(/\/$/, '') + url
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: fullUrl,
|
||||
method: method.toUpperCase(),
|
||||
data: data || {},
|
||||
header: headers,
|
||||
timeout: 30000,
|
||||
success(res) {
|
||||
resolve(res.data || {})
|
||||
},
|
||||
fail(err) {
|
||||
console.error('[requestRaw fail]', fullUrl, err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function generateId() {
|
||||
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
"uniStatistics": {
|
||||
"enable": false,
|
||||
"debug": false
|
||||
},
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
@@ -22,6 +26,7 @@
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"usesCleartextTraffic" : true,
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
|
||||
@@ -16,6 +16,27 @@
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/register",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "注册"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/forget",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "忘记密码"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/geetest-webview",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "人机验证"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dashboard/dashboard",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<view class="page-inner">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<FaIcon name="chevron-left" color="#303133" :size="18" />
|
||||
<text class="nav-text">返回</text>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">忘记密码</text>
|
||||
<text class="subtitle">
|
||||
{{ currentStep === 1 ? '验证您的账号' : currentStep === 2 ? '通过手机号验证身份' : '设置新密码' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<!-- 第一步:验证租户和账号 -->
|
||||
<view v-if="currentStep === 1" class="form">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.tenant_name"
|
||||
placeholder="租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.account"
|
||||
placeholder="账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyAccount">
|
||||
{{ loading ? '验证中...' : '下一步' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二步:验证手机号并发送验证码 -->
|
||||
<view v-if="currentStep === 2" class="form">
|
||||
<view class="info-box">
|
||||
<text class="info-label">租户</text>
|
||||
<text class="info-value">{{ form.tenant_name }}</text>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
<text class="info-label">账号</text>
|
||||
<text class="info-value">{{ form.account }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="verifyResult.phone" class="field">
|
||||
<u-input
|
||||
v-model="form.phone"
|
||||
placeholder="手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="empty-contact">
|
||||
<text>账号未绑定手机号,请联系管理员</text>
|
||||
</view>
|
||||
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="form.sms_code"
|
||||
placeholder="短信验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || codeLoading || !verifyResult.phone }"
|
||||
@tap="handleSendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s` : (codeLoading ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyPhone">
|
||||
{{ loading ? '验证中...' : '下一步' }}
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<text class="link-text" @tap="currentStep = 1">返回上一步</text>
|
||||
<text class="link-text" @tap="goBack">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三步:重置密码 -->
|
||||
<view v-if="currentStep === 3" class="form">
|
||||
<view class="info-box">
|
||||
<text class="info-label">租户</text>
|
||||
<text class="info-value">{{ form.tenant_name }}</text>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
<text class="info-label">账号</text>
|
||||
<text class="info-value">{{ form.account }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.new_password"
|
||||
placeholder="新密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.confirm_password"
|
||||
placeholder="确认新密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleResetPassword">
|
||||
{{ loading ? '提交中...' : '重置密码' }}
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<text class="link-text" @tap="currentStep = 2">返回上一步</text>
|
||||
<text class="link-text" @tap="goBack">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onUnmounted } from 'vue'
|
||||
import { verifyAccount, resetPassword, sendResetCode } from '@/api/auth.js'
|
||||
import { setTenantName, getTenantName } from '@/utils/auth.js'
|
||||
|
||||
const currentStep = ref(1)
|
||||
const loading = ref(false)
|
||||
const codeLoading = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
const form = reactive({
|
||||
tenant_name: getTenantName() || '',
|
||||
account: '',
|
||||
phone: '',
|
||||
sms_code: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const verifyResult = reactive({
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const inputStyle = {
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0 8rpx',
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 第一步:验证租户和账号
|
||||
async function handleVerifyAccount() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim()) {
|
||||
uni.showToast({ title: '请填写租户和账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await verifyAccount({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim()
|
||||
})
|
||||
verifyResult.phone = data?.phone || ''
|
||||
verifyResult.email = data?.email || ''
|
||||
if (!verifyResult.phone && !verifyResult.email) {
|
||||
uni.showToast({ title: '账号未绑定验证方式', icon: 'none' })
|
||||
return
|
||||
}
|
||||
form.phone = verifyResult.phone || ''
|
||||
currentStep.value = 2
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:发送验证码
|
||||
async function handleSendCode() {
|
||||
if (codeLoading.value || countdown.value > 0) return
|
||||
if (!form.phone.trim()) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone)) {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
codeLoading.value = true
|
||||
try {
|
||||
await sendResetCode({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim(),
|
||||
channel: 'sms'
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
codeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步验证:验证手机和验证码
|
||||
async function handleVerifyPhone() {
|
||||
if (loading.value) return
|
||||
if (!form.phone.trim()) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.sms_code.trim()) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 后端的验证码验证在重置密码时进行,这里只是提交到第三步
|
||||
currentStep.value = 3
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第三步:重置密码
|
||||
async function handleResetPassword() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim() || !form.phone.trim()) {
|
||||
uni.showToast({ title: '请填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.new_password) {
|
||||
uni.showToast({ title: '请输入新密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.new_password !== form.confirm_password) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.new_password.length < 6) {
|
||||
uni.showToast({ title: '密码长度不能少于6个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.sms_code.trim()) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await resetPassword({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim(),
|
||||
sms_code: form.sms_code.trim(),
|
||||
new_password: form.new_password,
|
||||
confirm_password: form.confirm_password
|
||||
})
|
||||
setTenantName(form.tenant_name.trim())
|
||||
uni.showToast({ title: '重置成功,请登录', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 48rpx;
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 24rpx);
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding-left: 16rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: $color-text;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-contact {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-top: 12rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 16rpx 0 4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<web-view :src="webviewSrc" @message="onMessage" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const webviewSrc = ref('')
|
||||
let eventChannel = null
|
||||
|
||||
onLoad((query) => {
|
||||
const captchaId = decodeURIComponent(query.captchaId || '')
|
||||
webviewSrc.value = `/static/html/geetest-captcha.html?captchaId=${encodeURIComponent(captchaId)}`
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1]
|
||||
eventChannel = page.getOpenerEventChannel?.()
|
||||
})
|
||||
|
||||
function onMessage(e) {
|
||||
const payload = (e.detail && e.detail.data && e.detail.data[0]) || {}
|
||||
if (payload.type === 'success') {
|
||||
eventChannel?.emit('geetestSuccess', payload.result || {})
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
eventChannel?.emit('geetestFail', payload.msg || '人机验证未通过')
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
+316
-152
@@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="page-inner">
|
||||
<!-- 品牌区 -->
|
||||
<view class="header">
|
||||
<view class="logo">
|
||||
<text class="logo-char">云</text>
|
||||
</view>
|
||||
<text class="title">欢迎回来</text>
|
||||
<text class="subtitle">登录云泽,开启你的旅程</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录方式切换 -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab"
|
||||
@@ -24,9 +19,19 @@
|
||||
>账号登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="form-card">
|
||||
<view class="form">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="tenantName"
|
||||
placeholder="请输入租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<template v-if="loginType === 'phone'">
|
||||
<view class="field">
|
||||
<u-input
|
||||
@@ -46,7 +51,7 @@
|
||||
</view>
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="code"
|
||||
v-model="smsCode"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
@@ -57,9 +62,9 @@
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 }"
|
||||
@tap="sendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : '获取验证码' }}</text>
|
||||
:class="{ disabled: countdown > 0 || sendingCode }"
|
||||
@tap="sendPhoneCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : (sendingCode ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -85,14 +90,46 @@
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<!-- 后台开启短信/邮箱登录校验时展示 -->
|
||||
<view v-if="needLoginCode" class="field field-row">
|
||||
<u-input
|
||||
v-model="verifyCode"
|
||||
placeholder="请输入登录验证码"
|
||||
type="number"
|
||||
maxlength="8"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || sendingCode }"
|
||||
@tap="sendAccountVerifyCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : (sendingCode ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
<view v-if="verifyHint" class="verify-hint">
|
||||
<text>{{ verifyHint }}</text>
|
||||
</view>
|
||||
<view class="remember-row" @tap="rememberMe = !rememberMe">
|
||||
<view class="remember-dot" :class="{ on: rememberMe }">
|
||||
<text v-if="rememberMe" class="remember-check">✓</text>
|
||||
</view>
|
||||
<text class="remember-text">记住我</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="btn-primary" @tap="handleLogin">登 录</view>
|
||||
<view class="btn-ghost" @tap="handleTestLogin">测试登录</view>
|
||||
<view class="btn-primary" :class="{ disabled: submitting }" @tap="handleLogin">
|
||||
{{ submitting ? '登录中...' : '登 录' }}
|
||||
</view>
|
||||
|
||||
<view class="link-row">
|
||||
<text class="link" @tap="goRegister">注册账号</text>
|
||||
<text class="link" @tap="goForget">忘记密码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="agreement" @tap="agreed = !agreed">
|
||||
<view class="agree-dot" :class="{ on: agreed }">
|
||||
<text v-if="agreed" class="agree-check">✓</text>
|
||||
@@ -104,37 +141,43 @@
|
||||
<text class="agree-link">《隐私政策》</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 第三方 -->
|
||||
<view class="oauth">
|
||||
<text class="oauth-label">其他方式</text>
|
||||
<view class="oauth-icons">
|
||||
<view class="oauth-btn" @tap="socialLogin('wechat')">
|
||||
<FaIcon name="weixin" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
<view class="oauth-btn" @tap="socialLogin('qq')">
|
||||
<FaIcon name="qq" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
<view class="oauth-btn" @tap="socialLogin('alipay')">
|
||||
<FaIcon name="alipay" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { loginSuccess, isLoggedIn } from '@/utils/auth.js'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import {
|
||||
login,
|
||||
loginBySms,
|
||||
sendLoginCode,
|
||||
fetchVerifyConfig
|
||||
} from '@/api/auth.js'
|
||||
import {
|
||||
loginSuccess,
|
||||
isLoggedIn,
|
||||
setTenantName,
|
||||
getTenantName,
|
||||
getRememberLogin,
|
||||
saveRememberLogin,
|
||||
clearRememberLogin
|
||||
} from '@/utils/auth.js'
|
||||
import { showGeetest4 } from '@/utils/geetest.js'
|
||||
|
||||
const loginType = ref('phone')
|
||||
const loginType = ref('account')
|
||||
const tenantName = ref('')
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const smsCode = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const verifyCode = ref('')
|
||||
const agreed = ref(true)
|
||||
const rememberMe = ref(false)
|
||||
const countdown = ref(0)
|
||||
const sendingCode = ref(false)
|
||||
const submitting = ref(false)
|
||||
const openVerify = ref(false)
|
||||
const verifyType = ref('')
|
||||
let timer = null
|
||||
|
||||
const inputStyle = {
|
||||
@@ -143,57 +186,199 @@ const inputStyle = {
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
onMounted(() => {
|
||||
if (isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}
|
||||
const needLoginCode = computed(() => {
|
||||
return openVerify.value && (verifyType.value === 'sms' || verifyType.value === 'email')
|
||||
})
|
||||
|
||||
function sendCode() {
|
||||
if (countdown.value > 0) return
|
||||
const needGeetest = computed(() => {
|
||||
return loginType.value === 'account' && !needLoginCode.value
|
||||
})
|
||||
|
||||
const verifyHint = computed(() => {
|
||||
if (!openVerify.value) return ''
|
||||
if (verifyType.value === 'sms') return '已开启短信验证,请先获取验证码'
|
||||
if (verifyType.value === 'email') return '已开启邮箱验证,请先获取验证码'
|
||||
return ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
return
|
||||
}
|
||||
const remembered = getRememberLogin()
|
||||
if (remembered.rememberMe) {
|
||||
rememberMe.value = true
|
||||
tenantName.value = remembered.tenantName
|
||||
username.value = remembered.account
|
||||
password.value = remembered.password
|
||||
} else {
|
||||
tenantName.value = getTenantName()
|
||||
}
|
||||
const cfg = await fetchVerifyConfig()
|
||||
openVerify.value = cfg.openVerify
|
||||
verifyType.value = cfg.verifyType
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function sendPhoneCode() {
|
||||
if (countdown.value > 0 || sendingCode.value) return
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) clearInterval(timer)
|
||||
}, 1000)
|
||||
sendingCode.value = true
|
||||
try {
|
||||
await sendLoginCode({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: phone.value.trim(),
|
||||
channel: 'sms'
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// toast 已在 request 中处理
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAccountVerifyCode() {
|
||||
if (countdown.value > 0 || sendingCode.value) return
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const channel = verifyType.value === 'email' ? 'email' : 'sms'
|
||||
sendingCode.value = true
|
||||
try {
|
||||
await sendLoginCode({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
channel
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function navigateAfterLogin() {
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}, 500)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function handleTestLogin() {
|
||||
loginSuccess({ nickname: '测试用户', phone: '13800000000' })
|
||||
navigateAfterLogin()
|
||||
async function submitAccountLogin(geetestResult = null) {
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!password.value.trim()) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (needLoginCode.value && !verifyCode.value.trim()) {
|
||||
uni.showToast({ title: '请输入登录验证码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
const payload = {
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
password: password.value
|
||||
}
|
||||
if (needLoginCode.value) {
|
||||
payload.code = verifyCode.value.trim()
|
||||
}
|
||||
if (geetestResult) {
|
||||
Object.assign(payload, geetestResult)
|
||||
}
|
||||
|
||||
const data = await login(payload)
|
||||
setTenantName(tenantName.value.trim())
|
||||
loginSuccess({
|
||||
token: data.token,
|
||||
user: data.user
|
||||
})
|
||||
if (rememberMe.value) {
|
||||
saveRememberLogin({
|
||||
tenantName: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
password: password.value
|
||||
})
|
||||
} else {
|
||||
clearRememberLogin()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
async function handleLogin() {
|
||||
if (submitting.value) return
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (loginType.value === 'phone') {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code.value || code.value.length < 4) {
|
||||
if (!smsCode.value || smsCode.value.length < 4) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loginSuccess({ phone: phone.value, nickname: '用户' + phone.value.slice(-4) })
|
||||
} else {
|
||||
const data = await loginBySms({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
phone: phone.value.trim(),
|
||||
code: smsCode.value.trim()
|
||||
})
|
||||
setTenantName(tenantName.value.trim())
|
||||
loginSuccess({
|
||||
token: data.token,
|
||||
user: data.user || { phone: phone.value, nickname: '用户' + phone.value.slice(-4) }
|
||||
})
|
||||
navigateAfterLogin()
|
||||
return
|
||||
}
|
||||
|
||||
if (needGeetest.value) {
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return
|
||||
@@ -202,22 +387,34 @@ function handleLogin() {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loginSuccess({ nickname: username.value })
|
||||
try {
|
||||
const geetestResult = await showGeetest4()
|
||||
const ok = await submitAccountLogin(geetestResult)
|
||||
if (ok) navigateAfterLogin()
|
||||
} catch (err) {
|
||||
const msg = err?.message || '人机验证失败'
|
||||
if (msg !== '人机验证未通过') {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
}
|
||||
navigateAfterLogin()
|
||||
}
|
||||
|
||||
function socialLogin(type) {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const names = { wechat: '微信', qq: 'QQ', alipay: '支付宝' }
|
||||
loginSuccess({ nickname: names[type] + '用户' })
|
||||
uni.showToast({ title: `${names[type]}登录成功`, icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}, 500)
|
||||
|
||||
const ok = await submitAccountLogin()
|
||||
if (ok) navigateAfterLogin()
|
||||
} catch {
|
||||
// toast 已处理
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goRegister() {
|
||||
uni.navigateTo({ url: '/pages/login/register' })
|
||||
}
|
||||
|
||||
function goForget() {
|
||||
uni.navigateTo({ url: '/pages/login/forget' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -238,23 +435,6 @@ function socialLogin(type) {
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: $radius-lg;
|
||||
background: $color-primary-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.logo-char {
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
@@ -296,9 +476,6 @@ function socialLogin(type) {
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
@@ -339,6 +516,47 @@ function socialLogin(type) {
|
||||
}
|
||||
}
|
||||
|
||||
.verify-hint {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
line-height: 1.5;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.remember-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.remember-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 1rpx solid $color-border;
|
||||
background: $color-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.on {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.remember-check {
|
||||
font-size: 18rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
@@ -354,22 +572,21 @@ function socialLogin(type) {
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
height: 96rpx;
|
||||
background: $color-card;
|
||||
border: 1rpx solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
.link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 4rpx 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
.link {
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
@@ -377,6 +594,7 @@ function socialLogin(type) {
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
margin-top: 32rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.agree-dot {
|
||||
@@ -411,58 +629,4 @@ function socialLogin(type) {
|
||||
.agree-link {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.oauth {
|
||||
margin-top: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.oauth-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
position: relative;
|
||||
padding: 0 32rpx;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 80rpx;
|
||||
height: 1rpx;
|
||||
background: $color-border;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-icons {
|
||||
display: flex;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<view class="page-inner">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<FaIcon name="chevron-left" color="#303133" :size="18" />
|
||||
<text class="nav-text">返回</text>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">注册账号</text>
|
||||
<text class="subtitle">按租户创建管理员账号</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.tenant_name"
|
||||
placeholder="租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.account"
|
||||
placeholder="账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.name"
|
||||
placeholder="姓名"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.phone"
|
||||
placeholder="手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="form.sms_code"
|
||||
placeholder="短信验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || codeLoading }"
|
||||
@tap="handleSendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s` : (codeLoading ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.email"
|
||||
placeholder="邮箱(可选)"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.password"
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.confirm_password"
|
||||
placeholder="确认密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleSubmit">
|
||||
{{ loading ? '提交中...' : '注 册' }}
|
||||
</view>
|
||||
<view class="footer-link" @tap="goBack">
|
||||
<text>已有账号?去登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onUnmounted } from 'vue'
|
||||
import { register, sendRegisterCode } from '@/api/auth.js'
|
||||
import { setTenantName } from '@/utils/auth.js'
|
||||
|
||||
const loading = ref(false)
|
||||
const codeLoading = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
const form = reactive({
|
||||
tenant_name: '',
|
||||
account: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
sms_code: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const inputStyle = {
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0 8rpx',
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSendCode() {
|
||||
if (codeLoading.value || countdown.value > 0) return
|
||||
if (!form.tenant_name || !form.account || !form.phone) {
|
||||
uni.showToast({ title: '请先填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
codeLoading.value = true
|
||||
try {
|
||||
await sendRegisterCode({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim()
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
codeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim() || !form.phone.trim()) {
|
||||
uni.showToast({ title: '请填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.password) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.password !== form.confirm_password) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await register({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
name: form.name.trim(),
|
||||
phone: form.phone.trim(),
|
||||
sms_code: form.sms_code.trim(),
|
||||
email: form.email.trim(),
|
||||
password: form.password,
|
||||
confirm_password: form.confirm_password
|
||||
})
|
||||
setTenantName(form.tenant_name.trim())
|
||||
uni.showToast({ title: '注册成功,请登录', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}, 500)
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 48rpx;
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 24rpx);
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding-left: 16rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-top: 12rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 16rpx 0 4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -6,8 +6,8 @@
|
||||
<text class="avatar-text">{{ avatarText }}</text>
|
||||
</view>
|
||||
<view class="profile-detail">
|
||||
<text class="profile-name">{{ user?.nickname || '云泽用户' }}</text>
|
||||
<text class="profile-id">ID: {{ userId }}</text>
|
||||
<text class="profile-name">{{ user?.nickname || user?.name || '云泽用户' }}</text>
|
||||
<text class="profile-id">{{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }}</text>
|
||||
</view>
|
||||
<view class="profile-edit" @tap="onEdit">
|
||||
<FaIcon name="pen-to-square" color="#ffffff" :size="16" />
|
||||
@@ -43,7 +43,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" @tap="handleLogout">
|
||||
<view class="logout-btn" hover-class="logout-btn-hover" @tap.stop="handleLogout">
|
||||
<text class="logout-text">退出登录</text>
|
||||
</view>
|
||||
|
||||
@@ -56,20 +56,24 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUser, logout, isLoggedIn } from '@/utils/auth.js'
|
||||
import { getUser, setUser, logout, isLoggedIn } from '@/utils/auth.js'
|
||||
import { getCurrentUser, logoutApi } from '@/api/auth.js'
|
||||
import AppTabbar from '@/components/AppTabbar.vue'
|
||||
|
||||
const user = ref(null)
|
||||
|
||||
const avatarText = computed(() => {
|
||||
const name = user.value?.nickname || '云'
|
||||
const name = user.value?.nickname || user.value?.name || '云'
|
||||
return name.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
const userId = computed(() => {
|
||||
return user.value?.phone
|
||||
? user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
: '10086'
|
||||
if (user.value?.phone) {
|
||||
return user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
if (user.value?.account) return user.value.account
|
||||
if (user.value?.id) return String(user.value.id)
|
||||
return '-'
|
||||
})
|
||||
|
||||
const profileStats = ref([
|
||||
@@ -96,12 +100,32 @@ const menuGroups = ref([
|
||||
]
|
||||
])
|
||||
|
||||
async function loadUser() {
|
||||
user.value = getUser()
|
||||
try {
|
||||
const data = await getCurrentUser()
|
||||
if (data) {
|
||||
const prev = getUser() || {}
|
||||
const merged = {
|
||||
...prev,
|
||||
...data,
|
||||
nickname: data.name || data.nickname || data.account || prev.nickname || '云泽用户'
|
||||
}
|
||||
setUser(merged)
|
||||
user.value = merged
|
||||
}
|
||||
} catch {
|
||||
// 401 时 request 会跳转登录
|
||||
user.value = getUser()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
user.value = getUser()
|
||||
loadUser()
|
||||
})
|
||||
|
||||
function onEdit() {
|
||||
@@ -109,6 +133,10 @@ function onEdit() {
|
||||
}
|
||||
|
||||
function onMenuTap(item) {
|
||||
if (item.title === '账号安全') {
|
||||
uni.navigateTo({ url: '/pages/login/forget' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: item.title, icon: 'none' })
|
||||
}
|
||||
|
||||
@@ -116,12 +144,16 @@ function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmText: '退出',
|
||||
cancelText: '取消',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
if (!res.confirm) return
|
||||
// 先清本地并跳转,避免接口超时/失败导致“退出无效”
|
||||
logout()
|
||||
// 后端无状态退出,失败可忽略(不阻塞)
|
||||
logoutApi().catch(() => {})
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -289,18 +321,20 @@ function handleLogout() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
.logout-btn-hover {
|
||||
background: $color-bg-page !important;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-secondary;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 24rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<!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>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="./gt4.js"></script>
|
||||
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.4.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
var params = new URLSearchParams(window.location.search)
|
||||
var captchaId = params.get('captchaId') || ''
|
||||
|
||||
function postToUni(payload) {
|
||||
if (window.uni && typeof uni.postMessage === 'function') {
|
||||
uni.postMessage({ data: payload })
|
||||
}
|
||||
}
|
||||
|
||||
function closePage() {
|
||||
if (window.uni && typeof uni.navigateBack === 'function') {
|
||||
setTimeout(function () { uni.navigateBack() }, 120)
|
||||
}
|
||||
}
|
||||
|
||||
if (!captchaId || typeof initGeetest4 !== 'function') {
|
||||
postToUni({ type: 'fail', msg: '极验初始化失败' })
|
||||
closePage()
|
||||
return
|
||||
}
|
||||
|
||||
initGeetest4({
|
||||
captchaId: captchaId,
|
||||
product: 'bind',
|
||||
language: 'zh-CN'
|
||||
}, function (instance) {
|
||||
instance.onSuccess(function () {
|
||||
var result = instance.getValidate() || {}
|
||||
postToUni({
|
||||
type: 'success',
|
||||
result: {
|
||||
captcha_id: result.captcha_id || captchaId,
|
||||
lot_number: result.lot_number || '',
|
||||
pass_token: result.pass_token || '',
|
||||
gen_time: result.gen_time || '',
|
||||
captcha_output: result.captcha_output || ''
|
||||
}
|
||||
})
|
||||
closePage()
|
||||
})
|
||||
instance.onFail(function () {
|
||||
postToUni({ type: 'fail', msg: '人机验证未通过' })
|
||||
closePage()
|
||||
})
|
||||
instance.onError(function () {
|
||||
postToUni({ type: 'fail', msg: '人机验证加载失败' })
|
||||
closePage()
|
||||
})
|
||||
instance.showCaptcha()
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,487 @@
|
||||
"v4.2.0 Geetest Inc.";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('Geetest requires browser environment');
|
||||
}
|
||||
|
||||
var document = window.document;
|
||||
var Math = window.Math;
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var TIMEOUT = 10000;
|
||||
|
||||
function _Object(obj) {
|
||||
this._obj = obj;
|
||||
}
|
||||
|
||||
_Object.prototype = {
|
||||
_each: function (process) {
|
||||
var _obj = this._obj;
|
||||
for (var k in _obj) {
|
||||
if (_obj.hasOwnProperty(k)) {
|
||||
process(k, _obj[k]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
_extend: function (obj){
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value){
|
||||
self._obj[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var uuid = function () {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0;
|
||||
var v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
function Config(config) {
|
||||
var self = this;
|
||||
new _Object(config)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
Config.prototype = {
|
||||
apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'],
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
protocol: 'http://',
|
||||
typePath: '/load',
|
||||
fallback_config: {
|
||||
bypass: {
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
type: 'bypass',
|
||||
bypass: '/v4/bypass.js'
|
||||
}
|
||||
},
|
||||
_get_fallback_config: function () {
|
||||
var self = this;
|
||||
if (isString(self.type)) {
|
||||
return self.fallback_config[self.type];
|
||||
} else {
|
||||
return self.fallback_config.bypass;
|
||||
}
|
||||
},
|
||||
_extend: function (obj) {
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
var isNumber = function (value) {
|
||||
return (typeof value === 'number');
|
||||
};
|
||||
var isString = function (value) {
|
||||
return (typeof value === 'string');
|
||||
};
|
||||
var isBoolean = function (value) {
|
||||
return (typeof value === 'boolean');
|
||||
};
|
||||
var isObject = function (value) {
|
||||
return (typeof value === 'object' && value !== null);
|
||||
};
|
||||
var isFunction = function (value) {
|
||||
return (typeof value === 'function');
|
||||
};
|
||||
var MOBILE = /Mobi/i.test(navigator.userAgent);
|
||||
|
||||
var callbacks = {};
|
||||
var status = {};
|
||||
|
||||
var random = function () {
|
||||
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
|
||||
};
|
||||
|
||||
// bind 函数polify, ä¸å¸¦new功能的bind
|
||||
|
||||
var bind = function(target,context){
|
||||
if(typeof target !== 'function'){
|
||||
return;
|
||||
}
|
||||
var args = Array.prototype.slice.call(arguments,2);
|
||||
|
||||
if(Function.prototype.bind){
|
||||
return target.bind(context, args);
|
||||
}else {
|
||||
return function(){
|
||||
var _args = Array.prototype.slice.call(arguments);
|
||||
return target.apply(context,args.concat(_args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
var _isFunction = function(obj) {
|
||||
return typeof(obj) === 'function';
|
||||
};
|
||||
var _isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
var _isArray = function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
var _isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
var _isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
var _isBoolean = function(obj) {
|
||||
return toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
|
||||
function resolveKey(input){
|
||||
return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){
|
||||
return $1 + $3.toUpperCase() || "";
|
||||
})
|
||||
}
|
||||
|
||||
function camelizeKeys(input, convert){
|
||||
if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){
|
||||
return convert ? resolveKey(input) : input;
|
||||
}
|
||||
|
||||
if(_isArray(input)){
|
||||
var temp = [];
|
||||
for(var i = 0; i < input.length; i++){
|
||||
temp.push(camelizeKeys(input[i]));
|
||||
}
|
||||
|
||||
}else {
|
||||
var temp = {};
|
||||
for(var prop in input){
|
||||
if(input.hasOwnProperty(prop)){
|
||||
temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
var loadScript = function (url, cb, timeout) {
|
||||
var script = document.createElement("script");
|
||||
script.charset = "UTF-8";
|
||||
script.async = true;
|
||||
|
||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
||||
if ( /static\.geetest\.com/g.test(url)) {
|
||||
script.crossOrigin = "anonymous";
|
||||
}
|
||||
|
||||
script.onerror = function () {
|
||||
cb(true);
|
||||
// 错误触å‘了,超时逻辑就ä¸ç”¨äº†
|
||||
loaded = true;
|
||||
};
|
||||
var loaded = false;
|
||||
script.onload = script.onreadystatechange = function () {
|
||||
if (!loaded &&
|
||||
(!script.readyState ||
|
||||
"loaded" === script.readyState ||
|
||||
"complete" === script.readyState)) {
|
||||
|
||||
loaded = true;
|
||||
setTimeout(function () {
|
||||
cb(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
head.appendChild(script);
|
||||
|
||||
setTimeout(function () {
|
||||
if (!loaded) {
|
||||
script.onerror = script.onload = null;
|
||||
script.remove && script.remove();
|
||||
cb(true);
|
||||
}
|
||||
}, timeout || TIMEOUT);
|
||||
};
|
||||
|
||||
var normalizeDomain = function (domain) {
|
||||
// special domain: uems.sysu.edu.cn/jwxt/geetest/
|
||||
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
|
||||
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
|
||||
};
|
||||
var normalizePath = function (path) {
|
||||
|
||||
path = path && path.replace(/\/+/g, '/');
|
||||
if (path.indexOf('/') !== 0) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
var normalizeQuery = function (query) {
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
var q = '?';
|
||||
new _Object(query)._each(function (key, value) {
|
||||
if (isString(value) || isNumber(value) || isBoolean(value)) {
|
||||
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
|
||||
}
|
||||
});
|
||||
if (q === '?') {
|
||||
q = '';
|
||||
}
|
||||
return q.replace(/&$/, '');
|
||||
};
|
||||
var makeURL = function (protocol, domain, path, query) {
|
||||
domain = normalizeDomain(domain);
|
||||
|
||||
var url = normalizePath(path) + normalizeQuery(query);
|
||||
if (domain) {
|
||||
url = protocol + domain + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var load = function (config, protocol, domains, path, query, cb, handleCb) {
|
||||
var tryRequest = function (at) {
|
||||
// 处ç†jsonp回调,这里为了ä¿è¯æ¯ä¸ªä¸åŒjsonp都有唯一的回调函数
|
||||
if(handleCb){
|
||||
var cbName = "geetest_" + random();
|
||||
// 需è¦ä¸Žé¢„先定义好cbname傿•°ï¼Œåˆ 除对象
|
||||
window[cbName] = bind(handleCb, null, cbName);
|
||||
query.callback = cbName;
|
||||
}
|
||||
var url = makeURL(protocol, domains[at], path, query);
|
||||
loadScript(url, function (err) {
|
||||
if (err) {
|
||||
// 超时或者出错的时候 移除回调
|
||||
if(cbName){
|
||||
try {
|
||||
window[cbName] = function(){
|
||||
window[cbName] = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (at >= domains.length - 1) {
|
||||
cb(true);
|
||||
// report gettype error
|
||||
} else {
|
||||
tryRequest(at + 1);
|
||||
}
|
||||
} else {
|
||||
cb(false);
|
||||
}
|
||||
}, config.timeout);
|
||||
};
|
||||
tryRequest(0);
|
||||
};
|
||||
|
||||
|
||||
var jsonp = function (domains, path, config, callback) {
|
||||
|
||||
var handleCb = function (cbName, data) {
|
||||
|
||||
// ä¿è¯åªæ‰§è¡Œä¸€æ¬¡ï¼Œå…¨éƒ¨è¶…时的情况下ä¸ä¼šå†è§¦å‘;
|
||||
|
||||
if (data.status == 'success') {
|
||||
callback(data.data);
|
||||
} else if (!data.status) {
|
||||
callback(data);
|
||||
} else {
|
||||
//æŽ¥å£æœ‰è¿”回,但是返回了错误状æ€ï¼Œè¿›å…¥æŠ¥é”™é€»è¾‘
|
||||
callback(data);
|
||||
}
|
||||
window[cbName] = undefined;
|
||||
try {
|
||||
delete window[cbName];
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
load(config, config.protocol, domains, path, {
|
||||
callback: '',
|
||||
captcha_id: config.captchaId,
|
||||
challenge: config.challenge || uuid(),
|
||||
client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'),
|
||||
risk_type: config.riskType,
|
||||
user_info: config.userInfo,
|
||||
call_type: config.callType,
|
||||
lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase()
|
||||
}, function (err) {
|
||||
// ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”回,直接使用本地验è¯ç ,走宕机模å¼
|
||||
// 这里å¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘
|
||||
if(err && typeof config.offlineCb === 'function'){
|
||||
// 执行自己的宕机
|
||||
config.offlineCb();
|
||||
return;
|
||||
}
|
||||
if(err){
|
||||
callback(config._get_fallback_config());
|
||||
}
|
||||
}, handleCb);
|
||||
};
|
||||
|
||||
var reportError = function (config, url) {
|
||||
load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
|
||||
time: Date.now().getTime(),
|
||||
captcha_id: config.gt,
|
||||
challenge: config.challenge,
|
||||
exception_url: url,
|
||||
error_code: config.error_code
|
||||
}, function (err) {})
|
||||
}
|
||||
|
||||
var throwError = function (errorType, config, errObj) {
|
||||
var errors = {
|
||||
networkError: '网络错误',
|
||||
gtTypeError: 'gtå—æ®µä¸æ˜¯å—符串类型'
|
||||
};
|
||||
if (typeof config.onError === 'function') {
|
||||
config.onError({
|
||||
desc: errObj.desc,
|
||||
msg: errObj.msg,
|
||||
code: errObj.code
|
||||
});
|
||||
} else {
|
||||
throw new Error(errors[errorType]);
|
||||
}
|
||||
};
|
||||
|
||||
var detect = function () {
|
||||
return window.Geetest || document.getElementById("gt_lib");
|
||||
};
|
||||
|
||||
if (detect()) {
|
||||
status.slide = "loaded";
|
||||
}
|
||||
var GeetestIsLoad = function (fname) {
|
||||
var GeetestIsLoad = false;
|
||||
var tags = { js: 'script', css: 'link' };
|
||||
var tagname = fname && tags[fname.split('.').pop()];
|
||||
if (tagname !== undefined) {
|
||||
var elts = document.getElementsByTagName(tagname);
|
||||
for (var i in elts) {
|
||||
if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0)
|
||||
|| (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) {
|
||||
GeetestIsLoad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return GeetestIsLoad;
|
||||
};
|
||||
window.initGeetest4 = function (userConfig,callback) {
|
||||
|
||||
var config = new Config(userConfig);
|
||||
if (userConfig.https) {
|
||||
config.protocol = 'https://';
|
||||
} else if (!userConfig.protocol) {
|
||||
config.protocol = window.location.protocol + '//';
|
||||
}
|
||||
|
||||
|
||||
if (isObject(userConfig.getType)) {
|
||||
config._extend(userConfig.getType);
|
||||
}
|
||||
|
||||
jsonp(config.apiServers , config.typePath, config, function (newConfig) {
|
||||
//错误æ•获,第一个load请求å¯èƒ½ç›´æŽ¥æŠ¥é”™
|
||||
var newConfig = camelizeKeys(newConfig);
|
||||
|
||||
if(newConfig.status === 'error'){
|
||||
return throwError('networkError', config, newConfig);
|
||||
}
|
||||
|
||||
var type = newConfig.type;
|
||||
if(config.debug){
|
||||
new _Object(newConfig)._extend(config.debug)
|
||||
}
|
||||
var init = function () {
|
||||
config._extend(newConfig);
|
||||
callback(new window.Geetest4(config));
|
||||
};
|
||||
|
||||
callbacks[type] = callbacks[type] || [];
|
||||
|
||||
var s = status[type] || 'init';
|
||||
if (s === 'init') {
|
||||
status[type] = 'loading';
|
||||
|
||||
callbacks[type].push(init);
|
||||
|
||||
if(newConfig.gctPath){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) {
|
||||
if (err) {
|
||||
status[type] = 'fail';
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
status[type] = 'loaded';
|
||||
var cbs = callbacks[type];
|
||||
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
|
||||
var cb = cbs[i];
|
||||
if (isFunction(cb)) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
callbacks[type] = [];
|
||||
status[type] = 'init';
|
||||
}
|
||||
});
|
||||
} else if (s === "loaded") {
|
||||
// 判æ–gct是å¦éœ€è¦é‡æ–°åŠ è½½
|
||||
if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
return init();
|
||||
} else if (s === "fail") {
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else if (s === "loading") {
|
||||
callbacks[type].push(init);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
})(window);
|
||||
@@ -0,0 +1,487 @@
|
||||
"v4.2.0 Geetest Inc.";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('Geetest requires browser environment');
|
||||
}
|
||||
|
||||
var document = window.document;
|
||||
var Math = window.Math;
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var TIMEOUT = 10000;
|
||||
|
||||
function _Object(obj) {
|
||||
this._obj = obj;
|
||||
}
|
||||
|
||||
_Object.prototype = {
|
||||
_each: function (process) {
|
||||
var _obj = this._obj;
|
||||
for (var k in _obj) {
|
||||
if (_obj.hasOwnProperty(k)) {
|
||||
process(k, _obj[k]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
_extend: function (obj){
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value){
|
||||
self._obj[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var uuid = function () {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0;
|
||||
var v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
function Config(config) {
|
||||
var self = this;
|
||||
new _Object(config)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
Config.prototype = {
|
||||
apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'],
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
protocol: 'http://',
|
||||
typePath: '/load',
|
||||
fallback_config: {
|
||||
bypass: {
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
type: 'bypass',
|
||||
bypass: '/v4/bypass.js'
|
||||
}
|
||||
},
|
||||
_get_fallback_config: function () {
|
||||
var self = this;
|
||||
if (isString(self.type)) {
|
||||
return self.fallback_config[self.type];
|
||||
} else {
|
||||
return self.fallback_config.bypass;
|
||||
}
|
||||
},
|
||||
_extend: function (obj) {
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
var isNumber = function (value) {
|
||||
return (typeof value === 'number');
|
||||
};
|
||||
var isString = function (value) {
|
||||
return (typeof value === 'string');
|
||||
};
|
||||
var isBoolean = function (value) {
|
||||
return (typeof value === 'boolean');
|
||||
};
|
||||
var isObject = function (value) {
|
||||
return (typeof value === 'object' && value !== null);
|
||||
};
|
||||
var isFunction = function (value) {
|
||||
return (typeof value === 'function');
|
||||
};
|
||||
var MOBILE = /Mobi/i.test(navigator.userAgent);
|
||||
|
||||
var callbacks = {};
|
||||
var status = {};
|
||||
|
||||
var random = function () {
|
||||
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
|
||||
};
|
||||
|
||||
// bind 函数polify, ä¸å¸¦new功能的bind
|
||||
|
||||
var bind = function(target,context){
|
||||
if(typeof target !== 'function'){
|
||||
return;
|
||||
}
|
||||
var args = Array.prototype.slice.call(arguments,2);
|
||||
|
||||
if(Function.prototype.bind){
|
||||
return target.bind(context, args);
|
||||
}else {
|
||||
return function(){
|
||||
var _args = Array.prototype.slice.call(arguments);
|
||||
return target.apply(context,args.concat(_args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
var _isFunction = function(obj) {
|
||||
return typeof(obj) === 'function';
|
||||
};
|
||||
var _isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
var _isArray = function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
var _isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
var _isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
var _isBoolean = function(obj) {
|
||||
return toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
|
||||
function resolveKey(input){
|
||||
return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){
|
||||
return $1 + $3.toUpperCase() || "";
|
||||
})
|
||||
}
|
||||
|
||||
function camelizeKeys(input, convert){
|
||||
if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){
|
||||
return convert ? resolveKey(input) : input;
|
||||
}
|
||||
|
||||
if(_isArray(input)){
|
||||
var temp = [];
|
||||
for(var i = 0; i < input.length; i++){
|
||||
temp.push(camelizeKeys(input[i]));
|
||||
}
|
||||
|
||||
}else {
|
||||
var temp = {};
|
||||
for(var prop in input){
|
||||
if(input.hasOwnProperty(prop)){
|
||||
temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
var loadScript = function (url, cb, timeout) {
|
||||
var script = document.createElement("script");
|
||||
script.charset = "UTF-8";
|
||||
script.async = true;
|
||||
|
||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
||||
if ( /static\.geetest\.com/g.test(url)) {
|
||||
script.crossOrigin = "anonymous";
|
||||
}
|
||||
|
||||
script.onerror = function () {
|
||||
cb(true);
|
||||
// 错误触å‘了,超时逻辑就ä¸ç”¨äº†
|
||||
loaded = true;
|
||||
};
|
||||
var loaded = false;
|
||||
script.onload = script.onreadystatechange = function () {
|
||||
if (!loaded &&
|
||||
(!script.readyState ||
|
||||
"loaded" === script.readyState ||
|
||||
"complete" === script.readyState)) {
|
||||
|
||||
loaded = true;
|
||||
setTimeout(function () {
|
||||
cb(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
head.appendChild(script);
|
||||
|
||||
setTimeout(function () {
|
||||
if (!loaded) {
|
||||
script.onerror = script.onload = null;
|
||||
script.remove && script.remove();
|
||||
cb(true);
|
||||
}
|
||||
}, timeout || TIMEOUT);
|
||||
};
|
||||
|
||||
var normalizeDomain = function (domain) {
|
||||
// special domain: uems.sysu.edu.cn/jwxt/geetest/
|
||||
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
|
||||
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
|
||||
};
|
||||
var normalizePath = function (path) {
|
||||
|
||||
path = path && path.replace(/\/+/g, '/');
|
||||
if (path.indexOf('/') !== 0) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
var normalizeQuery = function (query) {
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
var q = '?';
|
||||
new _Object(query)._each(function (key, value) {
|
||||
if (isString(value) || isNumber(value) || isBoolean(value)) {
|
||||
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
|
||||
}
|
||||
});
|
||||
if (q === '?') {
|
||||
q = '';
|
||||
}
|
||||
return q.replace(/&$/, '');
|
||||
};
|
||||
var makeURL = function (protocol, domain, path, query) {
|
||||
domain = normalizeDomain(domain);
|
||||
|
||||
var url = normalizePath(path) + normalizeQuery(query);
|
||||
if (domain) {
|
||||
url = protocol + domain + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var load = function (config, protocol, domains, path, query, cb, handleCb) {
|
||||
var tryRequest = function (at) {
|
||||
// 处ç†jsonp回调,这里为了ä¿è¯æ¯ä¸ªä¸åŒjsonp都有唯一的回调函数
|
||||
if(handleCb){
|
||||
var cbName = "geetest_" + random();
|
||||
// 需è¦ä¸Žé¢„先定义好cbname傿•°ï¼Œåˆ 除对象
|
||||
window[cbName] = bind(handleCb, null, cbName);
|
||||
query.callback = cbName;
|
||||
}
|
||||
var url = makeURL(protocol, domains[at], path, query);
|
||||
loadScript(url, function (err) {
|
||||
if (err) {
|
||||
// 超时或者出错的时候 移除回调
|
||||
if(cbName){
|
||||
try {
|
||||
window[cbName] = function(){
|
||||
window[cbName] = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (at >= domains.length - 1) {
|
||||
cb(true);
|
||||
// report gettype error
|
||||
} else {
|
||||
tryRequest(at + 1);
|
||||
}
|
||||
} else {
|
||||
cb(false);
|
||||
}
|
||||
}, config.timeout);
|
||||
};
|
||||
tryRequest(0);
|
||||
};
|
||||
|
||||
|
||||
var jsonp = function (domains, path, config, callback) {
|
||||
|
||||
var handleCb = function (cbName, data) {
|
||||
|
||||
// ä¿è¯åªæ‰§è¡Œä¸€æ¬¡ï¼Œå…¨éƒ¨è¶…时的情况下ä¸ä¼šå†è§¦å‘;
|
||||
|
||||
if (data.status == 'success') {
|
||||
callback(data.data);
|
||||
} else if (!data.status) {
|
||||
callback(data);
|
||||
} else {
|
||||
//æŽ¥å£æœ‰è¿”回,但是返回了错误状æ€ï¼Œè¿›å…¥æŠ¥é”™é€»è¾‘
|
||||
callback(data);
|
||||
}
|
||||
window[cbName] = undefined;
|
||||
try {
|
||||
delete window[cbName];
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
load(config, config.protocol, domains, path, {
|
||||
callback: '',
|
||||
captcha_id: config.captchaId,
|
||||
challenge: config.challenge || uuid(),
|
||||
client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'),
|
||||
risk_type: config.riskType,
|
||||
user_info: config.userInfo,
|
||||
call_type: config.callType,
|
||||
lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase()
|
||||
}, function (err) {
|
||||
// ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”回,直接使用本地验è¯ç ,走宕机模å¼
|
||||
// 这里å¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘
|
||||
if(err && typeof config.offlineCb === 'function'){
|
||||
// 执行自己的宕机
|
||||
config.offlineCb();
|
||||
return;
|
||||
}
|
||||
if(err){
|
||||
callback(config._get_fallback_config());
|
||||
}
|
||||
}, handleCb);
|
||||
};
|
||||
|
||||
var reportError = function (config, url) {
|
||||
load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
|
||||
time: Date.now().getTime(),
|
||||
captcha_id: config.gt,
|
||||
challenge: config.challenge,
|
||||
exception_url: url,
|
||||
error_code: config.error_code
|
||||
}, function (err) {})
|
||||
}
|
||||
|
||||
var throwError = function (errorType, config, errObj) {
|
||||
var errors = {
|
||||
networkError: '网络错误',
|
||||
gtTypeError: 'gtå—æ®µä¸æ˜¯å—符串类型'
|
||||
};
|
||||
if (typeof config.onError === 'function') {
|
||||
config.onError({
|
||||
desc: errObj.desc,
|
||||
msg: errObj.msg,
|
||||
code: errObj.code
|
||||
});
|
||||
} else {
|
||||
throw new Error(errors[errorType]);
|
||||
}
|
||||
};
|
||||
|
||||
var detect = function () {
|
||||
return window.Geetest || document.getElementById("gt_lib");
|
||||
};
|
||||
|
||||
if (detect()) {
|
||||
status.slide = "loaded";
|
||||
}
|
||||
var GeetestIsLoad = function (fname) {
|
||||
var GeetestIsLoad = false;
|
||||
var tags = { js: 'script', css: 'link' };
|
||||
var tagname = fname && tags[fname.split('.').pop()];
|
||||
if (tagname !== undefined) {
|
||||
var elts = document.getElementsByTagName(tagname);
|
||||
for (var i in elts) {
|
||||
if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0)
|
||||
|| (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) {
|
||||
GeetestIsLoad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return GeetestIsLoad;
|
||||
};
|
||||
window.initGeetest4 = function (userConfig,callback) {
|
||||
|
||||
var config = new Config(userConfig);
|
||||
if (userConfig.https) {
|
||||
config.protocol = 'https://';
|
||||
} else if (!userConfig.protocol) {
|
||||
config.protocol = window.location.protocol + '//';
|
||||
}
|
||||
|
||||
|
||||
if (isObject(userConfig.getType)) {
|
||||
config._extend(userConfig.getType);
|
||||
}
|
||||
|
||||
jsonp(config.apiServers , config.typePath, config, function (newConfig) {
|
||||
//错误æ•获,第一个load请求å¯èƒ½ç›´æŽ¥æŠ¥é”™
|
||||
var newConfig = camelizeKeys(newConfig);
|
||||
|
||||
if(newConfig.status === 'error'){
|
||||
return throwError('networkError', config, newConfig);
|
||||
}
|
||||
|
||||
var type = newConfig.type;
|
||||
if(config.debug){
|
||||
new _Object(newConfig)._extend(config.debug)
|
||||
}
|
||||
var init = function () {
|
||||
config._extend(newConfig);
|
||||
callback(new window.Geetest4(config));
|
||||
};
|
||||
|
||||
callbacks[type] = callbacks[type] || [];
|
||||
|
||||
var s = status[type] || 'init';
|
||||
if (s === 'init') {
|
||||
status[type] = 'loading';
|
||||
|
||||
callbacks[type].push(init);
|
||||
|
||||
if(newConfig.gctPath){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) {
|
||||
if (err) {
|
||||
status[type] = 'fail';
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
status[type] = 'loaded';
|
||||
var cbs = callbacks[type];
|
||||
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
|
||||
var cb = cbs[i];
|
||||
if (isFunction(cb)) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
callbacks[type] = [];
|
||||
status[type] = 'init';
|
||||
}
|
||||
});
|
||||
} else if (s === "loaded") {
|
||||
// 判æ–gct是å¦éœ€è¦é‡æ–°åŠ è½½
|
||||
if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
return init();
|
||||
} else if (s === "fail") {
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else if (s === "loading") {
|
||||
callbacks[type].push(init);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
})(window);
|
||||
+78
-10
@@ -1,8 +1,13 @@
|
||||
const TOKEN_KEY = 'app_token'
|
||||
const USER_KEY = 'app_user'
|
||||
const TENANT_KEY = 'app_tenant_name'
|
||||
const REMEMBER_KEY = 'app_remember_me'
|
||||
const REMEMBER_TENANT_KEY = 'app_remember_tenant'
|
||||
const REMEMBER_ACCOUNT_KEY = 'app_remember_account'
|
||||
const REMEMBER_PASSWORD_KEY = 'app_remember_password'
|
||||
|
||||
export function setToken(token) {
|
||||
uni.setStorageSync(TOKEN_KEY, token)
|
||||
uni.setStorageSync(TOKEN_KEY, token || '')
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
@@ -10,13 +15,21 @@ export function getToken() {
|
||||
}
|
||||
|
||||
export function setUser(user) {
|
||||
uni.setStorageSync(USER_KEY, user)
|
||||
uni.setStorageSync(USER_KEY, user || null)
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
return uni.getStorageSync(USER_KEY) || null
|
||||
}
|
||||
|
||||
export function setTenantName(name) {
|
||||
uni.setStorageSync(TENANT_KEY, name || '')
|
||||
}
|
||||
|
||||
export function getTenantName() {
|
||||
return uni.getStorageSync(TENANT_KEY) || ''
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return !!getToken()
|
||||
}
|
||||
@@ -26,12 +39,67 @@ export function logout() {
|
||||
uni.removeStorageSync(USER_KEY)
|
||||
}
|
||||
|
||||
export function loginSuccess(user = {}) {
|
||||
setToken('demo_token_' + Date.now())
|
||||
setUser({
|
||||
nickname: user.nickname || '云泽用户',
|
||||
avatar: user.avatar || '',
|
||||
phone: user.phone || '',
|
||||
...user
|
||||
})
|
||||
/** 是否开启记住账号登录信息 */
|
||||
export function isRememberLogin() {
|
||||
return uni.getStorageSync(REMEMBER_KEY) === '1'
|
||||
}
|
||||
|
||||
/** 读取记住的账号登录信息 */
|
||||
export function getRememberLogin() {
|
||||
if (!isRememberLogin()) {
|
||||
return { rememberMe: false, tenantName: '', account: '', password: '' }
|
||||
}
|
||||
return {
|
||||
rememberMe: true,
|
||||
tenantName: uni.getStorageSync(REMEMBER_TENANT_KEY) || '',
|
||||
account: uni.getStorageSync(REMEMBER_ACCOUNT_KEY) || '',
|
||||
password: uni.getStorageSync(REMEMBER_PASSWORD_KEY) || ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存账号登录信息(租户、账号、密码) */
|
||||
export function saveRememberLogin({ tenantName = '', account = '', password = '' } = {}) {
|
||||
uni.setStorageSync(REMEMBER_KEY, '1')
|
||||
uni.setStorageSync(REMEMBER_TENANT_KEY, tenantName)
|
||||
uni.setStorageSync(REMEMBER_ACCOUNT_KEY, account)
|
||||
uni.setStorageSync(REMEMBER_PASSWORD_KEY, password)
|
||||
}
|
||||
|
||||
/** 清除记住的账号登录信息 */
|
||||
export function clearRememberLogin() {
|
||||
uni.removeStorageSync(REMEMBER_KEY)
|
||||
uni.removeStorageSync(REMEMBER_TENANT_KEY)
|
||||
uni.removeStorageSync(REMEMBER_ACCOUNT_KEY)
|
||||
uni.removeStorageSync(REMEMBER_PASSWORD_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功写入 token 与用户信息
|
||||
* @param {{ token?: string, user?: object, nickname?: string, phone?: string, account?: string, name?: string, avatar?: string, id?: number|string }} payload
|
||||
*/
|
||||
export function loginSuccess(payload = {}) {
|
||||
const token = payload.token || payload.access_token || ''
|
||||
if (token) {
|
||||
setToken(token)
|
||||
}
|
||||
|
||||
const rawUser = payload.user || payload
|
||||
const user = {
|
||||
id: rawUser.id,
|
||||
account: rawUser.account || '',
|
||||
name: rawUser.name || '',
|
||||
nickname: rawUser.nickname || rawUser.name || rawUser.account || '云泽用户',
|
||||
avatar: rawUser.avatar || '',
|
||||
phone: rawUser.phone || payload.phone || '',
|
||||
tid: rawUser.tid,
|
||||
rid: rawUser.rid,
|
||||
role_name: rawUser.role_name || '',
|
||||
...rawUser
|
||||
}
|
||||
// 保证 nickname 可用
|
||||
if (!user.nickname) {
|
||||
user.nickname = user.name || user.account || '云泽用户'
|
||||
}
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 极验 4.0 — 点击登录后弹出 bind 模式验证码,成功后返回校验参数供登录接口使用。
|
||||
*/
|
||||
import { getGeetest4Infos } from '@/api/auth.js'
|
||||
import { GEETEST4_CAPTCHA_ID } from '@/api/config.js'
|
||||
|
||||
// #ifdef H5
|
||||
import '@/static/js/gt4.js'
|
||||
// #endif
|
||||
|
||||
function normalizeValidate(result, fallbackCaptchaId) {
|
||||
return {
|
||||
captcha_id: result?.captcha_id || fallbackCaptchaId,
|
||||
lot_number: result?.lot_number || '',
|
||||
pass_token: result?.pass_token || '',
|
||||
gen_time: result?.gen_time || '',
|
||||
captcha_output: result?.captcha_output || ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 优先读后端配置,失败时使用本地 captcha_id */
|
||||
export async function fetchGeetest4CaptchaId() {
|
||||
try {
|
||||
const data = await getGeetest4Infos()
|
||||
if (data?.captcha_id) return data.captcha_id
|
||||
} catch {
|
||||
// 后端未配置或未开启时走本地兜底
|
||||
}
|
||||
return GEETEST4_CAPTCHA_ID
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
function showGeetest4H5(captchaId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof window === 'undefined' || !window.initGeetest4) {
|
||||
reject(new Error('极验 SDK 未加载'))
|
||||
return
|
||||
}
|
||||
window.initGeetest4(
|
||||
{
|
||||
captchaId,
|
||||
product: 'bind',
|
||||
language: 'zh-CN'
|
||||
},
|
||||
(instance) => {
|
||||
instance.onSuccess(() => {
|
||||
resolve(normalizeValidate(instance.getValidate(), captchaId))
|
||||
if (typeof instance.destroy === 'function') {
|
||||
instance.destroy()
|
||||
}
|
||||
})
|
||||
instance.onFail(() => {
|
||||
reject(new Error('人机验证未通过'))
|
||||
})
|
||||
instance.onError(() => {
|
||||
reject(new Error('人机验证加载失败'))
|
||||
})
|
||||
instance.showCaptcha()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
function showGeetest4App(captchaId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/geetest-webview?captchaId=${encodeURIComponent(captchaId)}`,
|
||||
events: {
|
||||
geetestSuccess(data) {
|
||||
resolve(normalizeValidate(data, captchaId))
|
||||
},
|
||||
geetestFail(msg) {
|
||||
reject(new Error(msg || '人机验证未通过'))
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
reject(new Error(err?.errMsg || '无法打开验证页面'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 弹出极验 4.0 验证
|
||||
* @returns {Promise<{ captcha_id, lot_number, pass_token, gen_time, captcha_output }>}
|
||||
*/
|
||||
export async function showGeetest4() {
|
||||
const captchaId = await fetchGeetest4CaptchaId()
|
||||
if (!captchaId) {
|
||||
throw new Error('未配置极验 captcha_id')
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
return showGeetest4H5(captchaId)
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
return showGeetest4App(captchaId)
|
||||
// #endif
|
||||
|
||||
// #ifndef H5 || APP-PLUS
|
||||
throw new Error('当前平台暂不支持极验验证,请使用 H5 或 App')
|
||||
// #endif
|
||||
}
|
||||
+21
-8
@@ -1,16 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
uni()
|
||||
],
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const apiTarget = (env.VITE_API_BASE_URL || 'http://localhost:9000').replace(/\/$/, '')
|
||||
|
||||
return {
|
||||
plugins: [uni()],
|
||||
server: {
|
||||
proxy: {
|
||||
// 勿用 /api:会与 uniapp/api/ 源码目录冲突,导致 request.js 等模块 404
|
||||
'/proxy-api': {
|
||||
target: apiTarget,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/proxy-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// 关闭废弃 API 警告
|
||||
silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'],
|
||||
silenceDeprecations: ['legacy-js-api', 'color-functions', 'import']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user