mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-22 19:14:29 +08:00
验证码配置重构完成
This commit is contained in:
@@ -94,7 +94,6 @@ type BaseConfig struct {
|
||||
DefaultNickname string `json:"default_nickname"` // 默认昵称
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
@@ -118,6 +117,7 @@ const (
|
||||
ConfigKeyNotice = "notice"
|
||||
ConfigKeyAgreement = "agreement"
|
||||
ConfigKeyPrivacy = "privacy"
|
||||
ConfigKeyMarkMap = "mark_map"
|
||||
ConfigKeyCaptcha = "captcha"
|
||||
ConfigKeyWxLogin = "wx_login"
|
||||
ConfigKeyLicense = "license"
|
||||
|
||||
@@ -81,18 +81,18 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Base.EnabledVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
// if h.App.SysConfig.Base.EnabledVerify {
|
||||
// var check bool
|
||||
// if data.X != 0 {
|
||||
// check = h.captcha.SlideCheck(data)
|
||||
// } else {
|
||||
// check = h.captcha.Check(data)
|
||||
// }
|
||||
// if !check {
|
||||
// resp.ERROR(c, "请先完人机验证")
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
|
||||
@@ -92,6 +92,9 @@ func (h *ConfigHandler) RegisterRoutes() {
|
||||
rg.POST("update/base", h.UpdateBase)
|
||||
rg.POST("update/power", h.UpdatePower)
|
||||
rg.POST("update/notice", h.UpdateNotice)
|
||||
rg.POST("update/agreement", h.UpdateAgreement)
|
||||
rg.POST("update/privacy", h.UpdatePrivacy)
|
||||
rg.POST("update/mark_map", h.UpdateMarkMap)
|
||||
rg.POST("update/captcha", h.UpdateCaptcha)
|
||||
rg.POST("update/wx_login", h.UpdateWxLogin)
|
||||
rg.POST("update/payment", h.UpdatePayment)
|
||||
@@ -193,6 +196,64 @@ func (h *ConfigHandler) UpdateNotice(c *gin.Context) {
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateAgreement 更新用户协议配置
|
||||
func (h *ConfigHandler) UpdateAgreement(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyAgreement, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdatePrivacy 更新隐私政策配置
|
||||
func (h *ConfigHandler) UpdatePrivacy(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyPrivacy, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateMarkMap 更新思维导图配置
|
||||
func (h *ConfigHandler) UpdateMarkMap(c *gin.Context) {
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Update(types.ConfigKeyMarkMap, data)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// UpdateCaptcha 更新行为验证码配置
|
||||
func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
|
||||
var data types.CaptchaConfig
|
||||
@@ -206,10 +267,7 @@ func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if data.Enabled {
|
||||
h.captchaService.UpdateConfig(data)
|
||||
}
|
||||
h.sysConfig.Captcha = data
|
||||
h.captchaService.UpdateConfig(data)
|
||||
resp.SUCCESS(c, data)
|
||||
|
||||
}
|
||||
|
||||
@@ -16,16 +16,13 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 今日头条函数实现
|
||||
|
||||
type CaptchaHandler struct {
|
||||
App *core.AppServer
|
||||
service *service.CaptchaService
|
||||
config types.CaptchaConfig
|
||||
}
|
||||
|
||||
func NewCaptchaHandler(app *core.AppServer, s *service.CaptchaService, sysConfig *types.SystemConfig) *CaptchaHandler {
|
||||
return &CaptchaHandler{App: app, service: s, config: sysConfig.Captcha}
|
||||
return &CaptchaHandler{App: app, service: s}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -37,10 +34,15 @@ func (h *CaptchaHandler) RegisterRoutes() {
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
group.GET("config", h.GetConfig)
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) GetConfig(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{"enabled": h.service.GetConfig().Enabled, "type": h.service.GetConfig().Type})
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
if !h.config.Enabled {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
@@ -56,7 +58,7 @@ func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
|
||||
// Check verify the captcha data
|
||||
func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
if !h.config.Enabled {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
@@ -80,7 +82,7 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
|
||||
// SlideGet 获取滑动验证图片
|
||||
func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
if !h.config.Enabled {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
@@ -96,7 +98,7 @@ func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
|
||||
// SlideCheck 滑动验证结果校验
|
||||
func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
|
||||
if !h.config.Enabled {
|
||||
if !h.service.GetConfig().Enabled {
|
||||
resp.ERROR(c, "验证码服务未启用")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -30,12 +29,7 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ChatModelHandler) RegisterRoutes() {
|
||||
group := h.App.Engine.Group("/api/model/")
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
group.GET("list", h.List)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
|
||||
@@ -24,10 +24,10 @@ const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sms *sms.SmsManager
|
||||
smtp *service.SmtpService
|
||||
captcha *service.CaptchaService
|
||||
redis *redis.Client
|
||||
sms *sms.SmsManager
|
||||
smtp *service.SmtpService
|
||||
captchaService *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(
|
||||
@@ -37,11 +37,11 @@ func NewSmsHandler(
|
||||
smtp *service.SmtpService,
|
||||
captcha *service.CaptchaService) *SmsHandler {
|
||||
return &SmsHandler{
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captcha: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captchaService: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
@@ -63,12 +63,12 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.Base.EnabledVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
|
||||
@@ -35,7 +35,7 @@ type UserHandler struct {
|
||||
redis *redis.Client
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
captcha *service.CaptchaService
|
||||
captchaService *service.CaptchaService
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewUserHandler(
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
levelDB: levelDB,
|
||||
captcha: captcha,
|
||||
captchaService: captcha,
|
||||
licenseService: licenseService,
|
||||
userService: userService,
|
||||
}
|
||||
@@ -104,12 +104,13 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.Base.EnabledVerify && data.RegWay == "username" {
|
||||
// 如果注册方式不是账号密码,则需要验证码
|
||||
if h.captchaService.GetConfig().Enabled && data.RegWay != "username" {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -279,15 +280,12 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
verifyKey := fmt.Sprintf("users/verify/%s", data.Username)
|
||||
needVerify, err := h.redis.Get(c, verifyKey).Bool()
|
||||
|
||||
if h.App.SysConfig.Base.EnabledVerify && needVerify {
|
||||
if h.captchaService.GetConfig().Enabled {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
check = h.captchaService.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
check = h.captchaService.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
@@ -298,19 +296,17 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
if !user.Status {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
|
||||
return
|
||||
}
|
||||
@@ -343,8 +339,6 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 移除登录行为验证码
|
||||
h.redis.Del(c, verifyKey)
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ func (s *CaptchaService) UpdateConfig(config types.CaptchaConfig) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) GetConfig() types.CaptchaConfig {
|
||||
return s.config
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Get() (interface{}, error) {
|
||||
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
@@ -48,7 +52,7 @@ func (s *CaptchaService) Get() (interface{}, error) {
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Check(data interface{}) bool {
|
||||
func (s *CaptchaService) Check(data any) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/check", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
@@ -66,7 +70,7 @@ func (s *CaptchaService) Check(data interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideGet() (interface{}, error) {
|
||||
func (s *CaptchaService) SlideGet() (any, error) {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/get", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
@@ -83,7 +87,7 @@ func (s *CaptchaService) SlideGet() (interface{}, error) {
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideCheck(data interface{}) bool {
|
||||
func (s *CaptchaService) SlideCheck(data any) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/check", types.GeekAPIURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
|
||||
@@ -4,19 +4,16 @@
|
||||
v-model="show"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="isMobileInternal"
|
||||
:append-to-body="true"
|
||||
style="width: 360px; --el-dialog-padding-primary: 5px 15px 15px 15px"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
class="text-center p-3"
|
||||
style="color: var(--el-text-color-primary)"
|
||||
v-if="isMobileInternal"
|
||||
>
|
||||
<div class="text-center p-3" style="color: var(--el-text-color-primary)">
|
||||
<span>人机验证</span>
|
||||
</div>
|
||||
</template>
|
||||
<slide-captcha
|
||||
v-if="isMobileInternal"
|
||||
v-if="type === 'slide'"
|
||||
:bg-img="bgImg"
|
||||
:bk-img="bkImg"
|
||||
:result="result"
|
||||
@@ -48,6 +45,13 @@ import { isMobile } from '@/utils/libs'
|
||||
import lodash from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'slide',
|
||||
},
|
||||
})
|
||||
|
||||
const show = ref(false)
|
||||
const maxDot = ref(5)
|
||||
const imageBase64 = ref('')
|
||||
@@ -98,7 +102,7 @@ const handleConfirm = (dts) => {
|
||||
const loadCaptcha = () => {
|
||||
show.value = true
|
||||
// 手机用滑动验证码
|
||||
if (isMobile()) {
|
||||
if (props.type === 'slide') {
|
||||
getSlideCaptcha()
|
||||
} else {
|
||||
handleRequestCaptCode()
|
||||
|
||||
@@ -226,6 +226,7 @@ export default {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
height: 360px;
|
||||
|
||||
.wg-cap-wrap__header {
|
||||
height: 50px;
|
||||
|
||||
@@ -207,6 +207,17 @@
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block text-sm">
|
||||
<el-checkbox v-model="agreeChecked">
|
||||
我已阅读并同意
|
||||
<a href="javascript:void(0)" class="text-blue-500" @click="openAgreement"
|
||||
>《用户协议》</a
|
||||
>
|
||||
和
|
||||
<a href="javascript:void(0)" class="text-blue-500" @click="openPrivacy">《隐私政策》</a>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<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"
|
||||
@@ -252,9 +263,17 @@
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<captcha v-if="enableVerify" @success="submit" ref="captchaRef" />
|
||||
<captcha v-if="enableCaptcha" :type="captchaType" @success="submit" ref="captchaRef" />
|
||||
|
||||
<reset-pass @hide="showResetPass = false" :show="showResetPass" />
|
||||
|
||||
<el-dialog v-model="showAgreement" title="用户协议" :append-to-body="true">
|
||||
<div class="prose" v-html="agreementHtml"></div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showPrivacy" title="隐私政策" :append-to-body="true">
|
||||
<div class="prose" v-html="privacyHtml"></div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -265,12 +284,13 @@ import SendMsg from '@/components/SendMsg.vue'
|
||||
import { getSystemInfo } from '@/store/cache'
|
||||
import { setUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains } from '@/utils/libs'
|
||||
import { validateEmail, validateMobile } from '@/utils/validate'
|
||||
import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
@@ -313,10 +333,16 @@ const captchaRef = ref(null)
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(['hide', 'success'])
|
||||
const action = ref('login')
|
||||
const enableVerify = ref(false)
|
||||
const enableCaptcha = ref(false)
|
||||
const captchaType = ref('')
|
||||
const showResetPass = ref(false)
|
||||
const store = useSharedStore()
|
||||
const loading = ref(false)
|
||||
const agreeChecked = ref(false)
|
||||
const showAgreement = ref(false)
|
||||
const showPrivacy = ref(false)
|
||||
const agreementHtml = ref('')
|
||||
const privacyHtml = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
@@ -341,12 +367,16 @@ onMounted(() => {
|
||||
if (res.data['wechat_card_url'] !== '') {
|
||||
wxImg.value = res.data['wechat_card_url']
|
||||
}
|
||||
enableVerify.value = res.data['enabled_verify']
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet('/api/captcha/config').then((res) => {
|
||||
enableCaptcha.value = res.data['enabled']
|
||||
captchaType.value = res.data['type']
|
||||
})
|
||||
})
|
||||
|
||||
const submit = (verifyData) => {
|
||||
@@ -365,7 +395,7 @@ const submitLogin = () => {
|
||||
if (!data.value.password) {
|
||||
return ElMessage.error('请输入密码')
|
||||
}
|
||||
if (enableVerify.value) {
|
||||
if (enableCaptcha.value) {
|
||||
captchaRef.value.loadCaptcha()
|
||||
action.value = 'login'
|
||||
} else {
|
||||
@@ -418,7 +448,10 @@ const submitRegister = () => {
|
||||
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
|
||||
return ElMessage.error('请输入验证码')
|
||||
}
|
||||
if (enableVerify.value && activeName.value === 'username') {
|
||||
if (!agreeChecked.value) {
|
||||
return ElMessage.error('请先阅读并同意《用户协议》和《隐私政策》')
|
||||
}
|
||||
if (enableCaptcha.value && activeName.value === 'username') {
|
||||
captchaRef.value.loadCaptcha()
|
||||
action.value = 'register'
|
||||
} else {
|
||||
@@ -451,6 +484,34 @@ const doRegister = (verifyData) => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 打开并加载协议
|
||||
const openAgreement = () => {
|
||||
if (!agreementHtml.value) {
|
||||
httpGet('/api/config/get?key=agreement')
|
||||
.then((res) => {
|
||||
agreementHtml.value = marked.parse(res.data?.content || '')
|
||||
showAgreement.value = true
|
||||
})
|
||||
.catch((e) => ElMessage.error('加载用户协议失败:' + e.message))
|
||||
} else {
|
||||
showAgreement.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 打开并加载隐私政策
|
||||
const openPrivacy = () => {
|
||||
if (!privacyHtml.value) {
|
||||
httpGet('/api/config/get?key=privacy')
|
||||
.then((res) => {
|
||||
privacyHtml.value = marked.parse(res.data?.content || '')
|
||||
showPrivacy.value = true
|
||||
})
|
||||
.catch((e) => ElMessage.error('加载隐私政策失败:' + e.message))
|
||||
} else {
|
||||
showPrivacy.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:before-close="close"
|
||||
:title="title"
|
||||
class="reset-pass-dialog"
|
||||
:append-to-body="true"
|
||||
>
|
||||
<div class="form">
|
||||
<el-form :model="form" label-width="80px" label-position="left">
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<div class="py-4">
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
@click="generate"
|
||||
>
|
||||
<i v-if="isGenerating" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
@@ -616,6 +616,7 @@
|
||||
<button
|
||||
class="px-10 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
@click="generate"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="isGenerating" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
@click="generate"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="isGenerating" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
@click="store.submitTask"
|
||||
:disabled="store.submitting"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="store.submitting" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
|
||||
@@ -46,10 +46,13 @@
|
||||
<div class="p-4">
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
@click="generateAI"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
>
|
||||
生成思维导图
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>生成思维导图</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +70,7 @@
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-green-400 to-blue-500 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-green-500 hover:to-blue-600 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
@click="generate"
|
||||
type="button"
|
||||
>
|
||||
直接生成(免费)
|
||||
</button>
|
||||
@@ -140,10 +144,8 @@ onMounted(async () => {
|
||||
if (cache) {
|
||||
text.value = cache
|
||||
} else {
|
||||
const res = await getSystemInfo().catch((e) => {
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
text.value = res.data['mark_map_text']
|
||||
const res = await httpGet('/api/config/get?key=mark_map')
|
||||
text.value = res.data?.content || ''
|
||||
content.value = text.value
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
@click="store.create"
|
||||
:disabled="store.loading"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="store.loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
@@ -245,6 +246,7 @@
|
||||
<div class="w-full py-2">
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-orange-300 to-purple-500 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-orange-300 hover:to-red-500 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
type="button"
|
||||
>
|
||||
<i class="iconfont icon-upload mr-2"></i>
|
||||
<span>上传音乐</span>
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
@click="store.createLumaVideo"
|
||||
:disabled="store.generating"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="store.generating" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
@@ -457,6 +458,7 @@
|
||||
@click="store.createKelingVideo"
|
||||
:disabled="store.generating"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
>
|
||||
<i v-if="store.generating" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
|
||||
@@ -80,22 +80,24 @@ const enableVerify = ref(false)
|
||||
const captchaRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
checkAdminSession()
|
||||
.then(() => {
|
||||
router.push('/admin')
|
||||
})
|
||||
.catch(() => {})
|
||||
onMounted(() => {
|
||||
// 判断是否登录
|
||||
checkAdminSession()
|
||||
.then(() => {
|
||||
router.push('/admin')
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
// 加载系统配置
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.admin_title
|
||||
logo.value = res.data.logo
|
||||
enableVerify.value = res.data['enabled_verify']
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('加载系统配置失败: ' + e.message)
|
||||
})
|
||||
// 加载系统配置
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.admin_title
|
||||
logo.value = res.data.logo
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('加载系统配置失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const login = function () {
|
||||
if (username.value === '') {
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
<template>
|
||||
<div class="agreement-config form" v-loading="loading">
|
||||
<div class="container">
|
||||
<h3>用户协议</h3>
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="agreement"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="agreement-config container" v-loading="loading">
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="agreement"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<div class="flex justify-center p-5">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +38,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
httpPost('/api/admin/config/update/base', { mark_map_text: agreement.value })
|
||||
httpPost('/api/admin/config/update/agreement', { content: agreement.value })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<div class="form" v-loading="loading">
|
||||
<el-form :model="api" label-width="140px">
|
||||
<el-form-item label="API 网关"><el-input v-model="api.api_url" /></el-form-item>
|
||||
<el-form-item label="AppId"><el-input v-model="api.app_id" /></el-form-item>
|
||||
<el-form-item label="Token"><el-input v-model="api.token" type="password" /></el-form-item>
|
||||
|
||||
<el-divider>即梦 AI</el-divider>
|
||||
<el-form-item label="AccessKey"
|
||||
><el-input v-model="api.jimeng_config.access_key"
|
||||
/></el-form-item>
|
||||
<el-form-item label="SecretKey"
|
||||
><el-input v-model="api.jimeng_config.secret_key"
|
||||
/></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
<el-button @click="test">测试</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const api = ref({
|
||||
api_url: '',
|
||||
app_id: '',
|
||||
token: '',
|
||||
jimeng_config: { access_key: '', secret_key: '' },
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
httpGet('/api/admin/config/get?key=api')
|
||||
.then((res) => (api.value = res.data || api.value))
|
||||
.catch(() => {})
|
||||
.finally(() => (loading.value = false))
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
ElMessage.info('当前后端未提供 /api 配置的更新接口,已保留只读展示')
|
||||
}
|
||||
|
||||
const test = () => {
|
||||
ElMessage.info('请在对应服务端手动测试 API 可用性')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
padding: 10px 20px 40px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -90,18 +90,6 @@
|
||||
<el-switch v-model="system['enabled_register']" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
启用验证码
|
||||
<span class="text-xs text-gray-500"
|
||||
>(启用验证码之后,注册登录都会加载行为验证码,增加安全性。)</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="system['enabled_verify']" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="注册方式" prop="register_ways">
|
||||
<el-checkbox-group v-model="system['register_ways']">
|
||||
<el-checkbox value="mobile">手机注册</el-checkbox>
|
||||
|
||||
@@ -1,45 +1,14 @@
|
||||
<template>
|
||||
<div class="markmap-config form" v-loading="loading">
|
||||
<div class="container">
|
||||
<h3>思维导图配置</h3>
|
||||
<el-form
|
||||
:model="system"
|
||||
label-width="150px"
|
||||
label-position="right"
|
||||
ref="systemFormRef"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
思维导图默认文本
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="用户访问思维导图页面时显示的默认文本内容,支持 Markdown 格式"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
:theme="store.theme"
|
||||
v-model="system['mark_map_text']"
|
||||
@on-upload-img="onUploadImg"
|
||||
placeholder="请输入思维导图页面的默认文本内容,支持 Markdown 格式"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="container" v-loading="loading">
|
||||
<md-editor
|
||||
:theme="store.theme"
|
||||
v-model="content"
|
||||
@on-upload-img="onUploadImg"
|
||||
placeholder="请输入思维导图页面的默认文本内容,支持 Markdown 格式"
|
||||
/>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="flex justify-center p-5">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -47,26 +16,25 @@
|
||||
<script setup>
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MdEditor from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
const system = ref({})
|
||||
const content = ref('')
|
||||
const loading = ref(true)
|
||||
const systemFormRef = ref(null)
|
||||
const store = useSharedStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 加载系统配置
|
||||
httpGet('/api/admin/config/get?key=system')
|
||||
httpGet('/api/admin/config/get?key=mark_map')
|
||||
.then((res) => {
|
||||
system.value = res.data
|
||||
loading.value = false
|
||||
content.value = res.data?.content || ''
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('加载系统配置失败: ' + e.message)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
@@ -74,9 +42,8 @@ onMounted(() => {
|
||||
const rules = reactive({})
|
||||
|
||||
const save = function () {
|
||||
httpPost('/api/admin/config/update', {
|
||||
key: 'system',
|
||||
config: { mark_map_text: system.value.mark_map_text },
|
||||
httpPost('/api/admin/config/update/mark_map', {
|
||||
content: content.value,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
@@ -111,8 +78,8 @@ const onUploadImg = (files, callback) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../../../assets/css/admin/form.scss' as *;
|
||||
@use '../../../assets/css/main.scss' as *;
|
||||
@use '@/assets/css/admin/form.scss' as *;
|
||||
@use '@/assets/css/main.scss' as *;
|
||||
|
||||
.markmap-config {
|
||||
display: flex;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<template>
|
||||
<div class="notice-config form" v-loading="loading">
|
||||
<div class="container">
|
||||
<h3>公告配置</h3>
|
||||
<md-editor class="mgb20" v-model="notice" :theme="store.theme" @on-upload-img="onUploadImg" />
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="notice-config container" v-loading="loading">
|
||||
<md-editor class="mgb20" v-model="notice" :theme="store.theme" @on-upload-img="onUploadImg" />
|
||||
<div class="flex justify-center p-5">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<template>
|
||||
<div class="privacy-config form" v-loading="loading">
|
||||
<div class="privacy-config container" v-loading="loading">
|
||||
<div class="container">
|
||||
<h3>隐私声明</h3>
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="privacy"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="flex justify-center p-5">
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +40,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
httpPost('/api/admin/config/update/notice', { content: privacy.value })
|
||||
httpPost('/api/admin/config/update/privacy', { content: privacy.value })
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
|
||||
<button
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
|
||||
type="button"
|
||||
@click="jimengStore.submitTask"
|
||||
:disabled="jimengStore.submitting"
|
||||
>
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
<button
|
||||
@click="suno.create"
|
||||
:disabled="suno.loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="suno.loading" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
<button
|
||||
@click="video.createLumaVideo"
|
||||
:disabled="video.generating"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="video.generating" class="iconfont icon-loading animate-spin"></i>
|
||||
@@ -423,6 +424,7 @@
|
||||
<button
|
||||
@click="video.createKelingVideo"
|
||||
:disabled="video.generating"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="video.generating" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
|
||||
Reference in New Issue
Block a user