注册功能

This commit is contained in:
2026-02-24 15:15:16 +08:00
parent adc5a4d5fe
commit d4c0dcf6b1
3 changed files with 447 additions and 102 deletions

View File

@@ -8,5 +8,6 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
</body> </body>
</html> </html>

28
src/api/register.js Normal file
View File

@@ -0,0 +1,28 @@
import axios from 'axios'
// 创建独立的 axios 实例,避免被全局拦截器影响
const registerAxios = axios.create({
timeout: 30000,
headers: { 'Content-Type': 'application/json' },
})
// 注册接口使用 tkNewAdmin 后端
const REGISTER_BASE_URL = process.env.NODE_ENV === 'development'
? 'http://192.168.2.22:48080'
: 'https://backstageapi.yolozs.com'
/**
* 租户注册
* 调用 tkNewAdmin 后端的 /admin-api/system/tenant/register 接口
*/
export function tenantRegister(data) {
return registerAxios.post(`${REGISTER_BASE_URL}/admin-api/system/tenant/register`, data)
.then(res => {
console.log('注册返回', res)
if (res.data && res.data.code === 0) {
return res.data.data
}
const msg = (res.data && res.data.msg) || (res.data && res.data.message) || '注册失败'
return Promise.reject(new Error(msg))
})
}

View File

@@ -1,13 +1,7 @@
```
<template> <template>
<div <div
class="min-h-screen bg-[#F0F4F8] flex items-center justify-center font-sans antialiased relative overflow-hidden transition-colors duration-300"> class="min-h-screen bg-[#F0F4F8] flex items-center justify-center font-sans antialiased relative overflow-hidden transition-colors duration-300">
<div class="absolute top-8 right-8 flex gap-4 z-20"> <div class="absolute top-8 right-8 flex gap-4 z-20">
<!-- Network Settings (Placeholder/Mock) -->
<!-- <div class="bg-white/95 border border-slate-200 rounded-2xl px-3 py-2 shadow-lg cursor-pointer hover:-translate-y-px transition-all flex items-center gap-2">
<span class="text-sm font-semibold text-slate-700">{{ $t('login.network') }}</span>
</div> -->
<!-- Language Selector --> <!-- Language Selector -->
<el-dropdown> <el-dropdown>
<div <div
@@ -40,95 +34,261 @@
<img :src="logo" alt="Logo" class="w-[200px] h-auto" /> <img :src="logo" alt="Logo" class="w-[200px] h-auto" />
</div> </div>
<div class="mb-8"> <!-- ==================== 登录表单 ==================== -->
<h1 class="text-2xl font-bold text-gray-800 mb-2">欢迎回来</h1> <template v-if="mode === 'login'">
<p class="text-gray-500 text-sm">请输入您的账户信息以登录平台</p> <div class="mb-8">
</div> <h1 class="text-2xl font-bold text-gray-800 mb-2">欢迎回来</h1>
<p class="text-gray-500 text-sm">请输入您的账户信息以登录平台</p>
<form @submit.prevent="handleSubmit" class="space-y-5">
<!-- 租户号 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">租户号</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<input type="text" v-model="credentials.tenantName" placeholder="请输入租户号"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div> </div>
<!-- 账号 --> <form @submit.prevent="handleSubmit" class="space-y-5">
<div> <!-- 租户号 -->
<label class="block text-sm font-medium text-gray-700 mb-1">账号</label> <div>
<div class="relative rounded-md shadow-sm"> <label class="block text-sm font-medium text-gray-700 mb-1">租户号</label>
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="relative rounded-md shadow-sm">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
stroke="currentColor"> <svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor">
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
</svg> d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</div>
<input type="text" v-model="credentials.username" placeholder="请输入账号"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 密码 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<input type="password" v-model="credentials.password" placeholder="请输入密码"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 错误提示 -->
<div v-if="error"
class="flex items-center gap-2 text-red-600 text-xs bg-red-50 p-2.5 rounded-lg">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ error }}
</div>
<!-- 登录按钮 -->
<div class="pt-2">
<button type="submit" :disabled="isLoading"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-[#4F81E6] hover:from-blue-600 hover:to-[#3A6BC7] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#4F81E6] transition-all transform hover:scale-[1.01] disabled:opacity-70 disabled:cursor-not-allowed">
<template v-if="isLoading">
<span class="flex items-center gap-2">
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg> </svg>
登录中 </div>
</span> <input type="text" v-model="credentials.tenantName" placeholder="请输入租户号"
</template> class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
<template v-else> </div>
登 录 </div>
</template>
<!-- 账号 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">账号</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<input type="text" v-model="credentials.username" placeholder="请输入账号"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 密码 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<input type="password" v-model="credentials.password" placeholder="请输入密码"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 错误提示 -->
<div v-if="error"
class="flex items-center gap-2 text-red-600 text-xs bg-red-50 p-2.5 rounded-lg">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ error }}
</div>
<!-- 登录按钮 -->
<div class="pt-2">
<button type="submit" :disabled="isLoading"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-[#4F81E6] hover:from-blue-600 hover:to-[#3A6BC7] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#4F81E6] transition-all transform hover:scale-[1.01] disabled:opacity-70 disabled:cursor-not-allowed">
<template v-if="isLoading">
<span class="flex items-center gap-2">
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
登录中
</span>
</template>
<template v-else>
</template>
</button>
</div>
</form>
<!-- 切换到注册 -->
<div class="mt-6 text-center">
<span class="text-gray-500 text-sm">还没有账号</span>
<button @click="switchMode('register')"
class="text-[#4F81E6] text-sm font-medium hover:text-blue-700 ml-1 transition-colors">
立即注册
</button> </button>
</div> </div>
</form> </template>
<div class="mt-8 text-center"> <!-- ==================== 注册表单 ==================== -->
<template v-else>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-800 mb-2">注册新账号</h1>
<p class="text-gray-500 text-sm">填写以下信息创建您的租户账号</p>
</div>
<form @submit.prevent="handleRegister" class="space-y-4">
<!-- 租户名称 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">租户名称</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<input type="text" v-model="registerForm.name" placeholder="2-20个字符"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 联系人 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">联系人</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<input type="text" v-model="registerForm.contactName" placeholder="请输入联系人姓名"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 联系手机 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">联系手机</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<input type="tel" v-model="registerForm.contactMobile" placeholder="请输入手机号码"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 账号 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">账号</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<input type="text" v-model="registerForm.username" placeholder="4-30个字符"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 密码 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<input type="password" v-model="registerForm.password" placeholder="5-20个字符"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- 确认密码 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<input type="password" v-model="registerForm.confirmPassword"
placeholder="请再次输入密码"
class="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-2.5 transition-colors bg-gray-50 focus:bg-white" />
</div>
</div>
<!-- Turnstile 人机验证 -->
<div class="flex justify-center">
<div id="turnstile-container"></div>
</div>
<!-- 错误提示 -->
<div v-if="error"
class="flex items-center gap-2 text-red-600 text-xs bg-red-50 p-2.5 rounded-lg">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ error }}
</div>
<!-- 注册按钮 -->
<div class="pt-1">
<button type="submit" :disabled="isLoading"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-[#4F81E6] hover:from-blue-600 hover:to-[#3A6BC7] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#4F81E6] transition-all transform hover:scale-[1.01] disabled:opacity-70 disabled:cursor-not-allowed">
<template v-if="isLoading">
<span class="flex items-center gap-2">
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
注册中
</span>
</template>
<template v-else>
</template>
</button>
</div>
</form>
<!-- 切换到登录 -->
<div class="mt-4 text-center">
<span class="text-gray-500 text-sm">已有账号</span>
<button @click="switchMode('login')"
class="text-[#4F81E6] text-sm font-medium hover:text-blue-700 ml-1 transition-colors">
返回登录
</button>
</div>
</template>
<div class="mt-6 text-center">
<span class="text-gray-300 text-xs font-mono">v{{ version }}</span> <span class="text-gray-300 text-xs font-mono">v{{ version }}</span>
</div> </div>
</div> </div>
@@ -156,10 +316,12 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { isElectron, getAppVersion } from '../utils/electronBridge' import { isElectron, getAppVersion } from '../utils/electronBridge'
import { setUser, setToken, setUserPass, getUserPass, setPermissions } from '@/utils/storage' import { setUser, setToken, setUserPass, getUserPass, setPermissions } from '@/utils/storage'
import { tenantRegister } from '@/api/register'
import logo from '../assets/logo.png' import logo from '../assets/logo.png'
import illustration from '../assets/illustration.png' import illustration from '../assets/illustration.png'
@@ -167,31 +329,38 @@ const emit = defineEmits(['loginSuccess'])
const { locale } = useI18n() const { locale } = useI18n()
// 当前模式login / register
const mode = ref('login')
// Language Switcher // Language Switcher
const switchLanguage = (lang) => { const switchLanguage = (lang) => {
locale.value = lang locale.value = lang
localStorage.setItem('lang', lang) localStorage.setItem('lang', lang)
} }
// const STORAGE_KEY = 'login_credentials' // Deprecated in favor of getUserPass // 切换登录/注册
// const USER_KEY = 'user_data' // Deprecated in favor of setUser const switchMode = (target) => {
mode.value = target
error.value = ''
}
// ==================== 通用状态 ====================
const isLoading = ref(false)
const error = ref('')
const version = ref('')
// ==================== 登录相关 ====================
const credentials = ref({ const credentials = ref({
tenantName: '', tenantName: '',
username: '', username: '',
password: '', password: '',
}) })
const isLoading = ref(false)
const error = ref('')
const version = ref('')
onMounted(() => { onMounted(() => {
// 获取应用版本
getAppVersion().then(v => { getAppVersion().then(v => {
version.value = v version.value = v
}) })
// 加载保存的凭据
try { try {
const saved = getUserPass() const saved = getUserPass()
if (saved) { if (saved) {
@@ -214,7 +383,6 @@ const handleSubmit = async () => {
error.value = '' error.value = ''
try { try {
// 保存凭据 (Using compatible storage helper)
setUserPass(credentials.value) setUserPass(credentials.value)
console.log('[LoginPage] 开始登录...', credentials.value) console.log('[LoginPage] 开始登录...', credentials.value)
@@ -229,11 +397,9 @@ const handleSubmit = async () => {
console.log('[LoginPage] 登录结果:', result) console.log('[LoginPage] 登录结果:', result)
if (result.success && result.user) { if (result.success && result.user) {
// Save token and user info to localStorage using legacy keys to support ported views
setToken(result.user.tokenValue); setToken(result.user.tokenValue);
setUser(result.user); setUser(result.user);
// 保存权限信息
setPermissions({ setPermissions({
bigBrother: result.user.bigBrother, bigBrother: result.user.bigBrother,
crawl: result.user.crawl, crawl: result.user.crawl,
@@ -250,6 +416,156 @@ const handleSubmit = async () => {
isLoading.value = false isLoading.value = false
} }
} }
// ==================== 注册相关 ====================
const registerForm = ref({
name: '',
contactName: '',
contactMobile: '',
username: '',
password: '',
confirmPassword: '',
})
// Turnstile 人机验证
const TURNSTILE_SITE_KEY = '0x4AAAAAACYSAf0bQMQ347Pz'
const turnstileToken = ref('')
const turnstileWidgetId = ref('')
const initTurnstile = () => {
const waitForTurnstile = (retries = 0) => {
if (retries > 50) {
console.error('[Turnstile] SDK 加载超时')
return
}
if (window.turnstile && !turnstileWidgetId.value) {
const container = document.getElementById('turnstile-container')
if (!container) return
try {
turnstileWidgetId.value = window.turnstile.render('#turnstile-container', {
sitekey: TURNSTILE_SITE_KEY,
theme: 'light',
callback: (token) => {
turnstileToken.value = token
},
'error-callback': () => {
turnstileToken.value = ''
},
'expired-callback': () => {
turnstileToken.value = ''
},
})
} catch (err) {
console.error('[Turnstile] 渲染错误:', err)
}
} else if (!window.turnstile) {
setTimeout(() => waitForTurnstile(retries + 1), 100)
}
}
waitForTurnstile()
}
const resetTurnstile = () => {
if (window.turnstile && turnstileWidgetId.value) {
window.turnstile.reset(turnstileWidgetId.value)
turnstileToken.value = ''
}
}
const destroyTurnstile = () => {
if (window.turnstile && turnstileWidgetId.value) {
window.turnstile.remove(turnstileWidgetId.value)
turnstileWidgetId.value = ''
turnstileToken.value = ''
}
}
// 切换到注册页时初始化 Turnstile切回登录时销毁
watch(mode, (newMode) => {
if (newMode === 'register') {
nextTick(() => initTurnstile())
} else {
destroyTurnstile()
}
})
const handleRegister = async () => {
const form = registerForm.value
// 表单校验
if (!form.name || !form.contactName || !form.contactMobile || !form.username || !form.password || !form.confirmPassword) {
error.value = '请填写所有字段'
return
}
if (form.name.length < 2 || form.name.length > 20) {
error.value = '租户名称长度必须介于 2 和 20 之间'
return
}
if (!/^1[3-9]\d{9}$/.test(form.contactMobile)) {
error.value = '请输入正确的手机号码'
return
}
if (form.username.length < 4 || form.username.length > 30) {
error.value = '账号长度必须介于 4 和 30 之间'
return
}
if (form.password.length < 5 || form.password.length > 20) {
error.value = '密码长度必须介于 5 和 20 之间'
return
}
if (/[<>"'|\\]/.test(form.password)) {
error.value = '密码不能包含非法字符:< > " \' \\ |'
return
}
if (form.password !== form.confirmPassword) {
error.value = '两次输入的密码不一致'
return
}
if (!turnstileToken.value) {
error.value = '请完成人机验证'
return
}
isLoading.value = true
error.value = ''
try {
await tenantRegister({
name: form.name,
contactName: form.contactName,
contactMobile: form.contactMobile,
username: form.username,
password: form.password,
turnstileToken: turnstileToken.value,
})
ElMessage.success('注册成功,请登录')
// 将租户名和账号回填到登录表单
credentials.value.tenantName = form.name
credentials.value.username = form.username
credentials.value.password = ''
// 清空注册表单
registerForm.value = {
name: '',
contactName: '',
contactMobile: '',
username: '',
password: '',
confirmPassword: '',
}
// 切换回登录
mode.value = 'login'
} catch (err) {
error.value = err.message || '注册失败,请稍后重试'
resetTurnstile()
} finally {
isLoading.value = false
}
}
</script> </script>
<style> <style>