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
fb313356
Commit
fb313356
authored
Jan 05, 2026
by
yangjianbo
Browse files
Merge branch 'main' into test-dev
parents
048ed061
91f9d4c7
Changes
84
Show whitespace changes
Inline
Side-by-side
frontend/src/views/user/KeysView.vue
View file @
fb313356
...
...
@@ -141,7 +141,7 @@
<
template
#cell-status=
"{ value }"
>
<span
:class=
"['badge', value === 'active' ? 'badge-success' : 'badge-gray']"
>
{{
value
}}
{{
t
(
'
admin.accounts.status.
'
+
value
)
}}
</span>
</
template
>
...
...
@@ -335,12 +335,14 @@
/>
<span
v-else
class=
"text-gray-400"
>
{{
t
(
'
keys.selectGroup
'
)
}}
</span>
</
template
>
<
template
#option=
"{ option }"
>
<Group
Badge
<
template
#option=
"{ option
, selected
}"
>
<Group
OptionItem
:name=
"(option as unknown as GroupOption).label"
:platform=
"(option as unknown as GroupOption).platform"
:subscription-type=
"(option as unknown as GroupOption).subscriptionType"
:rate-multiplier=
"(option as unknown as GroupOption).rate"
:description=
"(option as unknown as GroupOption).description"
:selected=
"selected"
/>
</
template
>
</Select>
...
...
@@ -501,7 +503,8 @@
<div
v-if=
"groupSelectorKeyId !== null && dropdownPosition"
ref=
"dropdownRef"
class=
"animate-in fade-in slide-in-from-top-2 fixed z-[9999] w-64 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 duration-200 dark:bg-dark-800 dark:ring-white/10"
class=
"animate-in fade-in slide-in-from-top-2 fixed z-[100000020] w-64 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 duration-200 dark:bg-dark-800 dark:ring-white/10"
style=
"pointer-events: auto !important;"
:style=
"{ top: dropdownPosition.top + 'px', left: dropdownPosition.left + 'px' }"
>
<div
class=
"max-h-64 overflow-y-auto p-1.5"
>
...
...
@@ -516,26 +519,19 @@
? 'bg-primary-50 dark:bg-primary-900/20'
: 'hover:bg-gray-100 dark:hover:bg-dark-700'
]"
:title=
"option.description || undefined"
>
<Group
Badge
<Group
OptionItem
:name=
"option.label"
:platform=
"option.platform"
:subscription-type=
"option.subscriptionType"
:rate-multiplier=
"option.rate"
/>
<svg
v-if=
"
:description=
"option.description"
:selected=
"
selectedKeyForGroup?.group_id === option.value ||
(!selectedKeyForGroup?.group_id && option.value === null)
"
class=
"h-4 w-4 shrink-0 text-primary-600 dark:text-primary-400"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M5 13l4 4L19 7"
/>
</svg>
/>
</button>
</div>
</div>
...
...
@@ -562,6 +558,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import
Select
from
'
@/components/common/Select.vue
'
import
UseKeyModal
from
'
@/components/keys/UseKeyModal.vue
'
import
GroupBadge
from
'
@/components/common/GroupBadge.vue
'
import
GroupOptionItem
from
'
@/components/common/GroupOptionItem.vue
'
import
type
{
ApiKey
,
Group
,
PublicSettings
,
SubscriptionType
,
GroupPlatform
}
from
'
@/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
type
{
BatchApiKeyUsageStats
}
from
'
@/api/usage
'
...
...
@@ -570,6 +567,7 @@ import { formatDateTime } from '@/utils/format'
interface
GroupOption
{
value
:
number
label
:
string
description
:
string
|
null
rate
:
number
subscriptionType
:
SubscriptionType
platform
:
GroupPlatform
...
...
@@ -665,6 +663,7 @@ const groupOptions = computed(() =>
groups
.
value
.
map
((
group
)
=>
({
value
:
group
.
id
,
label
:
group
.
name
,
description
:
group
.
description
,
rate
:
group
.
rate_multiplier
,
subscriptionType
:
group
.
subscription_type
,
platform
:
group
.
platform
...
...
frontend/src/views/user/ProfileView.vue
View file @
fb313356
<
template
>
<AppLayout>
<div
class=
"mx-auto max-w-4xl space-y-6"
>
<!-- Account Stats Summary -->
<div
class=
"grid grid-cols-1 gap-6 sm:grid-cols-3"
>
<StatCard
:title=
"t('profile.accountBalance')"
:value=
"formatCurrency(user?.balance || 0)"
:icon=
"WalletIcon"
icon-variant=
"success"
/>
<StatCard
:title=
"t('profile.concurrencyLimit')"
:value=
"user?.concurrency || 0"
:icon=
"BoltIcon"
icon-variant=
"warning"
/>
<StatCard
:title=
"t('profile.memberSince')"
:value=
"formatDate(user?.created_at || '', 'YYYY-MM')"
:icon=
"CalendarIcon"
icon-variant=
"primary"
/>
<StatCard
:title=
"t('profile.accountBalance')"
:value=
"formatCurrency(user?.balance || 0)"
:icon=
"WalletIcon"
icon-variant=
"success"
/>
<StatCard
:title=
"t('profile.concurrencyLimit')"
:value=
"user?.concurrency || 0"
:icon=
"BoltIcon"
icon-variant=
"warning"
/>
<StatCard
:title=
"t('profile.memberSince')"
:value=
"formatDate(user?.created_at || '',
{ year: 'numeric', month: 'long' })" :icon="CalendarIcon" icon-variant="primary" />
</div>
<!-- User Information -->
<div
class=
"card overflow-hidden"
>
<div
class=
"border-b border-gray-100 bg-gradient-to-r from-primary-500/10 to-primary-600/5 px-6 py-5 dark:border-dark-700 dark:from-primary-500/20 dark:to-primary-600/10"
>
<div
class=
"flex items-center gap-4"
>
<!-- Avatar -->
<div
class=
"flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 text-2xl font-bold text-white shadow-lg shadow-primary-500/20"
>
{{
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
U
'
}}
</div>
<div
class=
"min-w-0 flex-1"
>
<h2
class=
"truncate text-lg font-semibold text-gray-900 dark:text-white"
>
{{
user
?.
email
}}
</h2>
<div
class=
"mt-1 flex items-center gap-2"
>
<span
:class=
"['badge', user?.role === 'admin' ? 'badge-primary' : 'badge-gray']"
>
{{
user
?.
role
===
'
admin
'
?
t
(
'
profile.administrator
'
)
:
t
(
'
profile.user
'
)
}}
</span>
<span
:class=
"['badge', user?.status === 'active' ? 'badge-success' : 'badge-danger']"
>
{{
user
?.
status
}}
</span>
</div>
</div>
</div>
</div>
<div
class=
"px-6 py-4"
>
<div
class=
"space-y-3"
>
<div
class=
"flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400"
>
<svg
class=
"h-4 w-4 text-gray-400 dark:text-gray-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
/>
</svg>
<span
class=
"truncate"
>
{{
user
?.
email
}}
</span>
</div>
<div
v-if=
"user?.username"
class=
"flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400"
>
<svg
class=
"h-4 w-4 text-gray-400 dark:text-gray-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
<span
class=
"truncate"
>
{{
user
.
username
}}
</span>
</div>
</div>
</div>
</div>
<!-- Contact Support Section -->
<div
v-if=
"contactInfo"
class=
"card border-primary-200 bg-gradient-to-r from-primary-50 to-primary-100/50 dark:border-primary-800/40 dark:from-primary-900/20 dark:to-primary-800/10"
>
<div
class=
"px-6 py-5"
>
<ProfileInfoCard
:user=
"user"
/>
<div
v-if=
"contactInfo"
class=
"card border-primary-200 bg-primary-50 dark:bg-primary-900/20 p-6"
>
<div
class=
"flex items-center gap-4"
>
<div
class=
"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30"
>
<svg
class=
"h-6 w-6 text-primary-600 dark:text-primary-400"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
</div>
<div
class=
"min-w-0 flex-1"
>
<h3
class=
"text-sm font-semibold text-primary-800 dark:text-primary-200"
>
{{
t
(
'
common.contactSupport
'
)
}}
</h3>
<p
class=
"mt-1 text-sm font-medium text-primary-600 dark:text-primary-300"
>
{{
contactInfo
}}
</p>
</div>
</div>
</div>
</div>
<!-- Edit Profile Section -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2
class=
"text-lg font-medium text-gray-900 dark:text-white"
>
{{
t
(
'
profile.editProfile
'
)
}}
</h2>
</div>
<div
class=
"px-6 py-6"
>
<form
@
submit.prevent=
"handleUpdateProfile"
class=
"space-y-4"
>
<div>
<label
for=
"username"
class=
"input-label"
>
{{
t
(
'
profile.username
'
)
}}
</label>
<input
id=
"username"
v-model=
"profileForm.username"
type=
"text"
class=
"input"
:placeholder=
"t('profile.enterUsername')"
/>
</div>
<div
class=
"flex justify-end pt-4"
>
<button
type=
"submit"
:disabled=
"updatingProfile"
class=
"btn btn-primary"
>
{{
updatingProfile
?
t
(
'
profile.updating
'
)
:
t
(
'
profile.updateProfile
'
)
}}
</button>
</div>
</form>
</div>
</div>
<!-- Change Password Section -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2
class=
"text-lg font-medium text-gray-900 dark:text-white"
>
{{
t
(
'
profile.changePassword
'
)
}}
</h2>
</div>
<div
class=
"px-6 py-6"
>
<form
@
submit.prevent=
"handleChangePassword"
class=
"space-y-4"
>
<div>
<label
for=
"old_password"
class=
"input-label"
>
{{
t
(
'
profile.currentPassword
'
)
}}
</label>
<input
id=
"old_password"
v-model=
"passwordForm.old_password"
type=
"password"
required
autocomplete=
"current-password"
class=
"input"
/>
</div>
<div>
<label
for=
"new_password"
class=
"input-label"
>
{{
t
(
'
profile.newPassword
'
)
}}
</label>
<input
id=
"new_password"
v-model=
"passwordForm.new_password"
type=
"password"
required
autocomplete=
"new-password"
class=
"input"
/>
<p
class=
"input-hint"
>
{{
t
(
'
profile.passwordHint
'
)
}}
</p>
</div>
<div>
<label
for=
"confirm_password"
class=
"input-label"
>
{{
t
(
'
profile.confirmNewPassword
'
)
}}
</label>
<input
id=
"confirm_password"
v-model=
"passwordForm.confirm_password"
type=
"password"
required
autocomplete=
"new-password"
class=
"input"
/>
<p
v-if=
"passwordForm.new_password && passwordForm.confirm_password && passwordForm.new_password !== passwordForm.confirm_password"
class=
"input-error-text"
>
{{
t
(
'
profile.passwordsNotMatch
'
)
}}
</p>
</div>
<div
class=
"flex justify-end pt-4"
>
<button
type=
"submit"
:disabled=
"changingPassword"
class=
"btn btn-primary"
>
{{
changingPassword
?
t
(
'
profile.changingPassword
'
)
:
t
(
'
profile.changePasswordButton
'
)
}}
</button>
</div>
</form>
<div
class=
"p-3 bg-primary-100 rounded-xl text-primary-600"
>
💬
</div>
<div><h3
class=
"font-semibold text-primary-800 dark:text-primary-200"
>
{{
t
(
'
common.contactSupport
'
)
}}
</h3><p
class=
"text-sm font-medium"
>
{{
contactInfo
}}
</p></div>
</div>
</div>
<ProfileEditForm
:initial-username=
"user?.username || ''"
/>
<ProfilePasswordForm
/>
</div>
</AppLayout>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
h
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
formatDate
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
import
{
userAPI
,
authAPI
}
from
'
@/api
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
{
ref
,
computed
,
h
,
onMounted
}
from
'
vue
'
;
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
;
import
{
formatDate
}
from
'
@/utils/format
'
import
{
authAPI
}
from
'
@/api
'
;
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
StatCard
from
'
@/components/common/StatCard.vue
'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfileEditForm
from
'
@/components/user/profile/ProfileEditForm.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
// SVG Icon Components
const
WalletIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
'
stroke-linecap
'
:
'
round
'
,
'
stroke-linejoin
'
:
'
round
'
,
d
:
'
M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3
'
})
]
)
}
const
BoltIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
'
stroke-linecap
'
:
'
round
'
,
'
stroke-linejoin
'
:
'
round
'
,
d
:
'
m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z
'
})
]
)
}
const
CalendarIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
'
stroke-linecap
'
:
'
round
'
,
'
stroke-linejoin
'
:
'
round
'
,
d
:
'
M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5
'
})
]
)
}
const
authStore
=
useAuthStore
()
const
appStore
=
useAppStore
()
const
user
=
computed
(()
=>
authStore
.
user
)
const
passwordForm
=
ref
({
old_password
:
''
,
new_password
:
''
,
confirm_password
:
''
})
const
profileForm
=
ref
({
username
:
''
})
const
changingPassword
=
ref
(
false
)
const
updatingProfile
=
ref
(
false
)
const
{
t
}
=
useI18n
();
const
authStore
=
useAuthStore
();
const
user
=
computed
(()
=>
authStore
.
user
)
const
contactInfo
=
ref
(
''
)
onMounted
(
async
()
=>
{
try
{
const
settings
=
await
authAPI
.
getPublicSettings
()
contactInfo
.
value
=
settings
.
contact_info
||
''
// Initialize profile form with current user data
if
(
user
.
value
)
{
profileForm
.
value
.
username
=
user
.
value
.
username
||
''
}
}
catch
(
error
)
{
console
.
error
(
'
Failed to load contact info:
'
,
error
)
}
})
const
formatCurrency
=
(
value
:
number
):
string
=>
{
return
`$
${
value
.
toFixed
(
2
)}
`
}
const
handleChangePassword
=
async
()
=>
{
// Validate password match
if
(
passwordForm
.
value
.
new_password
!==
passwordForm
.
value
.
confirm_password
)
{
appStore
.
showError
(
t
(
'
profile.passwordsNotMatch
'
))
return
}
// Validate password length
if
(
passwordForm
.
value
.
new_password
.
length
<
8
)
{
appStore
.
showError
(
t
(
'
profile.passwordTooShort
'
))
return
}
changingPassword
.
value
=
true
try
{
await
userAPI
.
changePassword
(
passwordForm
.
value
.
old_password
,
passwordForm
.
value
.
new_password
)
// Clear form
passwordForm
.
value
=
{
old_password
:
''
,
new_password
:
''
,
confirm_password
:
''
}
appStore
.
showSuccess
(
t
(
'
profile.passwordChangeSuccess
'
))
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
profile.passwordChangeFailed
'
))
}
finally
{
changingPassword
.
value
=
false
}
}
const
handleUpdateProfile
=
async
()
=>
{
// Basic validation
if
(
!
profileForm
.
value
.
username
.
trim
())
{
appStore
.
showError
(
t
(
'
profile.usernameRequired
'
))
return
}
updatingProfile
.
value
=
true
try
{
const
updatedUser
=
await
userAPI
.
updateProfile
({
username
:
profileForm
.
value
.
username
})
// Update auth store with new user data
authStore
.
user
=
updatedUser
const
WalletIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
d
:
'
M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12
'
})])
}
const
BoltIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
d
:
'
m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z
'
})])
}
const
CalendarIcon
=
{
render
:
()
=>
h
(
'
svg
'
,
{
fill
:
'
none
'
,
viewBox
:
'
0 0 24 24
'
,
stroke
:
'
currentColor
'
,
'
stroke-width
'
:
'
1.5
'
},
[
h
(
'
path
'
,
{
d
:
'
M6.75 3v2.25M17.25 3v2.25
'
})])
}
appStore
.
showSuccess
(
t
(
'
profile.updateSuccess
'
))
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
profile.updateFailed
'
))
}
finally
{
updatingProfile
.
value
=
false
}
}
onMounted
(
async
()
=>
{
try
{
const
s
=
await
authAPI
.
getPublicSettings
();
contactInfo
.
value
=
s
.
contact_info
||
''
}
catch
{}
})
const
formatCurrency
=
(
v
:
number
)
=>
`$
${
v
.
toFixed
(
2
)}
`
</
script
>
\ No newline at end of file
frontend/vite.config.js
deleted
100644 → 0
View file @
048ed061
import
{
defineConfig
}
from
'
vite
'
;
import
vue
from
'
@vitejs/plugin-vue
'
;
import
checker
from
'
vite-plugin-checker
'
;
import
{
resolve
}
from
'
path
'
;
export
default
defineConfig
({
plugins
:
[
vue
(),
checker
({
typescript
:
true
,
vueTsc
:
true
})
],
resolve
:
{
alias
:
{
'
@
'
:
resolve
(
__dirname
,
'
src
'
)
}
},
build
:
{
outDir
:
'
../backend/internal/web/dist
'
,
emptyOutDir
:
true
},
server
:
{
host
:
'
0.0.0.0
'
,
port
:
3000
,
proxy
:
{
'
/api
'
:
{
target
:
'
http://localhost:8080
'
,
changeOrigin
:
true
},
'
/setup
'
:
{
target
:
'
http://localhost:8080
'
,
changeOrigin
:
true
}
}
}
});
frontend/vite.config.ts
View file @
fb313356
...
...
@@ -13,9 +13,16 @@ export default defineConfig({
],
resolve
:
{
alias
:
{
'
@
'
:
resolve
(
__dirname
,
'
src
'
)
'
@
'
:
resolve
(
__dirname
,
'
src
'
),
// 使用 vue-i18n 运行时版本,避免 CSP unsafe-eval 问题
'
vue-i18n
'
:
'
vue-i18n/dist/vue-i18n.runtime.esm-bundler.js
'
}
},
define
:
{
// 启用 vue-i18n JIT 编译,在 CSP 环境下处理消息插值
// JIT 编译器生成 AST 对象而非 JS 代码,无需 unsafe-eval
__INTLIFY_JIT_COMPILATION__
:
true
},
build
:
{
outDir
:
'
../backend/internal/web/dist
'
,
emptyOutDir
:
true
...
...
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