2025-12-27 22:37:36 +08:00

425 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="signup-container">
<div class="signup-card">
<div class="signup-header">
<div class="logo">
<img src="/logo.svg" :alt="appName" width="48" height="48" />
</div>
<h1 class="title">{{ appName }}</h1>
</div>
<n-form
ref="formRef"
:model="formData"
:rules="rules"
size="medium"
:show-label="false"
@keyup.enter="handleSignup"
>
<n-form-item path="username">
<n-input
v-model:value="formData.username"
:placeholder="t('Username')"
:input-props="{ autocomplete: 'username' }"
>
<template #prefix>
<Icon icon="tabler:user" />
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input
v-model:value="formData.password"
type="password"
:placeholder="t('Password')"
:input-props="{ autocomplete: 'new-password' }"
show-password-on="click"
>
<template #prefix>
<Icon icon="tabler:lock" />
</template>
</n-input>
</n-form-item>
<n-form-item path="confirmPassword">
<n-input
v-model:value="formData.confirmPassword"
type="password"
:placeholder="t('Confirm Password')"
:input-props="{ autocomplete: 'new-password' }"
show-password-on="click"
>
<template #prefix>
<Icon icon="tabler:lock" />
</template>
</n-input>
</n-form-item>
<n-form-item path="email">
<n-input
v-model:value="formData.email"
:placeholder="isEnglish ? t('Email') : t('Email (Optional)')"
:input-props="{ autocomplete: 'email', type: 'email' }"
>
<template #prefix>
<Icon icon="tabler:mail" />
</template>
</n-input>
</n-form-item>
<n-form-item v-if="!isEnglish" path="phoneNumber">
<n-input
v-model:value="formData.phoneNumber"
:placeholder="t('Mobile')"
:input-props="{ autocomplete: 'tel' }"
>
<template #prefix>
<Icon icon="tabler:phone" />
</template>
</n-input>
</n-form-item>
<n-form-item>
<n-button
type="primary"
size="medium"
block
:loading="loading"
@click="handleSignup"
class="brand-button"
>
{{ t('Sign up') }}
</n-button>
</n-form-item>
</n-form>
<div class="signup-footer">
<n-text depth="3">
{{ t('Already have an account?') }}
<router-link to="/login" class="login-link">
{{ t('Login') }}
</router-link>
</n-text>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { NForm, NFormItem, NInput, NButton, NText, useMessage } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { useAuthStore } from '../../shared/stores/auth'
import { t, getCurrentLocale } from '../../shared/i18n'
const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()
const formRef = ref()
const loading = ref(false)
const formData = reactive({
username: '',
password: '',
confirmPassword: '',
email: '',
phoneNumber: ''
})
const validatePasswordMatch = (_rule: any, value: string) => {
if (value !== formData.password) {
return new Error(t('Passwords do not match'))
}
return true
}
const isEnglish = computed(() => getCurrentLocale() === 'en-US')
const rules = computed(() => {
const rules: any = {
username: [
{ required: true, message: t('Please enter username'), trigger: 'blur' },
{ min: 3, message: t('Username must be at least 3 characters'), trigger: 'blur' }
],
password: [
{ required: true, message: t('Please enter password'), trigger: 'blur' },
{ min: 6, message: t('Password must be at least 6 characters'), trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: t('Please confirm password'), trigger: 'blur' },
{ validator: validatePasswordMatch, trigger: 'blur' }
]
}
// 英文版email必填手机号可选
if (isEnglish.value) {
rules.email = [
{ required: true, message: t('Please enter email'), trigger: 'blur' },
{
validator: (_rule: any, value: string) => {
// required规则已处理空值这里只验证格式
if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return new Error(t('Please enter a valid email address'))
}
return true
},
trigger: 'blur'
}
]
rules.phoneNumber = [
{
validator: (_rule: any, value: string) => {
if (!value) return true
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(value)) {
return new Error(t('Please enter a valid phone number'))
}
return true
},
trigger: 'blur'
}
]
} else {
// 中文版email可选手机号必填
rules.email = [
{
validator: (_rule: any, value: string) => {
if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return new Error(t('Please enter a valid email address'))
}
return true
},
trigger: 'blur'
}
]
rules.phoneNumber = [
{ required: true, message: t('Please enter phone number'), trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: t('Please enter a valid phone number'),
trigger: 'blur'
}
]
}
return rules
})
const getReferrerIfAny = () => {
try {
return document.referrer || ''
} catch {
return ''
}
}
const handleSignup = async () => {
try {
await formRef.value?.validate()
loading.value = true
const response = await fetch('/api/action/jcloud.api.account.signup_with_username', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
username: formData.username,
password: formData.password,
email: formData.email || null,
phone_number: isEnglish.value ? (formData.phoneNumber || null) : formData.phoneNumber || null,
referrer: getReferrerIfAny(),
product: router.currentRoute.value.query.product || null
})
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorMessage = errorData?.messages?.length
? errorData.messages.join('\n')
: (errorData?.message || errorData?.exc || '注册失败,请检查您的信息')
throw new Error(errorMessage)
}
const data = await response.json()
if (data && data.success === false) {
throw new Error(data.message || '注册失败,请检查您的信息')
}
localStorage.setItem('login_email', formData.username || formData.email)
if (data && data.dashboard_route) {
window.location.href = data.dashboard_route
} else {
window.location.href = '/'
}
} catch (error: any) {
console.error('注册异常:', error)
message.error(error.message || t('Sign up failed, please try again'))
} finally {
loading.value = false
}
}
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
onMounted(async () => {
// 初始化认证状态
await authStore.initAuth()
// 如果已经登录,直接跳转
if (authStore.isLoggedIn) {
router.push('/')
}
})
</script>
<style scoped>
.signup-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
padding: 20px;
}
.signup-card {
width: 100%;
max-width: 480px;
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.signup-header {
text-align: center;
margin-bottom: 20px;
}
:deep(.n-form-item) {
margin-bottom: 6px !important;
}
:deep(.n-form-item:last-child) {
margin-bottom: 0 !important;
}
/* 只在有错误时才显示反馈区域,减少默认间距 */
:deep(.n-form-item:not(.n-form-item--error) .n-form-item__feedback-wrapper) {
min-height: 0 !important;
margin-top: 0 !important;
padding-top: 0 !important;
}
:deep(.n-form-item--error .n-form-item__feedback-wrapper) {
margin-top: 4px !important;
min-height: auto !important;
}
.logo {
margin-bottom: 12px;
}
.logo img {
width: 40px;
height: 40px;
}
.title {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin: 0 0 8px 0;
}
.subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.signup-footer {
text-align: center;
margin-top: 16px;
padding-top: 16px;
}
.login-link {
color: #1fc76f;
text-decoration: none;
font-weight: 500;
margin-left: 4px;
transition: color 0.2s;
}
.login-link:hover {
color: #1fc76f;
text-decoration: underline;
}
:deep(.brand-button) {
background: #e6f8f0 !important;
border: 1px solid #1fc76f !important;
color: #0d684b !important;
}
:deep(.brand-button .n-button__border),
:deep(.brand-button .n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
:deep(.brand-button:hover) {
background: #dcfce7 !important;
border-color: #1fc76f !important;
color: #166534 !important;
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
}
:deep(.brand-button:hover .n-button__border),
:deep(.brand-button:hover .n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
:deep(.brand-button:active) {
background: #1fc76f !important;
border-color: #1fc76f !important;
color: white !important;
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2) !important;
}
:deep(.brand-button:active .n-button__border),
:deep(.brand-button:active .n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
:deep(.brand-button:focus) {
background: #e6f8f0 !important;
border-color: #1fc76f !important;
color: #0d684b !important;
}
:deep(.brand-button:focus .n-button__border),
:deep(.brand-button:focus .n-button__state-border) {
border: none !important;
border-color: transparent !important;
}
</style>