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
3f05ef2a
Unverified
Commit
3f05ef2a
authored
Apr 26, 2026
by
Oliver Li
Committed by
GitHub
Apr 26, 2026
Browse files
Merge branch 'Wei-Shaw:main' into vertex
parents
6d11f9ed
c056db74
Changes
47
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/auth/RegisterView.vue
View file @
3f05ef2a
...
...
@@ -15,17 +15,20 @@
<
LinuxDoOAuthSection
v
-
if
=
"
linuxdoOAuthEnabled
"
:
disabled
=
"
isLoading
"
:
aff
-
code
=
"
formData.aff_code
"
:
show
-
divider
=
"
false
"
/>
<
WechatOAuthSection
v
-
if
=
"
wechatOAuthEnabled
"
:
disabled
=
"
isLoading
"
:
aff
-
code
=
"
formData.aff_code
"
:
show
-
divider
=
"
false
"
/>
<
OidcOAuthSection
v
-
if
=
"
oidcOAuthEnabled
"
:
disabled
=
"
isLoading
"
:
provider
-
name
=
"
oidcOAuthProviderName
"
:
aff
-
code
=
"
formData.aff_code
"
:
show
-
divider
=
"
false
"
/>
<
div
class
=
"
flex items-center gap-3
"
>
...
...
@@ -293,6 +296,11 @@ import {
isRegistrationEmailSuffixAllowed
,
normalizeRegistrationEmailSuffixWhitelist
}
from
'
@/utils/registrationEmailPolicy
'
import
{
clearAffiliateReferralCode
,
loadAffiliateReferralCode
,
resolveAffiliateReferralCode
}
from
'
@/utils/oauthAffiliate
'
const
{
t
,
locale
}
=
useI18n
()
...
...
@@ -378,9 +386,19 @@ watch(validationToastMessage, (value, previousValue) => {
}
}
)
function
syncAffiliateReferralCode
():
string
{
const
code
=
resolveAffiliateReferralCode
(
route
.
query
.
aff
,
route
.
query
.
aff_code
)
if
(
code
)
{
formData
.
aff_code
=
code
}
return
code
}
// ==================== Lifecycle ====================
onMounted
(
async
()
=>
{
syncAffiliateReferralCode
()
try
{
const
settings
=
await
getPublicSettings
()
registrationEnabled
.
value
=
settings
.
registration_enabled
...
...
@@ -407,10 +425,7 @@ onMounted(async () => {
await
validatePromoCodeDebounced
(
promoParam
)
}
}
const
affParam
=
(
route
.
query
.
aff
as
string
)
||
(
route
.
query
.
aff_code
as
string
)
if
(
affParam
)
{
formData
.
aff_code
=
affParam
.
trim
()
}
syncAffiliateReferralCode
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
)
}
finally
{
...
...
@@ -418,6 +433,13 @@ onMounted(async () => {
}
}
)
watch
(
()
=>
[
route
.
query
.
aff
,
route
.
query
.
aff_code
],
()
=>
{
syncAffiliateReferralCode
()
}
)
onUnmounted
(()
=>
{
if
(
promoValidateTimeout
)
{
clearTimeout
(
promoValidateTimeout
)
...
...
@@ -702,6 +724,11 @@ async function handleRegister(): Promise<void> {
isLoading
.
value
=
true
try
{
const
affCode
=
formData
.
aff_code
.
trim
()
||
loadAffiliateReferralCode
()
if
(
affCode
)
{
formData
.
aff_code
=
affCode
}
// If email verification is enabled, redirect to verification page
if
(
emailVerifyEnabled
.
value
)
{
// Store registration data in sessionStorage
...
...
@@ -713,7 +740,7 @@ async function handleRegister(): Promise<void> {
turnstile_token
:
turnstileToken
.
value
,
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
,
...(
formData
.
aff
_c
ode
?
{
aff_code
:
formData
.
aff
_c
ode
}
:
{
}
)
...(
aff
C
ode
?
{
aff_code
:
aff
C
ode
}
:
{
}
)
}
)
)
...
...
@@ -729,8 +756,9 @@ async function handleRegister(): Promise<void> {
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
,
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
,
...(
formData
.
aff
_c
ode
?
{
aff_code
:
formData
.
aff
_c
ode
}
:
{
}
)
...(
aff
C
ode
?
{
aff_code
:
aff
C
ode
}
:
{
}
)
}
)
clearAffiliateReferralCode
()
// Show success toast
appStore
.
showSuccess
(
t
(
'
auth.accountCreatedSuccess
'
,
{
siteName
:
siteName
.
value
}
))
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
3f05ef2a
...
...
@@ -340,6 +340,11 @@ import {
type
OAuthTokenResponse
,
type
PendingOAuthExchangeResponse
}
from
'
@/api/auth
'
import
{
clearAllAffiliateReferralCodes
,
loadOAuthAffiliateCode
,
oauthAffiliatePayload
}
from
'
@/utils/oauthAffiliate
'
const
route
=
useRoute
()
const
router
=
useRouter
()
...
...
@@ -802,6 +807,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
clearPendingAuthSession
()
clearAllAffiliateReferralCodes
()
appStore
.
showSuccess
(
bindSuccessMessage
)
await
router
.
replace
(
bindRedirect
)
return
...
...
@@ -813,6 +819,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
persistOAuthTokenContext
(
completion
)
await
authStore
.
setToken
(
completion
.
access_token
)
clearAllAffiliateReferralCodes
()
appStore
.
showSuccess
(
t
(
'
auth.loginSuccess
'
))
await
router
.
replace
(
redirect
)
}
...
...
@@ -861,18 +868,20 @@ async function handleSubmitInvitation() {
isSubmitting
.
value
=
true
try
{
const
affCode
=
loadOAuthAffiliateCode
()
const
decision
=
currentAdoptionDecision
()
const
completion
:
PendingWeChatCompletion
=
legacyPendingOAuthToken
.
value
?
(
await
apiClient
.
post
<
PendingWeChatCompletion
>
(
'
/auth/oauth/wechat/complete-registration
'
,
{
pending_oauth_token
:
legacyPendingOAuthToken
.
value
,
invitation_code
:
invitationCode
.
value
.
trim
(),
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
oauthAffiliatePayload
(
affCode
),
...
serializeAdoptionDecision
(
decision
)
}
)
).
data
:
await
completeWeChatOAuthRegistration
(
invitationCode
.
value
.
trim
(),
currentAdoptionDecision
()
)
:
affCode
?
await
completeWeChatOAuthRegistration
(
invitationCode
.
value
.
trim
(),
decision
,
affCode
)
:
await
completeWeChatOAuthRegistration
(
invitationCode
.
value
.
trim
(),
decision
)
await
finalizePendingAccountResponse
(
completion
)
}
catch
(
e
:
unknown
)
{
const
err
=
e
as
{
message
?:
string
;
response
?:
{
data
?:
{
message
?:
string
}
}
}
...
...
@@ -907,6 +916,7 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
invitation_code
:
payload
.
invitationCode
||
undefined
,
...
oauthAffiliatePayload
(
loadOAuthAffiliateCode
()),
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
}
)
await
finalizePendingAccountResponse
(
data
)
...
...
@@ -955,6 +965,7 @@ async function handleSubmitTotpChallenge() {
}
)
persistOAuthTokenContext
(
completion
)
await
authStore
.
setToken
(
completion
.
access_token
)
clearAllAffiliateReferralCodes
()
appStore
.
showSuccess
(
t
(
'
auth.loginSuccess
'
))
await
router
.
replace
(
redirectTo
.
value
)
}
catch
(
e
:
unknown
)
{
...
...
@@ -1015,6 +1026,7 @@ onMounted(async () => {
if
(
legacyLogin
)
{
persistOAuthTokenContext
(
legacyLogin
)
await
authStore
.
setToken
(
legacyLogin
.
access_token
)
clearAllAffiliateReferralCodes
()
appStore
.
showSuccess
(
t
(
'
auth.loginSuccess
'
))
await
router
.
replace
(
redirect
)
return
...
...
frontend/src/views/auth/__tests__/EmailVerifyView.spec.ts
View file @
3f05ef2a
...
...
@@ -112,6 +112,7 @@ describe('EmailVerifyView', () => {
apiClientPostMock
.
mockReset
()
authStoreState
.
pendingAuthSession
=
null
sessionStorage
.
clear
()
localStorage
.
clear
()
getPublicSettingsMock
.
mockResolvedValue
({
turnstile_enabled
:
false
,
...
...
@@ -136,6 +137,7 @@ describe('EmailVerifyView', () => {
JSON
.
stringify
({
email
:
'
fresh@example.com
'
,
password
:
'
secret-123
'
,
aff_code
:
'
AFF123
'
,
})
)
...
...
@@ -334,6 +336,7 @@ describe('EmailVerifyView', () => {
email
:
'
fresh@example.com
'
,
password
:
'
secret-123
'
,
verify_code
:
'
123456
'
,
aff_code
:
'
AFF123
'
,
})
expect
(
persistOAuthTokenContextMock
).
toHaveBeenCalledWith
({
access_token
:
'
oauth-access-token
'
,
...
...
frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
View file @
3f05ef2a
...
...
@@ -93,6 +93,7 @@ describe('LinuxDoCallbackView', () => {
})
window
.
location
.
hash
=
''
localStorage
.
clear
()
sessionStorage
.
clear
()
})
it
(
'
accepts the legacy fragment token success callback without pending-session exchange
'
,
async
()
=>
{
...
...
frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
View file @
3f05ef2a
...
...
@@ -97,6 +97,7 @@ describe('OidcCallbackView', () => {
})
window
.
location
.
hash
=
''
localStorage
.
clear
()
sessionStorage
.
clear
()
})
it
(
'
accepts the legacy fragment token success callback without pending-session exchange
'
,
async
()
=>
{
...
...
frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
View file @
3f05ef2a
...
...
@@ -172,6 +172,7 @@ describe('WechatCallbackView', () => {
appStoreState
.
cachedPublicSettings
=
null
appStoreState
.
publicSettingsLoaded
=
false
localStorage
.
clear
()
sessionStorage
.
clear
()
locationState
.
current
=
{
href
:
'
http://localhost/auth/wechat/callback
'
,
hash
:
''
,
...
...
frontend/src/views/user/AffiliateView.vue
View file @
3f05ef2a
...
...
@@ -9,21 +9,17 @@
<template
v-else-if=
"detail"
>
<div
class=
"grid gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<!-- 返利比例:用主色突出,让用户一眼看到「能拿多少」 -->
<div
class=
"card relative overflow-hidden p-5"
>
<div
class=
"absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary-500/10"
></div>
<div
class=
"relative"
>
<p
class=
"flex items-center gap-1.5 text-sm text-gray-500 dark:text-dark-400"
>
<Icon
name=
"dollar"
size=
"sm"
class=
"text-primary-500"
/>
{{
t
(
'
affiliate.stats.rebateRate
'
)
}}
</p>
<p
class=
"mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400"
>
{{
formattedRebateRate
}}
<span
class=
"ml-0.5 text-base font-medium"
>
%
</span>
</p>
<p
class=
"mt-1 text-xs text-gray-400 dark:text-dark-500"
>
{{
t
(
'
affiliate.stats.rebateRateHint
'
)
}}
</p>
</div>
<div
class=
"card p-5"
>
<p
class=
"flex items-center gap-1.5 text-sm text-gray-500 dark:text-dark-400"
>
<Icon
name=
"dollar"
size=
"sm"
class=
"text-primary-500"
/>
{{
t
(
'
affiliate.stats.rebateRate
'
)
}}
</p>
<p
class=
"mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400"
>
{{
formattedRebateRate
}}
<span
class=
"ml-0.5 text-base font-medium"
>
%
</span>
</p>
<p
class=
"mt-1 text-xs text-gray-400 dark:text-dark-500"
>
{{
t
(
'
affiliate.stats.rebateRateHint
'
)
}}
</p>
</div>
<div
class=
"card p-5"
>
<p
class=
"text-sm text-gray-500 dark:text-dark-400"
>
{{
t
(
'
affiliate.stats.invitedUsers
'
)
}}
</p>
...
...
@@ -42,6 +38,9 @@
<p
class=
"mt-2 text-2xl font-semibold text-gray-900 dark:text-white"
>
{{
formatCurrency
(
detail
.
aff_history_quota
)
}}
</p>
<p
v-if=
"detail.aff_frozen_quota > 0"
class=
"mt-1 text-xs text-amber-600 dark:text-amber-400"
>
{{
t
(
'
affiliate.stats.frozenQuota
'
)
}}
:
{{
formatCurrency
(
detail
.
aff_frozen_quota
)
}}
</p>
</div>
</div>
...
...
@@ -79,6 +78,7 @@
<li>
1.
{{
t
(
'
affiliate.tips.line1
'
)
}}
</li>
<li>
2.
{{
t
(
'
affiliate.tips.line2
'
,
{
rate
:
`${formattedRebateRate
}
%`
}
)
}}
<
/li
>
<
li
>
3
.
{{
t
(
'
affiliate.tips.line3
'
)
}}
<
/li
>
<
li
v
-
if
=
"
detail.aff_frozen_quota > 0
"
>
4
.
{{
t
(
'
affiliate.tips.line4
'
)
}}
<
/li
>
<
/ul
>
<
/div
>
<
/div
>
...
...
@@ -115,6 +115,7 @@
<
tr
class
=
"
border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-dark-400
"
>
<
th
class
=
"
px-3 py-2 font-medium
"
>
{{
t
(
'
affiliate.invitees.columns.email
'
)
}}
<
/th
>
<
th
class
=
"
px-3 py-2 font-medium
"
>
{{
t
(
'
affiliate.invitees.columns.username
'
)
}}
<
/th
>
<
th
class
=
"
px-3 py-2 font-medium text-right
"
>
{{
t
(
'
affiliate.invitees.columns.rebate
'
)
}}
<
/th
>
<
th
class
=
"
px-3 py-2 font-medium
"
>
{{
t
(
'
affiliate.invitees.columns.joinedAt
'
)
}}
<
/th
>
<
/tr
>
<
/thead
>
...
...
@@ -126,6 +127,7 @@
>
<
td
class
=
"
px-3 py-3 text-gray-900 dark:text-white
"
>
{{
item
.
email
||
'
-
'
}}
<
/td
>
<
td
class
=
"
px-3 py-3 text-gray-700 dark:text-gray-300
"
>
{{
item
.
username
||
'
-
'
}}
<
/td
>
<
td
class
=
"
px-3 py-3 text-right font-medium text-emerald-600 dark:text-emerald-400
"
>
{{
formatCurrency
(
item
.
total_rebate
)
}}
<
/td
>
<
td
class
=
"
px-3 py-3 text-gray-700 dark:text-gray-300
"
>
{{
formatDateTime
(
item
.
created_at
)
||
'
-
'
}}
<
/td
>
<
/tr
>
<
/tbody
>
...
...
Prev
1
2
3
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