Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
15884f36
Commit
15884f36
authored
Jan 10, 2026
by
shaw
Browse files
Merge branch 'longgexx/main'
parents
3d498413
b03fb9c2
Changes
81
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/auth/RegisterView.vue
View file @
15884f36
...
...
@@ -95,6 +95,57 @@
<
/p
>
<
/div
>
<!--
Promo
Code
Input
(
Optional
)
-->
<
div
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
{{
t
(
'
auth.promoCodeLabel
'
)
}}
<
span
class
=
"
ml-1 text-xs font-normal text-gray-400 dark:text-dark-500
"
>
({{
t
(
'
common.optional
'
)
}}
)
<
/span
>
<
/label
>
<
div
class
=
"
relative
"
>
<
div
class
=
"
pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5
"
>
<
Icon
name
=
"
gift
"
size
=
"
md
"
:
class
=
"
promoValidation.valid ? 'text-green-500' : 'text-gray-400 dark:text-dark-500'
"
/>
<
/div
>
<
input
id
=
"
promo_code
"
v
-
model
=
"
formData.promo_code
"
type
=
"
text
"
:
disabled
=
"
isLoading
"
class
=
"
input pl-11 pr-10
"
:
class
=
"
{
'border-green-500 focus:border-green-500 focus:ring-green-500': promoValidation.valid,
'border-red-500 focus:border-red-500 focus:ring-red-500': promoValidation.invalid
}
"
:
placeholder
=
"
t('auth.promoCodePlaceholder')
"
@
input
=
"
handlePromoCodeInput
"
/>
<!--
Validation
indicator
-->
<
div
v
-
if
=
"
promoValidating
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
svg
class
=
"
h-4 w-4 animate-spin text-gray-400
"
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
>
<
/div
>
<
div
v
-
else
-
if
=
"
promoValidation.valid
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
checkCircle
"
size
=
"
md
"
class
=
"
text-green-500
"
/>
<
/div
>
<
div
v
-
else
-
if
=
"
promoValidation.invalid
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
exclamationCircle
"
size
=
"
md
"
class
=
"
text-red-500
"
/>
<
/div
>
<
/div
>
<!--
Promo
code
validation
result
-->
<
transition
name
=
"
fade
"
>
<
div
v
-
if
=
"
promoValidation.valid
"
class
=
"
mt-2 flex items-center gap-2 rounded-lg bg-green-50 px-3 py-2 dark:bg-green-900/20
"
>
<
Icon
name
=
"
gift
"
size
=
"
sm
"
class
=
"
text-green-600 dark:text-green-400
"
/>
<
span
class
=
"
text-sm text-green-700 dark:text-green-400
"
>
{{
t
(
'
auth.promoCodeValid
'
,
{
amount
:
promoValidation
.
bonusAmount
?.
toFixed
(
2
)
}
)
}}
<
/span
>
<
/div
>
<
p
v
-
else
-
if
=
"
promoValidation.invalid
"
class
=
"
input-error-text
"
>
{{
promoValidation
.
message
}}
<
/p
>
<
/transition
>
<
/div
>
<!--
Turnstile
Widget
-->
<
div
v
-
if
=
"
turnstileEnabled && turnstileSiteKey
"
>
<
TurnstileWidget
...
...
@@ -180,21 +231,22 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
onMounted
}
from
'
vue
'
import
{
useRouter
}
from
'
vue-router
'
import
{
ref
,
reactive
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useRouter
,
useRoute
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
LinuxDoOAuthSection
from
'
@/components/auth/LinuxDoOAuthSection.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
getPublicSettings
}
from
'
@/api/auth
'
import
{
getPublicSettings
,
validatePromoCode
}
from
'
@/api/auth
'
const
{
t
}
=
useI18n
()
// ==================== Router & Stores ====================
const
router
=
useRouter
()
const
route
=
useRoute
()
const
authStore
=
useAuthStore
()
const
appStore
=
useAppStore
()
...
...
@@ -217,9 +269,20 @@ const linuxdoOAuthEnabled = ref<boolean>(false)
const
turnstileRef
=
ref
<
InstanceType
<
typeof
TurnstileWidget
>
|
null
>
(
null
)
const
turnstileToken
=
ref
<
string
>
(
''
)
// Promo code validation
const
promoValidating
=
ref
<
boolean
>
(
false
)
const
promoValidation
=
reactive
({
valid
:
false
,
invalid
:
false
,
bonusAmount
:
null
as
number
|
null
,
message
:
''
}
)
let
promoValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
formData
=
reactive
({
email
:
''
,
password
:
''
password
:
''
,
promo_code
:
''
}
)
const
errors
=
reactive
({
...
...
@@ -231,6 +294,14 @@ const errors = reactive({
// ==================== Lifecycle ====================
onMounted
(
async
()
=>
{
// Read promo code from URL parameter
const
promoParam
=
route
.
query
.
promo
as
string
if
(
promoParam
)
{
formData
.
promo_code
=
promoParam
// Validate the promo code from URL
await
validatePromoCodeDebounced
(
promoParam
)
}
try
{
const
settings
=
await
getPublicSettings
()
registrationEnabled
.
value
=
settings
.
registration_enabled
...
...
@@ -246,6 +317,85 @@ onMounted(async () => {
}
}
)
onUnmounted
(()
=>
{
if
(
promoValidateTimeout
)
{
clearTimeout
(
promoValidateTimeout
)
}
}
)
// ==================== Promo Code Validation ====================
function
handlePromoCodeInput
():
void
{
const
code
=
formData
.
promo_code
.
trim
()
// Clear previous validation
promoValidation
.
valid
=
false
promoValidation
.
invalid
=
false
promoValidation
.
bonusAmount
=
null
promoValidation
.
message
=
''
if
(
!
code
)
{
promoValidating
.
value
=
false
return
}
// Debounce validation
if
(
promoValidateTimeout
)
{
clearTimeout
(
promoValidateTimeout
)
}
promoValidateTimeout
=
setTimeout
(()
=>
{
validatePromoCodeDebounced
(
code
)
}
,
500
)
}
async
function
validatePromoCodeDebounced
(
code
:
string
):
Promise
<
void
>
{
if
(
!
code
.
trim
())
return
promoValidating
.
value
=
true
try
{
const
result
=
await
validatePromoCode
(
code
)
if
(
result
.
valid
)
{
promoValidation
.
valid
=
true
promoValidation
.
invalid
=
false
promoValidation
.
bonusAmount
=
result
.
bonus_amount
||
0
promoValidation
.
message
=
''
}
else
{
promoValidation
.
valid
=
false
promoValidation
.
invalid
=
true
promoValidation
.
bonusAmount
=
null
// 根据错误码显示对应的翻译
promoValidation
.
message
=
getPromoErrorMessage
(
result
.
error_code
)
}
}
catch
(
error
)
{
console
.
error
(
'
Failed to validate promo code:
'
,
error
)
promoValidation
.
valid
=
false
promoValidation
.
invalid
=
true
promoValidation
.
message
=
t
(
'
auth.promoCodeInvalid
'
)
}
finally
{
promoValidating
.
value
=
false
}
}
function
getPromoErrorMessage
(
errorCode
?:
string
):
string
{
switch
(
errorCode
)
{
case
'
PROMO_CODE_NOT_FOUND
'
:
return
t
(
'
auth.promoCodeNotFound
'
)
case
'
PROMO_CODE_EXPIRED
'
:
return
t
(
'
auth.promoCodeExpired
'
)
case
'
PROMO_CODE_DISABLED
'
:
return
t
(
'
auth.promoCodeDisabled
'
)
case
'
PROMO_CODE_MAX_USED
'
:
return
t
(
'
auth.promoCodeMaxUsed
'
)
case
'
PROMO_CODE_ALREADY_USED
'
:
return
t
(
'
auth.promoCodeAlreadyUsed
'
)
default
:
return
t
(
'
auth.promoCodeInvalid
'
)
}
}
// ==================== Turnstile Handlers ====================
function
onTurnstileVerify
(
token
:
string
):
void
{
...
...
@@ -316,6 +466,20 @@ async function handleRegister(): Promise<void> {
return
}
// Check promo code validation status
if
(
formData
.
promo_code
.
trim
())
{
// If promo code is being validated, wait
if
(
promoValidating
.
value
)
{
errorMessage
.
value
=
t
(
'
auth.promoCodeValidating
'
)
return
}
// If promo code is invalid, block submission
if
(
promoValidation
.
invalid
)
{
errorMessage
.
value
=
t
(
'
auth.promoCodeInvalidCannotRegister
'
)
return
}
}
isLoading
.
value
=
true
try
{
...
...
@@ -327,7 +491,8 @@ async function handleRegister(): Promise<void> {
JSON
.
stringify
({
email
:
formData
.
email
,
password
:
formData
.
password
,
turnstile_token
:
turnstileToken
.
value
turnstile_token
:
turnstileToken
.
value
,
promo_code
:
formData
.
promo_code
||
undefined
}
)
)
...
...
@@ -340,7 +505,8 @@ async function handleRegister(): Promise<void> {
await
authStore
.
register
({
email
:
formData
.
email
,
password
:
formData
.
password
,
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
,
promo_code
:
formData
.
promo_code
||
undefined
}
)
// Show success toast
...
...
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment