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
2fe8932c
Unverified
Commit
2fe8932c
authored
Feb 03, 2026
by
Call White
Committed by
GitHub
Feb 03, 2026
Browse files
Merge pull request #3 from cyhhao/main
merge to main
parents
2f2e76f9
adb77af1
Changes
267
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/auth/RegisterView.vue
View file @
2fe8932c
...
@@ -96,7 +96,7 @@
...
@@ -96,7 +96,7 @@
<
/div
>
<
/div
>
<!--
Promo
Code
Input
(
Optional
)
-->
<!--
Promo
Code
Input
(
Optional
)
-->
<
div
>
<
div
v
-
if
=
"
promoCodeEnabled
"
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
{{
t
(
'
auth.promoCodeLabel
'
)
}}
{{
t
(
'
auth.promoCodeLabel
'
)
}}
<
span
class
=
"
ml-1 text-xs font-normal text-gray-400 dark:text-dark-500
"
>
({{
t
(
'
common.optional
'
)
}}
)
<
/span
>
<
span
class
=
"
ml-1 text-xs font-normal text-gray-400 dark:text-dark-500
"
>
({{
t
(
'
common.optional
'
)
}}
)
<
/span
>
...
@@ -260,6 +260,7 @@ const showPassword = ref<boolean>(false)
...
@@ -260,6 +260,7 @@ const showPassword = ref<boolean>(false)
// Public settings
// Public settings
const
registrationEnabled
=
ref
<
boolean
>
(
true
)
const
registrationEnabled
=
ref
<
boolean
>
(
true
)
const
emailVerifyEnabled
=
ref
<
boolean
>
(
false
)
const
emailVerifyEnabled
=
ref
<
boolean
>
(
false
)
const
promoCodeEnabled
=
ref
<
boolean
>
(
true
)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
...
@@ -294,22 +295,25 @@ const errors = reactive({
...
@@ -294,22 +295,25 @@ const errors = reactive({
// ==================== Lifecycle ====================
// ==================== Lifecycle ====================
onMounted
(
async
()
=>
{
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
{
try
{
const
settings
=
await
getPublicSettings
()
const
settings
=
await
getPublicSettings
()
registrationEnabled
.
value
=
settings
.
registration_enabled
registrationEnabled
.
value
=
settings
.
registration_enabled
emailVerifyEnabled
.
value
=
settings
.
email_verify_enabled
emailVerifyEnabled
.
value
=
settings
.
email_verify_enabled
promoCodeEnabled
.
value
=
settings
.
promo_code_enabled
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
linuxdoOAuthEnabled
.
value
=
settings
.
linuxdo_oauth_enabled
linuxdoOAuthEnabled
.
value
=
settings
.
linuxdo_oauth_enabled
// Read promo code from URL parameter only if promo code is enabled
if
(
promoCodeEnabled
.
value
)
{
const
promoParam
=
route
.
query
.
promo
as
string
if
(
promoParam
)
{
formData
.
promo_code
=
promoParam
// Validate the promo code from URL
await
validatePromoCodeDebounced
(
promoParam
)
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
)
console
.
error
(
'
Failed to load public settings:
'
,
error
)
}
finally
{
}
finally
{
...
...
frontend/src/views/auth/ResetPasswordView.vue
0 → 100644
View file @
2fe8932c
<
template
>
<AuthLayout>
<div
class=
"space-y-6"
>
<!-- Title -->
<div
class=
"text-center"
>
<h2
class=
"text-2xl font-bold text-gray-900 dark:text-white"
>
{{
t
(
'
auth.resetPasswordTitle
'
)
}}
</h2>
<p
class=
"mt-2 text-sm text-gray-500 dark:text-dark-400"
>
{{
t
(
'
auth.resetPasswordHint
'
)
}}
</p>
</div>
<!-- Invalid Link State -->
<div
v-if=
"isInvalidLink"
class=
"space-y-6"
>
<div
class=
"rounded-xl border border-red-200 bg-red-50 p-6 dark:border-red-800/50 dark:bg-red-900/20"
>
<div
class=
"flex flex-col items-center gap-4 text-center"
>
<div
class=
"flex h-12 w-12 items-center justify-center rounded-full bg-red-100 dark:bg-red-800/50"
>
<Icon
name=
"exclamationCircle"
size=
"lg"
class=
"text-red-600 dark:text-red-400"
/>
</div>
<div>
<h3
class=
"text-lg font-semibold text-red-800 dark:text-red-200"
>
{{
t
(
'
auth.invalidResetLink
'
)
}}
</h3>
<p
class=
"mt-2 text-sm text-red-700 dark:text-red-300"
>
{{
t
(
'
auth.invalidResetLinkHint
'
)
}}
</p>
</div>
</div>
</div>
<div
class=
"text-center"
>
<router-link
to=
"/forgot-password"
class=
"inline-flex items-center gap-2 font-medium text-primary-600 transition-colors hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
>
{{
t
(
'
auth.requestNewResetLink
'
)
}}
</router-link>
</div>
</div>
<!-- Success State -->
<div
v-else-if=
"isSuccess"
class=
"space-y-6"
>
<div
class=
"rounded-xl border border-green-200 bg-green-50 p-6 dark:border-green-800/50 dark:bg-green-900/20"
>
<div
class=
"flex flex-col items-center gap-4 text-center"
>
<div
class=
"flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-800/50"
>
<Icon
name=
"checkCircle"
size=
"lg"
class=
"text-green-600 dark:text-green-400"
/>
</div>
<div>
<h3
class=
"text-lg font-semibold text-green-800 dark:text-green-200"
>
{{
t
(
'
auth.passwordResetSuccess
'
)
}}
</h3>
<p
class=
"mt-2 text-sm text-green-700 dark:text-green-300"
>
{{
t
(
'
auth.passwordResetSuccessHint
'
)
}}
</p>
</div>
</div>
</div>
<div
class=
"text-center"
>
<router-link
to=
"/login"
class=
"btn btn-primary inline-flex items-center gap-2"
>
<Icon
name=
"login"
size=
"md"
/>
{{
t
(
'
auth.signIn
'
)
}}
</router-link>
</div>
</div>
<!-- Form State -->
<form
v-else
@
submit.prevent=
"handleSubmit"
class=
"space-y-5"
>
<!-- Email (readonly) -->
<div>
<label
for=
"email"
class=
"input-label"
>
{{
t
(
'
auth.emailLabel
'
)
}}
</label>
<div
class=
"relative"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<Icon
name=
"mail"
size=
"md"
class=
"text-gray-400 dark:text-dark-500"
/>
</div>
<input
id=
"email"
:value=
"email"
type=
"email"
readonly
disabled
class=
"input pl-11 bg-gray-50 dark:bg-dark-700"
/>
</div>
</div>
<!-- New Password Input -->
<div>
<label
for=
"password"
class=
"input-label"
>
{{
t
(
'
auth.newPassword
'
)
}}
</label>
<div
class=
"relative"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<Icon
name=
"lock"
size=
"md"
class=
"text-gray-400 dark:text-dark-500"
/>
</div>
<input
id=
"password"
v-model=
"formData.password"
:type=
"showPassword ? 'text' : 'password'"
required
autocomplete=
"new-password"
:disabled=
"isLoading"
class=
"input pl-11 pr-11"
:class=
"
{ 'input-error': errors.password }"
:placeholder="t('auth.newPasswordPlaceholder')"
/>
<button
type=
"button"
@
click=
"showPassword = !showPassword"
class=
"absolute inset-y-0 right-0 flex items-center pr-3.5 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-dark-300"
>
<Icon
v-if=
"showPassword"
name=
"eyeOff"
size=
"md"
/>
<Icon
v-else
name=
"eye"
size=
"md"
/>
</button>
</div>
<p
v-if=
"errors.password"
class=
"input-error-text"
>
{{
errors
.
password
}}
</p>
</div>
<!-- Confirm Password Input -->
<div>
<label
for=
"confirmPassword"
class=
"input-label"
>
{{
t
(
'
auth.confirmPassword
'
)
}}
</label>
<div
class=
"relative"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<Icon
name=
"lock"
size=
"md"
class=
"text-gray-400 dark:text-dark-500"
/>
</div>
<input
id=
"confirmPassword"
v-model=
"formData.confirmPassword"
:type=
"showConfirmPassword ? 'text' : 'password'"
required
autocomplete=
"new-password"
:disabled=
"isLoading"
class=
"input pl-11 pr-11"
:class=
"
{ 'input-error': errors.confirmPassword }"
:placeholder="t('auth.confirmPasswordPlaceholder')"
/>
<button
type=
"button"
@
click=
"showConfirmPassword = !showConfirmPassword"
class=
"absolute inset-y-0 right-0 flex items-center pr-3.5 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-dark-300"
>
<Icon
v-if=
"showConfirmPassword"
name=
"eyeOff"
size=
"md"
/>
<Icon
v-else
name=
"eye"
size=
"md"
/>
</button>
</div>
<p
v-if=
"errors.confirmPassword"
class=
"input-error-text"
>
{{
errors
.
confirmPassword
}}
</p>
</div>
<!-- Error Message -->
<transition
name=
"fade"
>
<div
v-if=
"errorMessage"
class=
"rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-shrink-0"
>
<Icon
name=
"exclamationCircle"
size=
"md"
class=
"text-red-500"
/>
</div>
<p
class=
"text-sm text-red-700 dark:text-red-400"
>
{{
errorMessage
}}
</p>
</div>
</div>
</transition>
<!-- Submit Button -->
<button
type=
"submit"
:disabled=
"isLoading"
class=
"btn btn-primary w-full"
>
<svg
v-if=
"isLoading"
class=
"-ml-1 mr-2 h-4 w-4 animate-spin text-white"
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>
<Icon
v-else
name=
"checkCircle"
size=
"md"
class=
"mr-2"
/>
{{
isLoading
?
t
(
'
auth.resettingPassword
'
)
:
t
(
'
auth.resetPassword
'
)
}}
</button>
</form>
</div>
<!-- Footer -->
<template
#footer
>
<p
class=
"text-gray-500 dark:text-dark-400"
>
{{
t
(
'
auth.rememberedPassword
'
)
}}
<router-link
to=
"/login"
class=
"font-medium text-primary-600 transition-colors hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
>
{{
t
(
'
auth.signIn
'
)
}}
</router-link>
</p>
</
template
>
</AuthLayout>
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
useRoute
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
resetPassword
}
from
'
@/api/auth
'
const
{
t
}
=
useI18n
()
// ==================== Router & Stores ====================
const
route
=
useRoute
()
const
appStore
=
useAppStore
()
// ==================== State ====================
const
isLoading
=
ref
<
boolean
>
(
false
)
const
isSuccess
=
ref
<
boolean
>
(
false
)
const
errorMessage
=
ref
<
string
>
(
''
)
const
showPassword
=
ref
<
boolean
>
(
false
)
const
showConfirmPassword
=
ref
<
boolean
>
(
false
)
// URL parameters
const
email
=
ref
<
string
>
(
''
)
const
token
=
ref
<
string
>
(
''
)
const
formData
=
reactive
({
password
:
''
,
confirmPassword
:
''
})
const
errors
=
reactive
({
password
:
''
,
confirmPassword
:
''
})
// Check if the reset link is valid (has email and token)
const
isInvalidLink
=
computed
(()
=>
!
email
.
value
||
!
token
.
value
)
// ==================== Lifecycle ====================
onMounted
(()
=>
{
// Get email and token from URL query parameters
email
.
value
=
(
route
.
query
.
email
as
string
)
||
''
token
.
value
=
(
route
.
query
.
token
as
string
)
||
''
})
// ==================== Validation ====================
function
validateForm
():
boolean
{
errors
.
password
=
''
errors
.
confirmPassword
=
''
let
isValid
=
true
// Password validation
if
(
!
formData
.
password
)
{
errors
.
password
=
t
(
'
auth.passwordRequired
'
)
isValid
=
false
}
else
if
(
formData
.
password
.
length
<
6
)
{
errors
.
password
=
t
(
'
auth.passwordMinLength
'
)
isValid
=
false
}
// Confirm password validation
if
(
!
formData
.
confirmPassword
)
{
errors
.
confirmPassword
=
t
(
'
auth.confirmPasswordRequired
'
)
isValid
=
false
}
else
if
(
formData
.
password
!==
formData
.
confirmPassword
)
{
errors
.
confirmPassword
=
t
(
'
auth.passwordsDoNotMatch
'
)
isValid
=
false
}
return
isValid
}
// ==================== Form Handlers ====================
async
function
handleSubmit
():
Promise
<
void
>
{
errorMessage
.
value
=
''
if
(
!
validateForm
())
{
return
}
isLoading
.
value
=
true
try
{
await
resetPassword
({
email
:
email
.
value
,
token
:
token
.
value
,
new_password
:
formData
.
password
})
isSuccess
.
value
=
true
appStore
.
showSuccess
(
t
(
'
auth.passwordResetSuccess
'
))
}
catch
(
error
:
unknown
)
{
const
err
=
error
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
;
code
?:
string
}
}
}
// Check for invalid/expired token error
if
(
err
.
response
?.
data
?.
code
===
'
INVALID_RESET_TOKEN
'
)
{
errorMessage
.
value
=
t
(
'
auth.invalidOrExpiredToken
'
)
}
else
if
(
err
.
response
?.
data
?.
detail
)
{
errorMessage
.
value
=
err
.
response
.
data
.
detail
}
else
if
(
err
.
message
)
{
errorMessage
.
value
=
err
.
message
}
else
{
errorMessage
.
value
=
t
(
'
auth.resetPasswordFailed
'
)
}
appStore
.
showError
(
errorMessage
.
value
)
}
finally
{
isLoading
.
value
=
false
}
}
</
script
>
<
style
scoped
>
.fade-enter-active
,
.fade-leave-active
{
transition
:
all
0.3s
ease
;
}
.fade-enter-from
,
.fade-leave-to
{
opacity
:
0
;
transform
:
translateY
(
-8px
);
}
</
style
>
frontend/src/views/user/KeysView.vue
View file @
2fe8932c
...
@@ -133,6 +133,7 @@
...
@@ -133,6 +133,7 @@
</button>
</button>
<!-- Import to CC Switch Button -->
<!-- Import to CC Switch Button -->
<button
<button
v-if=
"!publicSettings?.hide_ccs_import_button"
@
click=
"importToCcswitch(row)"
@
click=
"importToCcswitch(row)"
class=
"flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
class=
"flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
>
>
...
...
frontend/src/views/user/ProfileView.vue
View file @
2fe8932c
...
@@ -15,6 +15,7 @@
...
@@ -15,6 +15,7 @@
</div>
</div>
<ProfileEditForm
:initial-username=
"user?.username || ''"
/>
<ProfileEditForm
:initial-username=
"user?.username || ''"
/>
<ProfilePasswordForm
/>
<ProfilePasswordForm
/>
<ProfileTotpCard
/>
</div>
</div>
</AppLayout>
</AppLayout>
</
template
>
</
template
>
...
@@ -27,6 +28,7 @@ import StatCard from '@/components/common/StatCard.vue'
...
@@ -27,6 +28,7 @@ import StatCard from '@/components/common/StatCard.vue'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfileEditForm
from
'
@/components/user/profile/ProfileEditForm.vue
'
import
ProfileEditForm
from
'
@/components/user/profile/ProfileEditForm.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
import
ProfileTotpCard
from
'
@/components/user/profile/ProfileTotpCard.vue
'
import
{
Icon
}
from
'
@/components/icons
'
import
{
Icon
}
from
'
@/components/icons
'
const
{
t
}
=
useI18n
();
const
authStore
=
useAuthStore
();
const
user
=
computed
(()
=>
authStore
.
user
)
const
{
t
}
=
useI18n
();
const
authStore
=
useAuthStore
();
const
user
=
computed
(()
=>
authStore
.
user
)
...
...
frontend/src/views/user/PurchaseSubscriptionView.vue
0 → 100644
View file @
2fe8932c
<
template
>
<AppLayout>
<div
class=
"purchase-page-layout"
>
<div
class=
"flex items-start justify-between gap-4"
>
<div>
<h2
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
purchase.title
'
)
}}
</h2>
<p
class=
"mt-1 text-sm text-gray-500 dark:text-dark-400"
>
{{
t
(
'
purchase.description
'
)
}}
</p>
</div>
<div
class=
"flex items-center gap-2"
>
<a
v-if=
"isValidUrl"
:href=
"purchaseUrl"
target=
"_blank"
rel=
"noopener noreferrer"
class=
"btn btn-secondary btn-sm"
>
<Icon
name=
"externalLink"
size=
"sm"
class=
"mr-1.5"
:stroke-width=
"2"
/>
{{
t
(
'
purchase.openInNewTab
'
)
}}
</a>
</div>
</div>
<div
class=
"card flex-1 min-h-0 overflow-hidden"
>
<div
v-if=
"loading"
class=
"flex h-full items-center justify-center py-12"
>
<div
class=
"h-8 w-8 animate-spin rounded-full border-2 border-primary-500 border-t-transparent"
></div>
</div>
<div
v-else-if=
"!purchaseEnabled"
class=
"flex h-full items-center justify-center p-10 text-center"
>
<div
class=
"max-w-md"
>
<div
class=
"mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
>
<Icon
name=
"creditCard"
size=
"lg"
class=
"text-gray-400"
/>
</div>
<h3
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
purchase.notEnabledTitle
'
)
}}
</h3>
<p
class=
"mt-2 text-sm text-gray-500 dark:text-dark-400"
>
{{
t
(
'
purchase.notEnabledDesc
'
)
}}
</p>
</div>
</div>
<div
v-else-if=
"!isValidUrl"
class=
"flex h-full items-center justify-center p-10 text-center"
>
<div
class=
"max-w-md"
>
<div
class=
"mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
>
<Icon
name=
"link"
size=
"lg"
class=
"text-gray-400"
/>
</div>
<h3
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
purchase.notConfiguredTitle
'
)
}}
</h3>
<p
class=
"mt-2 text-sm text-gray-500 dark:text-dark-400"
>
{{
t
(
'
purchase.notConfiguredDesc
'
)
}}
</p>
</div>
</div>
<iframe
v-else
:src=
"purchaseUrl"
class=
"h-full w-full border-0"
allowfullscreen
></iframe>
</div>
</div>
</AppLayout>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
ref
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
loading
=
ref
(
false
)
const
purchaseEnabled
=
computed
(()
=>
{
return
appStore
.
cachedPublicSettings
?.
purchase_subscription_enabled
??
false
})
const
purchaseUrl
=
computed
(()
=>
{
return
(
appStore
.
cachedPublicSettings
?.
purchase_subscription_url
||
''
).
trim
()
})
const
isValidUrl
=
computed
(()
=>
{
const
url
=
purchaseUrl
.
value
return
url
.
startsWith
(
'
http://
'
)
||
url
.
startsWith
(
'
https://
'
)
})
onMounted
(
async
()
=>
{
if
(
appStore
.
publicSettingsLoaded
)
return
loading
.
value
=
true
try
{
await
appStore
.
fetchPublicSettings
()
}
finally
{
loading
.
value
=
false
}
})
</
script
>
<
style
scoped
>
.purchase-page-layout
{
@apply
flex
flex-col
gap-6;
height
:
calc
(
100vh
-
64px
-
4rem
);
/* 减去 header + lg:p-8 的上下padding */
}
</
style
>
frontend/tsconfig.json
View file @
2fe8932c
...
@@ -21,5 +21,6 @@
...
@@ -21,5 +21,6 @@
"types"
:
[
"vite/client"
]
"types"
:
[
"vite/client"
]
},
},
"include"
:
[
"src/**/*.ts"
,
"src/**/*.tsx"
,
"src/**/*.vue"
],
"include"
:
[
"src/**/*.ts"
,
"src/**/*.tsx"
,
"src/**/*.vue"
],
"exclude"
:
[
"src/**/__tests__/**"
,
"src/**/*.spec.ts"
,
"src/**/*.test.ts"
],
"references"
:
[{
"path"
:
"./tsconfig.node.json"
}]
"references"
:
[{
"path"
:
"./tsconfig.node.json"
}]
}
}
frontend/vite.config.ts
View file @
2fe8932c
import
{
defineConfig
,
Plugin
}
from
'
vite
'
import
{
defineConfig
,
loadEnv
,
Plugin
}
from
'
vite
'
import
vue
from
'
@vitejs/plugin-vue
'
import
vue
from
'
@vitejs/plugin-vue
'
import
checker
from
'
vite-plugin-checker
'
import
checker
from
'
vite-plugin-checker
'
import
{
resolve
}
from
'
path
'
import
{
resolve
}
from
'
path
'
...
@@ -7,9 +7,7 @@ import { resolve } from 'path'
...
@@ -7,9 +7,7 @@ import { resolve } from 'path'
* Vite 插件:开发模式下注入公开配置到 index.html
* Vite 插件:开发模式下注入公开配置到 index.html
* 与生产模式的后端注入行为保持一致,消除闪烁
* 与生产模式的后端注入行为保持一致,消除闪烁
*/
*/
function
injectPublicSettings
():
Plugin
{
function
injectPublicSettings
(
backendUrl
:
string
):
Plugin
{
const
backendUrl
=
process
.
env
.
VITE_DEV_PROXY_TARGET
||
'
http://localhost:8080
'
return
{
return
{
name
:
'
inject-public-settings
'
,
name
:
'
inject-public-settings
'
,
transformIndexHtml
:
{
transformIndexHtml
:
{
...
@@ -35,15 +33,21 @@ function injectPublicSettings(): Plugin {
...
@@ -35,15 +33,21 @@ function injectPublicSettings(): Plugin {
}
}
}
}
export
default
defineConfig
({
export
default
defineConfig
(({
mode
})
=>
{
plugins
:
[
// 加载环境变量
vue
(),
const
env
=
loadEnv
(
mode
,
process
.
cwd
(),
''
)
checker
({
const
backendUrl
=
env
.
VITE_DEV_PROXY_TARGET
||
'
http://localhost:8080
'
typescript
:
true
,
const
devPort
=
Number
(
env
.
VITE_DEV_PORT
||
3000
)
vueTsc
:
true
}),
return
{
injectPublicSettings
()
plugins
:
[
],
vue
(),
checker
({
typescript
:
true
,
vueTsc
:
true
}),
injectPublicSettings
(
backendUrl
)
],
resolve
:
{
resolve
:
{
alias
:
{
alias
:
{
'
@
'
:
resolve
(
__dirname
,
'
src
'
),
'
@
'
:
resolve
(
__dirname
,
'
src
'
),
...
@@ -102,17 +106,18 @@ export default defineConfig({
...
@@ -102,17 +106,18 @@ export default defineConfig({
}
}
}
}
},
},
server
:
{
server
:
{
host
:
'
0.0.0.0
'
,
host
:
'
0.0.0.0
'
,
port
:
Number
(
process
.
env
.
VITE_DEV_PORT
||
3000
),
port
:
devPort
,
proxy
:
{
proxy
:
{
'
/api
'
:
{
'
/api
'
:
{
target
:
process
.
env
.
VITE_DEV_PROXY_TARGET
||
'
http://localhost:8080
'
,
target
:
backendUrl
,
changeOrigin
:
true
changeOrigin
:
true
},
},
'
/setup
'
:
{
'
/setup
'
:
{
target
:
process
.
env
.
VITE_DEV_PROXY_TARGET
||
'
http://localhost:8080
'
,
target
:
backendUrl
,
changeOrigin
:
true
changeOrigin
:
true
}
}
}
}
}
}
}
...
...
Prev
1
…
10
11
12
13
14
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