mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-30 15:04:30 +08:00
微信登录验证完成
This commit is contained in:
@@ -129,8 +129,8 @@ func (h *OrderHandler) Clear(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
deleteIds := make([]uint, 0)
|
deleteIds := make([]uint, 0)
|
||||||
for _, order := range orders {
|
for _, order := range orders {
|
||||||
// 只删除 15 分钟内的未支付订单
|
// 只删除 5 分钟内的未支付订单
|
||||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
|
if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) {
|
||||||
deleteIds = append(deleteIds, order.Id)
|
deleteIds = append(deleteIds, order.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (h *PaymentHandler) SyncOrders() error {
|
|||||||
for _, order := range orders {
|
for _, order := range orders {
|
||||||
time.Sleep(time.Second * 1)
|
time.Sleep(time.Second * 1)
|
||||||
//超时15分钟的订单,直接标记为已关闭
|
//超时15分钟的订单,直接标记为已关闭
|
||||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
|
if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) {
|
||||||
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)
|
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)
|
||||||
logger.Errorf("订单超时:%v", order)
|
logger.Errorf("订单超时:%v", order)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package handler
|
|||||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geekai/core"
|
"geekai/core"
|
||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
@@ -37,6 +38,8 @@ type UserHandler struct {
|
|||||||
licenseService *service.LicenseService
|
licenseService *service.LicenseService
|
||||||
captchaService *service.CaptchaService
|
captchaService *service.CaptchaService
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
wxLoginService *service.WxLoginService
|
||||||
|
ipSearcher *xdb.Searcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserHandler(
|
func NewUserHandler(
|
||||||
@@ -47,6 +50,8 @@ func NewUserHandler(
|
|||||||
levelDB *store.LevelDB,
|
levelDB *store.LevelDB,
|
||||||
captcha *service.CaptchaService,
|
captcha *service.CaptchaService,
|
||||||
userService *service.UserService,
|
userService *service.UserService,
|
||||||
|
wxLoginService *service.WxLoginService,
|
||||||
|
ipSearcher *xdb.Searcher,
|
||||||
licenseService *service.LicenseService) *UserHandler {
|
licenseService *service.LicenseService) *UserHandler {
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
BaseHandler: BaseHandler{DB: db, App: app},
|
BaseHandler: BaseHandler{DB: db, App: app},
|
||||||
@@ -56,6 +61,8 @@ func NewUserHandler(
|
|||||||
captchaService: captcha,
|
captchaService: captcha,
|
||||||
licenseService: licenseService,
|
licenseService: licenseService,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
wxLoginService: wxLoginService,
|
||||||
|
ipSearcher: ipSearcher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +74,10 @@ func (h *UserHandler) RegisterRoutes() {
|
|||||||
group.POST("register", h.Register)
|
group.POST("register", h.Register)
|
||||||
group.POST("login", h.Login)
|
group.POST("login", h.Login)
|
||||||
group.POST("resetPass", h.ResetPass)
|
group.POST("resetPass", h.ResetPass)
|
||||||
group.GET("clogin", h.CLogin)
|
group.GET("login/qrcode", h.GetWxLoginQRCode)
|
||||||
|
group.POST("login/callback", h.WxLoginCallback)
|
||||||
|
group.GET("login/status", h.GetWxLoginState)
|
||||||
group.GET("logout", h.Logout)
|
group.GET("logout", h.Logout)
|
||||||
group.GET("clogin/callback", h.CLoginCallback)
|
|
||||||
|
|
||||||
// 需要用户授权的接口
|
// 需要用户授权的接口
|
||||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||||
@@ -150,30 +158,8 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证邀请码
|
|
||||||
inviteCode := model.InviteCode{}
|
|
||||||
if data.InviteCode != "" {
|
|
||||||
res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "无效的邀请码")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
salt := utils.RandString(8)
|
|
||||||
user := model.User{
|
|
||||||
Username: data.Username,
|
|
||||||
Password: utils.GenPassword(data.Password, salt),
|
|
||||||
Avatar: "/images/avatar/user.png",
|
|
||||||
Salt: salt,
|
|
||||||
Status: true,
|
|
||||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
|
||||||
ChatConfig: "{}",
|
|
||||||
ChatModels: "{}",
|
|
||||||
Power: h.App.SysConfig.Base.InitPower,
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the username is existing
|
// check if the username is existing
|
||||||
|
user := model.User{Username: data.Username, Password: data.Password}
|
||||||
var item model.User
|
var item model.User
|
||||||
session := h.DB.Session(&gorm.Session{})
|
session := h.DB.Session(&gorm.Session{})
|
||||||
if data.Mobile != "" {
|
if data.Mobile != "" {
|
||||||
@@ -193,78 +179,19 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 被邀请人也获得赠送算力
|
user, err := h.createNewUser(user, data.InviteCode)
|
||||||
if data.InviteCode != "" {
|
if err != nil {
|
||||||
user.Power += h.App.SysConfig.Base.InvitePower
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
|
||||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
|
||||||
} else {
|
|
||||||
defaultNickname := h.App.SysConfig.Base.DefaultNickname
|
|
||||||
if defaultNickname == "" {
|
|
||||||
defaultNickname = "极客学长"
|
|
||||||
}
|
|
||||||
user.Nickname = fmt.Sprintf("%s@%d", defaultNickname, utils.RandomNumber(6))
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := h.DB.Begin()
|
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录邀请关系
|
token, err := h.doLogin(&user, c.ClientIP())
|
||||||
if data.InviteCode != "" {
|
|
||||||
// 增加邀请数量
|
|
||||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
|
||||||
if h.App.SysConfig.Base.InvitePower > 0 {
|
|
||||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
|
||||||
Type: types.PowerInvite,
|
|
||||||
Model: "Invite",
|
|
||||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加邀请记录
|
|
||||||
err := tx.Create(&model.InviteLog{
|
|
||||||
InviterId: inviteCode.UserId,
|
|
||||||
UserId: user.Id,
|
|
||||||
Username: user.Username,
|
|
||||||
InviteCode: inviteCode.Code,
|
|
||||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower),
|
|
||||||
}).Error
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
|
||||||
// 自动登录创建 token
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"user_id": user.Id,
|
|
||||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
|
||||||
})
|
|
||||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 保存到 redis
|
|
||||||
key = fmt.Sprintf("users/%d", user.Id)
|
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
|
||||||
resp.ERROR(c, "error with save token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login 用户登录
|
// Login 用户登录
|
||||||
@@ -311,35 +238,13 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后登录时间和IP
|
token, err := h.doLogin(&user, c.ClientIP())
|
||||||
user.LastLoginIp = c.ClientIP()
|
|
||||||
user.LastLoginAt = time.Now().Unix()
|
|
||||||
h.DB.Model(&user).Updates(user)
|
|
||||||
|
|
||||||
h.DB.Create(&model.UserLoginLog{
|
|
||||||
UserId: user.Id,
|
|
||||||
Username: user.Username,
|
|
||||||
LoginIp: c.ClientIP(),
|
|
||||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建 token
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"user_id": user.Id,
|
|
||||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
|
||||||
})
|
|
||||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 保存到 redis
|
|
||||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
|
||||||
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
|
|
||||||
resp.ERROR(c, "error with save token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout 注 销
|
// Logout 注 销
|
||||||
@@ -351,130 +256,208 @@ func (h *UserHandler) Logout(c *gin.Context) {
|
|||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLogin 第三方登录请求二维码
|
// GetWxLoginQRCode 获取微信登录二维码URL
|
||||||
func (h *UserHandler) CLogin(c *gin.Context) {
|
func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) {
|
||||||
|
if !h.wxLoginService.GetConfig().Enabled {
|
||||||
|
resp.ERROR(c, "微信登录功能未启用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.wxLoginService.GetConfig().ApiKey == "" {
|
||||||
|
resp.ERROR(c, "微信登录服务令牌未配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := utils.RandString(32)
|
||||||
|
qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{
|
||||||
|
"url": qrCodeURL,
|
||||||
|
"state": state,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLoginCallback 第三方登录回调
|
// 查询微信登录状态
|
||||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
func (h *UserHandler) GetWxLoginState(c *gin.Context) {
|
||||||
// loginType := c.Query("login_type")
|
state := c.Query("state")
|
||||||
// code := c.Query("code")
|
if state == "" {
|
||||||
// userId := h.GetInt(c, "user_id", 0)
|
resp.ERROR(c, "参数错误")
|
||||||
// action := c.Query("action")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// var res types.BizVo
|
status, err := h.wxLoginService.GetLoginStatus(state)
|
||||||
// apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
|
if err != nil {
|
||||||
// r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
|
resp.ERROR(c, err.Error())
|
||||||
// SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
return
|
||||||
// SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
}
|
||||||
// SetSuccessResult(&res).
|
|
||||||
// Post(apiURL)
|
|
||||||
// if err != nil {
|
|
||||||
// resp.ERROR(c, err.Error())
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if r.IsErrorState() {
|
|
||||||
// resp.ERROR(c, "error with login http status: "+r.Status)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if res.Code != types.Success {
|
if status.Status != service.LoginStatusSuccess {
|
||||||
// resp.ERROR(c, "error with http response: "+res.Message)
|
resp.SUCCESS(c, status)
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // login successfully
|
// 登录成功
|
||||||
// data := res.Data.(map[string]interface{})
|
var user model.User
|
||||||
// var user model.User
|
h.DB.Where("openid = ?", status.OpenID).First(&user)
|
||||||
// if action == "bind" && userId > 0 {
|
if user.Id == 0 {
|
||||||
// err = h.DB.Where("openid", data["openid"]).First(&user).Error
|
// 创建新用户
|
||||||
// if err == nil {
|
user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "")
|
||||||
// resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
|
if err != nil {
|
||||||
// return
|
resp.ERROR(c, err.Error())
|
||||||
// }
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// err = h.DB.Where("id", userId).First(&user).Error
|
token, err := h.doLogin(&user, c.ClientIP())
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// resp.ERROR(c, "绑定用户不存在")
|
resp.ERROR(c, err.Error())
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
|
|
||||||
// err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
|
status.Status = service.LoginStatusExpired
|
||||||
// if err != nil {
|
h.wxLoginService.SetLoginStatus(state, *status)
|
||||||
// resp.ERROR(c, "更新用户信息失败,"+err.Error())
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// resp.SUCCESS(c, gin.H{"token": ""})
|
status.Status = service.LoginStatusSuccess
|
||||||
// return
|
status.Token = token
|
||||||
// }
|
resp.SUCCESS(c, status)
|
||||||
|
}
|
||||||
|
|
||||||
// session := gin.H{}
|
// createNewUser 创建新用户
|
||||||
// tx := h.DB.Where("openid", data["openid"]).First(&user)
|
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) {
|
||||||
// if tx.Error != nil {
|
if user.OpenId != "" {
|
||||||
// // create new user
|
user.Platform = "wechat"
|
||||||
// var totalUser int64
|
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
|
||||||
// h.DB.Model(&model.User{}).Count(&totalUser)
|
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8))
|
||||||
// if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
user.Password = "geekai123"
|
||||||
// resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
} else {
|
||||||
// return
|
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||||
// }
|
if user.Username == "" || user.Password == "" {
|
||||||
|
return user, fmt.Errorf("用户名或密码不能为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// salt := utils.RandString(8)
|
salt := utils.RandString(8)
|
||||||
// password := fmt.Sprintf("%d", utils.RandomNumber(8))
|
user.Salt = salt
|
||||||
// user = model.User{
|
user.Password = utils.GenPassword(user.Password, salt)
|
||||||
// Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
|
user.Avatar = "/images/avatar/user.png"
|
||||||
// Password: utils.GenPassword(password, salt),
|
user.Status = true
|
||||||
// Avatar: fmt.Sprintf("%s", data["avatar"]),
|
user.ChatRoles = utils.JsonEncode([]string{"gpt"})
|
||||||
// Salt: salt,
|
user.ChatConfig = "{}"
|
||||||
// Status: true,
|
user.ChatModels = "{}"
|
||||||
// ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
user.Power = h.App.SysConfig.Base.InitPower
|
||||||
// Power: h.App.SysConfig.InitPower,
|
|
||||||
// OpenId: fmt.Sprintf("%s", data["openid"]),
|
|
||||||
// Nickname: fmt.Sprintf("%s", data["nickname"]),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// tx = h.DB.Create(&user)
|
// 创建用户
|
||||||
// if tx.Error != nil {
|
tx := h.DB.Begin()
|
||||||
// resp.ERROR(c, "保存数据失败")
|
if err := tx.Create(&user).Error; err != nil {
|
||||||
// logger.Error(tx.Error)
|
return user, err
|
||||||
// return
|
}
|
||||||
// }
|
|
||||||
// session["username"] = user.Username
|
|
||||||
// session["password"] = password
|
|
||||||
// } else { // login directly
|
|
||||||
// // 更新最后登录时间和IP
|
|
||||||
// user.LastLoginIp = c.ClientIP()
|
|
||||||
// user.LastLoginAt = time.Now().Unix()
|
|
||||||
// h.DB.Model(&user).Updates(user)
|
|
||||||
|
|
||||||
// h.DB.Create(&model.UserLoginLog{
|
// 记录邀请关系
|
||||||
// UserId: user.Id,
|
if inviteCode != "" {
|
||||||
// Username: user.Username,
|
inviteCode := model.InviteCode{}
|
||||||
// LoginIp: c.ClientIP(),
|
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error
|
||||||
// LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
if err != nil {
|
||||||
// })
|
return user, fmt.Errorf("无效的邀请码")
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // 创建 token
|
// 增加邀请数量
|
||||||
// token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||||
// "user_id": user.Id,
|
if h.App.SysConfig.Base.InvitePower > 0 {
|
||||||
// "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
|
||||||
// })
|
Type: types.PowerInvite,
|
||||||
// tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
Model: "Invite",
|
||||||
// if err != nil {
|
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username),
|
||||||
// resp.ERROR(c, "Failed to generate token, "+err.Error())
|
})
|
||||||
// return
|
if err != nil {
|
||||||
// }
|
tx.Rollback()
|
||||||
// // 保存到 redis
|
return user, err
|
||||||
// key := fmt.Sprintf("users/%d", user.Id)
|
}
|
||||||
// if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
|
||||||
// resp.ERROR(c, "error with save token: "+err.Error())
|
// 添加邀请记录
|
||||||
// return
|
err = tx.Create(&model.InviteLog{
|
||||||
// }
|
InviterId: inviteCode.UserId,
|
||||||
// session["token"] = tokenString
|
UserId: user.Id,
|
||||||
// resp.SUCCESS(c, session)
|
Username: user.Username,
|
||||||
|
InviteCode: inviteCode.Code,
|
||||||
|
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower),
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doLogin 执行登录操作
|
||||||
|
func (h *UserHandler) doLogin(user *model.User, ip string) (string, error) {
|
||||||
|
// 更新最后登录时间和IP
|
||||||
|
user.LastLoginIp = ip
|
||||||
|
user.LastLoginAt = time.Now().Unix()
|
||||||
|
err := h.DB.Model(user).Updates(user).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to update user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录登录日志
|
||||||
|
h.DB.Create(&model.UserLoginLog{
|
||||||
|
UserId: user.Id,
|
||||||
|
Username: user.Username,
|
||||||
|
LoginIp: ip,
|
||||||
|
LoginAddress: utils.Ip2Region(h.ipSearcher, ip),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建 token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": user.Id,
|
||||||
|
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||||
|
})
|
||||||
|
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 redis
|
||||||
|
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||||
|
if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil {
|
||||||
|
return "", fmt.Errorf("error with save token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WxLoginCallback 微信登录回调处理
|
||||||
|
func (h *UserHandler) WxLoginCallback(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.OpenID == "" || data.State == "" {
|
||||||
|
resp.ERROR(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置登录状态
|
||||||
|
status := service.LoginStatus{
|
||||||
|
Status: service.LoginStatusSuccess,
|
||||||
|
OpenID: data.OpenID,
|
||||||
|
}
|
||||||
|
h.wxLoginService.SetLoginStatus(data.State, status)
|
||||||
|
|
||||||
|
resp.SUCCESS(c, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session 获取/验证会话
|
// Session 获取/验证会话
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ func (s *WxLoginService) UpdateConfig(config types.WxLoginConfig) {
|
|||||||
s.config = config
|
s.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *WxLoginService) GetConfig() types.WxLoginConfig {
|
||||||
|
return s.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WxLoginService) SetConfig(config types.WxLoginConfig) {
|
||||||
|
s.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (s *WxLoginService) GetLoginQrCodeUrl(state string) (string, error) {
|
func (s *WxLoginService) GetLoginQrCodeUrl(state string) (string, error) {
|
||||||
if s.config.ApiKey == "" {
|
if s.config.ApiKey == "" {
|
||||||
return "", errors.New("无效的 API Key")
|
return "", errors.New("无效的 API Key")
|
||||||
|
|||||||
@@ -1,68 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-dialog w-full">
|
<div class="login-dialog w-full">
|
||||||
<div class="login-box" v-if="login">
|
<div class="login-box" v-if="login">
|
||||||
<el-form :model="data" class="form space-y-5">
|
<custom-tabs v-model="loginActiveName" @tab-click="handleTabClick">
|
||||||
<div class="block">
|
<!-- 账号密码登录 -->
|
||||||
<el-input placeholder="账号" size="large" v-model="data.username" autocomplete="off">
|
<custom-tab-pane name="account" width="48">
|
||||||
<template #prefix>
|
<template #label>
|
||||||
<el-icon>
|
<div class="flex items-center justify-center px-3">
|
||||||
<Iphone />
|
<i class="iconfont icon-user-fill mr-2"></i>
|
||||||
</el-icon>
|
<span>账号登录</span>
|
||||||
</template>
|
</div>
|
||||||
</el-input>
|
</template>
|
||||||
</div>
|
<el-form :model="data" class="form space-y-5">
|
||||||
|
<div class="block">
|
||||||
|
<el-input placeholder="账号" size="large" v-model="data.username" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Iphone />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<el-input
|
<el-input
|
||||||
placeholder="请输入密码(8-16位)"
|
placeholder="请输入密码(8-16位)"
|
||||||
maxlength="16"
|
maxlength="16"
|
||||||
size="large"
|
size="large"
|
||||||
v-model="data.password"
|
v-model="data.password"
|
||||||
show-password
|
show-password
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Lock />
|
<Lock />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-row class="btn-row mt-8" :gutter="20">
|
<el-row class="btn-row mt-8" :gutter="20">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<button
|
<button
|
||||||
class="w-full h-12 rounded-xl text-base font-medium text-white bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg active:translate-y-0 shadow-md"
|
class="w-full h-12 rounded-xl text-base font-medium text-white bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg active:translate-y-0 shadow-md"
|
||||||
@click="submitLogin"
|
@click="submitLogin"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{{ loading ? '登录中...' : '登 录' }}
|
{{ loading ? '登录中...' : '登 录' }}
|
||||||
</button>
|
</button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div
|
||||||
class="text flex justify-center items-center pt-3 text-sm"
|
class="text flex justify-center items-center pt-3 text-sm"
|
||||||
style="color: var(--login-text-color)"
|
style="color: var(--login-text-color)"
|
||||||
>
|
>
|
||||||
还没有账号?
|
还没有账号?
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
class="ml-2 rounded-md px-2 py-1 transition-colors duration-200"
|
class="ml-2 rounded-md px-2 py-1 transition-colors duration-200"
|
||||||
style="color: var(--login-link-color)"
|
style="color: var(--login-link-color)"
|
||||||
@click="login = false"
|
@click="login = false"
|
||||||
@mouseenter="$event.target.style.background = 'var(--login-link-hover-bg)'"
|
@mouseenter="$event.target.style.background = 'var(--login-link-hover-bg)'"
|
||||||
@mouseleave="$event.target.style.background = 'transparent'"
|
@mouseleave="$event.target.style.background = 'transparent'"
|
||||||
>注册</el-button
|
>注册</el-button
|
||||||
>
|
>
|
||||||
|
|
||||||
<el-button type="info" class="forget ml-4" size="small" @click="showResetPass = true"
|
<el-button
|
||||||
>忘记密码?</el-button
|
type="info"
|
||||||
>
|
class="forget ml-4"
|
||||||
|
size="small"
|
||||||
|
@click="showResetPass = true"
|
||||||
|
>忘记密码?</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</custom-tab-pane>
|
||||||
|
|
||||||
|
<!-- 微信登录 -->
|
||||||
|
<custom-tab-pane name="wechat" width="48">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center justify-center px-3">
|
||||||
|
<i class="iconfont icon-wechat mr-2"></i>
|
||||||
|
<span>微信登录</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="wechat-login pt-3">
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<div class="qr-code-wrapper w-[200px] h-[200px] mx-auto" v-loading="qrcodeLoading">
|
||||||
|
<img :src="wechatLoginQRCode" class="qr-frame" v-if="wechatLoginQRCode" />
|
||||||
|
<!-- 二维码过期蒙版 -->
|
||||||
|
<div v-if="qrcodeExpired" class="qr-expired-mask">
|
||||||
|
<div class="expired-content">
|
||||||
|
<i class="iconfont icon-refresh-ccw expired-icon"></i>
|
||||||
|
<p class="expired-text">二维码已过期</p>
|
||||||
|
<button
|
||||||
|
@click="getWechatLoginURL"
|
||||||
|
class="bg-gray-200 text-gray-600 px-2.5 py-1 rounded-md hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-refresh text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mt-4 text-gray-600 dark:text-gray-400">
|
||||||
|
请使用微信扫描二维码登录
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</custom-tab-pane>
|
||||||
</el-form>
|
</custom-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="register-box w-full" v-else>
|
<div class="register-box w-full" v-else>
|
||||||
@@ -281,6 +329,8 @@
|
|||||||
import Captcha from '@/components/Captcha.vue'
|
import Captcha from '@/components/Captcha.vue'
|
||||||
import ResetPass from '@/components/ResetPass.vue'
|
import ResetPass from '@/components/ResetPass.vue'
|
||||||
import SendMsg from '@/components/SendMsg.vue'
|
import SendMsg from '@/components/SendMsg.vue'
|
||||||
|
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
|
||||||
|
import CustomTabs from '@/components/ui/CustomTabs.vue'
|
||||||
import { getSystemInfo } from '@/store/cache'
|
import { getSystemInfo } from '@/store/cache'
|
||||||
import { setUserToken } from '@/store/session'
|
import { setUserToken } from '@/store/session'
|
||||||
import { useSharedStore } from '@/store/sharedata'
|
import { useSharedStore } from '@/store/sharedata'
|
||||||
@@ -289,8 +339,9 @@ import { arrayContains } from '@/utils/libs'
|
|||||||
import { validateEmail, validateMobile } from '@/utils/validate'
|
import { validateEmail, validateMobile } from '@/utils/validate'
|
||||||
import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
|
import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -313,6 +364,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const login = ref(props.active === 'login')
|
const login = ref(props.active === 'login')
|
||||||
|
const loginActiveName = ref('account') // 新增:登录标签页激活状态
|
||||||
const data = ref({
|
const data = ref({
|
||||||
username: import.meta.env.VITE_USER,
|
username: import.meta.env.VITE_USER,
|
||||||
password: import.meta.env.VITE_PASS,
|
password: import.meta.env.VITE_PASS,
|
||||||
@@ -322,6 +374,15 @@ const data = ref({
|
|||||||
code: '',
|
code: '',
|
||||||
invite_code: props.inviteCode,
|
invite_code: props.inviteCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 微信登录相关变量
|
||||||
|
const wechatLoginQRCode = ref('')
|
||||||
|
const wechatLoginState = ref('')
|
||||||
|
const qrcodeLoading = ref(false)
|
||||||
|
const pollingTimer = ref(null)
|
||||||
|
const qrcodeExpired = ref(false)
|
||||||
|
const qrcodeTimer = ref(null)
|
||||||
|
|
||||||
const enableMobile = ref(false)
|
const enableMobile = ref(false)
|
||||||
const enableEmail = ref(false)
|
const enableEmail = ref(false)
|
||||||
const enableUser = ref(false)
|
const enableUser = ref(false)
|
||||||
@@ -379,6 +440,36 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听登录标签页切换
|
||||||
|
watch(loginActiveName, (newValue) => {
|
||||||
|
if (newValue === 'wechat') {
|
||||||
|
getWechatLoginURL()
|
||||||
|
} else {
|
||||||
|
// 其他登录方式,清除定时器
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
}
|
||||||
|
if (qrcodeTimer.value) {
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTabClick = (tab) => {
|
||||||
|
// CustomTabs组件传递的是tab对象,包含paneName属性
|
||||||
|
if (tab.paneName === 'wechat') {
|
||||||
|
getWechatLoginURL()
|
||||||
|
} else {
|
||||||
|
// 其他登录方式,清除定时器
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
}
|
||||||
|
if (qrcodeTimer.value) {
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submit = (verifyData) => {
|
const submit = (verifyData) => {
|
||||||
if (action.value === 'login') {
|
if (action.value === 'login') {
|
||||||
doLogin(verifyData)
|
doLogin(verifyData)
|
||||||
@@ -387,6 +478,107 @@ const submit = (verifyData) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取微信登录 URL
|
||||||
|
const getWechatLoginURL = () => {
|
||||||
|
wechatLoginQRCode.value = ''
|
||||||
|
qrcodeLoading.value = true
|
||||||
|
qrcodeExpired.value = false
|
||||||
|
|
||||||
|
// 清除可能存在的旧定时器
|
||||||
|
if (qrcodeTimer.value) {
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpGet('/api/user/login/qrcode')
|
||||||
|
.then((res) => {
|
||||||
|
// 生成二维码
|
||||||
|
QRCode.toDataURL(res.data.url, { width: 200, height: 200, margin: 2 }, (error, url) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
} else {
|
||||||
|
wechatLoginQRCode.value = url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wechatLoginState.value = res.data.state
|
||||||
|
// 开始轮询状态
|
||||||
|
startPolling()
|
||||||
|
|
||||||
|
// 设置1分钟后二维码过期
|
||||||
|
qrcodeTimer.value = setTimeout(() => {
|
||||||
|
qrcodeExpired.value = true
|
||||||
|
// 停止轮询
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
}
|
||||||
|
}, 60 * 1000) // 1分钟过期
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
ElMessage.error('获取微信登录 URL 失败,' + e.message)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
qrcodeLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
const startPolling = () => {
|
||||||
|
// 清除可能存在的旧定时器
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pollingTimer.value = setInterval(() => {
|
||||||
|
checkLoginStatus()
|
||||||
|
}, 1000) // 每1秒检查一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
if (!wechatLoginState.value) return
|
||||||
|
|
||||||
|
httpGet(`/api/user/login/status?state=${wechatLoginState.value}`)
|
||||||
|
.then((res) => {
|
||||||
|
const status = res.data.status
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
// 登录成功
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
setUserToken(res.data.token)
|
||||||
|
store.setIsLogin(true)
|
||||||
|
ElMessage.success('登录成功!')
|
||||||
|
emits('hide')
|
||||||
|
emits('success')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'expired':
|
||||||
|
// 二维码过期
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
qrcodeExpired.value = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pending':
|
||||||
|
// 继续轮询
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 其他错误情况
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
ElMessage.error('登录失败,请重试')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// 发生错误时显示过期状态
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
qrcodeExpired.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 登录操作
|
// 登录操作
|
||||||
const submitLogin = () => {
|
const submitLogin = () => {
|
||||||
if (!data.value.username) {
|
if (!data.value.username) {
|
||||||
@@ -512,22 +704,72 @@ const openPrivacy = () => {
|
|||||||
showPrivacy.value = true
|
showPrivacy.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清除定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value)
|
||||||
|
}
|
||||||
|
if (qrcodeTimer.value) {
|
||||||
|
clearTimeout(qrcodeTimer.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.login-dialog {
|
.login-dialog {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
// Dark theme support for Element Plus components
|
// 微信登录样式
|
||||||
:deep(.el-tabs) {
|
.wechat-login {
|
||||||
.el-tabs__header {
|
display: flex;
|
||||||
.el-tabs__nav-wrap {
|
justify-content: center;
|
||||||
.el-tabs__nav {
|
align-items: center;
|
||||||
.el-tabs__item {
|
min-height: 240px;
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
|
|
||||||
&.is-active {
|
.qr-code-container {
|
||||||
color: var(--el-color-primary);
|
text-align: center;
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.qr-frame {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-expired-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.expired-content {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.expired-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #f56565;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-text {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,6 +777,31 @@ const openPrivacy = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomTabs 组件样式优化
|
||||||
|
:deep(.custom-tabs-header) {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-tab-item) {
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-tab-active) {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-input) {
|
:deep(.el-input) {
|
||||||
.el-input__wrapper {
|
.el-input__wrapper {
|
||||||
background: var(--el-fill-color-blank);
|
background: var(--el-fill-color-blank);
|
||||||
@@ -571,4 +838,30 @@ const openPrivacy = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.login-dialog {
|
||||||
|
.wechat-login {
|
||||||
|
.qr-code-wrapper {
|
||||||
|
width: 240px !important;
|
||||||
|
height: 240px !important;
|
||||||
|
|
||||||
|
.qr-expired-mask {
|
||||||
|
.expired-content {
|
||||||
|
.expired-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-text {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,41 +5,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, inject, useSlots } from 'vue'
|
import { computed, inject, useSlots } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
width: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
// 支持百分比宽度,如 "30%" 会生成 style="width: 30%"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
// 从父组件注入当前激活的 tab
|
// 从父组件注入当前激活的 tab
|
||||||
const currentTab = inject('currentTab', '')
|
const currentTab = inject('currentTab', '')
|
||||||
|
|
||||||
const active = computed(() => {
|
const active = computed(() => {
|
||||||
return currentTab.value === props.name
|
return currentTab.value === props.name
|
||||||
})
|
})
|
||||||
|
|
||||||
// 向父组件提供当前 pane 的信息,优先使用 labelSlot
|
// 向父组件提供当前 pane 的信息,优先使用 labelSlot
|
||||||
const parentRegisterPane = inject('registerPane', () => {})
|
const parentRegisterPane = inject('registerPane', () => {})
|
||||||
|
|
||||||
// 立即注册,不要等到 onMounted
|
// 立即注册,不要等到 onMounted
|
||||||
parentRegisterPane({
|
parentRegisterPane({
|
||||||
name: props.name,
|
name: props.name,
|
||||||
label: props.label || '', // 如果没有传 label 则使用空字符串
|
label: props.label || '', // 如果没有传 label 则使用空字符串
|
||||||
labelSlot: slots.label,
|
labelSlot: slots.label,
|
||||||
})
|
width: props.width, // 传递 width 属性
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.custom-tab-pane {
|
.custom-tab-pane {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
<!--
|
||||||
|
CustomTabs 组件
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
<custom-tabs v-model="activeTab">
|
||||||
|
<custom-tab-pane name="tab1" label="标签1" width="30%">
|
||||||
|
内容1
|
||||||
|
</custom-tab-pane>
|
||||||
|
<custom-tab-pane name="tab2" label="标签2" width="70%">
|
||||||
|
内容2
|
||||||
|
</custom-tab-pane>
|
||||||
|
</custom-tabs>
|
||||||
|
|
||||||
|
width 属性说明:
|
||||||
|
- width="30%": 标签页宽度为 30%
|
||||||
|
- width="70%": 标签页宽度为 70%
|
||||||
|
- width="50": 标签页宽度为 50%
|
||||||
|
- 不设置 width: 标签页宽度为自适应
|
||||||
|
- 支持任意百分比值或数字值
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div
|
||||||
@@ -47,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex whitespace-nowrap overflow-x-auto scrollbar-hide"
|
class="flex whitespace-nowrap overflow-x-auto scrollbar-hide justify-between"
|
||||||
ref="tabsContainer"
|
ref="tabsContainer"
|
||||||
@scroll="checkScrollPosition"
|
@scroll="checkScrollPosition"
|
||||||
>
|
>
|
||||||
@@ -56,9 +76,10 @@
|
|||||||
v-for="(tab, index) in panes"
|
v-for="(tab, index) in panes"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
:class="{
|
:class="{
|
||||||
'!text-purple-600 bg-white shadow-sm custom-tab-active': modelValue === tab.name,
|
'!text-purple-600 bg-white shadow-sm custom-tab-active ': modelValue === tab.name,
|
||||||
'hover:bg-gray-50': modelValue !== tab.name,
|
'hover:bg-gray-50': modelValue !== tab.name,
|
||||||
}"
|
}"
|
||||||
|
:style="getWidthStyle(tab.width)"
|
||||||
@click="handleTabClick(tab.name, index)"
|
@click="handleTabClick(tab.name, index)"
|
||||||
ref="tabItems"
|
ref="tabItems"
|
||||||
>
|
>
|
||||||
@@ -87,6 +108,23 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'tab-click'])
|
const emit = defineEmits(['update:modelValue', 'tab-click'])
|
||||||
|
|
||||||
|
// 动态生成宽度样式对象
|
||||||
|
const getWidthStyle = (width) => {
|
||||||
|
if (!width) return {}
|
||||||
|
|
||||||
|
// 如果是百分比格式,直接使用
|
||||||
|
if (width.includes('%')) {
|
||||||
|
return { width: width }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是数字,转换为百分比
|
||||||
|
if (!isNaN(width)) {
|
||||||
|
return { width: `${width}%` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const tabsHeader = ref(null)
|
const tabsHeader = ref(null)
|
||||||
const tabsContainer = ref(null)
|
const tabsContainer = ref(null)
|
||||||
const tabItems = ref([])
|
const tabItems = ref([])
|
||||||
|
|||||||
Reference in New Issue
Block a user