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
3c341947
Commit
3c341947
authored
Dec 29, 2025
by
yangjianbo
Browse files
Merge branch 'main' into test-dev
parents
3a7d3387
c01db6b1
Changes
78
Expand all
Hide whitespace changes
Inline
Side-by-side
frontend/src/style.css
View file @
3c341947
...
@@ -307,6 +307,35 @@
...
@@ -307,6 +307,35 @@
@apply
flex
items-center
justify-end
gap-3;
@apply
flex
items-center
justify-end
gap-3;
}
}
/* ============ Dialog ============ */
.dialog-overlay
{
@apply
fixed
inset-0
z-50;
@apply
bg-black/40
dark
:
bg-black
/
60
;
@apply
flex
items-center
justify-center
p-4;
}
.dialog-container
{
@apply
flex
w-full
flex-col;
@apply
max-h-[90vh];
@apply
rounded-2xl
bg-white
dark
:
bg-dark-800
;
@apply
shadow-xl;
}
.dialog-header
{
@apply
border-b
border-gray-100
px-6
py-4
dark
:
border-dark-700
;
@apply
flex
items-center
justify-between;
}
.dialog-body
{
@apply
overflow-y-auto
px-6
py-4;
}
.dialog-footer
{
@apply
border-t
border-gray-100
px-6
py-4
dark
:
border-dark-700
;
@apply
bg-gray-50/60
dark
:
bg-dark-900
/
40
;
@apply
flex
items-center
justify-end
gap-3;
}
/* ============ Toast 通知 ============ */
/* ============ Toast 通知 ============ */
.toast
{
.toast
{
@apply
fixed
right-4
top-4
z-[100];
@apply
fixed
right-4
top-4
z-[100];
...
...
frontend/src/types/index.ts
View file @
3c341947
...
@@ -60,7 +60,11 @@ export interface PublicSettings {
...
@@ -60,7 +60,11 @@ export interface PublicSettings {
export
interface
AuthResponse
{
export
interface
AuthResponse
{
access_token
:
string
access_token
:
string
token_type
:
string
token_type
:
string
user
:
User
user
:
User
&
{
run_mode
?:
'
standard
'
|
'
simple
'
}
}
export
interface
CurrentUserResponse
extends
User
{
run_mode
?:
'
standard
'
|
'
simple
'
}
}
// ==================== Subscription Types ====================
// ==================== Subscription Types ====================
...
...
frontend/src/views/admin/AccountsView.vue
View file @
3c341947
This diff is collapsed.
Click to expand it.
frontend/src/views/admin/DashboardView.vue
View file @
3c341947
...
@@ -407,10 +407,20 @@ const trendData = ref<TrendDataPoint[]>([])
...
@@ -407,10 +407,20 @@ const trendData = ref<TrendDataPoint[]>([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
return
`
${
date
.
getFullYear
()}
-
${
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
)}
-
${
String
(
date
.
getDate
()).
padStart
(
2
,
'
0
'
)}
`
}
// Initialize date range immediately
const
now
=
new
Date
()
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
// Date range
// Date range
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
startDate
=
ref
(
''
)
const
startDate
=
ref
(
formatLocalDate
(
weekAgo
)
)
const
endDate
=
ref
(
''
)
const
endDate
=
ref
(
formatLocalDate
(
now
)
)
// Granularity options for Select component
// Granularity options for Select component
const
granularityOptions
=
computed
(()
=>
[
const
granularityOptions
=
computed
(()
=>
[
...
@@ -597,18 +607,6 @@ const onDateRangeChange = (range: {
...
@@ -597,18 +607,6 @@ const onDateRangeChange = (range: {
loadChartData
()
loadChartData
()
}
}
// Initialize default date range
const
initializeDateRange
=
()
=>
{
const
now
=
new
Date
()
const
today
=
now
.
toISOString
().
split
(
'
T
'
)[
0
]
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
startDate
.
value
=
weekAgo
.
toISOString
().
split
(
'
T
'
)[
0
]
endDate
.
value
=
today
granularity
.
value
=
'
day
'
}
// Load data
// Load data
const
loadDashboardStats
=
async
()
=>
{
const
loadDashboardStats
=
async
()
=>
{
loading
.
value
=
true
loading
.
value
=
true
...
@@ -649,7 +647,6 @@ const loadChartData = async () => {
...
@@ -649,7 +647,6 @@ const loadChartData = async () => {
onMounted
(()
=>
{
onMounted
(()
=>
{
loadDashboardStats
()
loadDashboardStats
()
initializeDateRange
()
loadChartData
()
loadChartData
()
})
})
</
script
>
</
script
>
...
...
frontend/src/views/admin/GroupsView.vue
View file @
3c341947
...
@@ -223,18 +223,19 @@
...
@@ -223,18 +223,19 @@
:
total
=
"
pagination.total
"
:
total
=
"
pagination.total
"
:
page
-
size
=
"
pagination.page_size
"
:
page
-
size
=
"
pagination.page_size
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
/>
<
/template
>
<
/template
>
<
/TablePageLayout
>
<
/TablePageLayout
>
<!--
Create
Group
Modal
-->
<!--
Create
Group
Modal
-->
<
Modal
<
BaseDialog
:
show
=
"
showCreateModal
"
:
show
=
"
showCreateModal
"
:
title
=
"
t('admin.groups.createGroup')
"
:
title
=
"
t('admin.groups.createGroup')
"
size
=
"
lg
"
width
=
"
normal
"
@
close
=
"
closeCreateModal
"
@
close
=
"
closeCreateModal
"
>
>
<
form
@
submit
.
prevent
=
"
handleCreateGroup
"
class
=
"
space-y-5
"
>
<
form
id
=
"
create-group-form
"
@
submit
.
prevent
=
"
handleCreateGroup
"
class
=
"
space-y-5
"
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.name
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.name
'
)
}}
<
/label
>
<
input
<
input
...
@@ -271,34 +272,66 @@
...
@@ -271,34 +272,66 @@
/>
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.rateMultiplierHint
'
)
}}
<
/p
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.rateMultiplierHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
createForm.subscription_type !== 'subscription'
"
class
=
"
flex items-center gap-3
"
>
<
div
v
-
if
=
"
createForm.subscription_type !== 'subscription'
"
>
<
button
<
div
class
=
"
mb-1.5 flex items-center gap-1
"
>
type
=
"
button
"
<
label
class
=
"
text-sm font-medium text-gray-700 dark:text-gray-300
"
>
@
click
=
"
createForm.is_exclusive = !createForm.is_exclusive
"
{{
t
(
'
admin.groups.form.exclusive
'
)
}}
:
class
=
"
[
<
/label
>
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
<!--
Help
Tooltip
-->
createForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
<
div
class
=
"
group relative inline-flex
"
>
]
"
<
svg
>
class
=
"
h-3.5 w-3.5 cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400
"
<
span
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
2
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<!--
Tooltip
Popover
-->
<
div
class
=
"
pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100
"
>
<
div
class
=
"
rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800
"
>
<
p
class
=
"
mb-2 text-xs font-medium
"
>
{{
t
(
'
admin.groups.exclusiveTooltip.title
'
)
}}
<
/p
>
<
p
class
=
"
mb-2 text-xs leading-relaxed text-gray-300
"
>
{{
t
(
'
admin.groups.exclusiveTooltip.description
'
)
}}
<
/p
>
<
div
class
=
"
rounded bg-gray-800 p-2 dark:bg-gray-700
"
>
<
p
class
=
"
text-xs leading-relaxed text-gray-300
"
>
<
span
class
=
"
text-primary-400
"
>
💡
{{
t
(
'
admin.groups.exclusiveTooltip.example
'
)
}}
<
/span
>
{{
t
(
'
admin.groups.exclusiveTooltip.exampleContent
'
)
}}
<
/p
>
<
/div
>
<!--
Arrow
-->
<
div
class
=
"
absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800
"
><
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center gap-3
"
>
<
button
type
=
"
button
"
@
click
=
"
createForm.is_exclusive = !createForm.is_exclusive
"
:
class
=
"
[
:
class
=
"
[
'inline-
block
h-
4
w-
4 transform rounded-full bg-white shadow
transition-
transform
',
'
relative
inline-
flex
h-
6
w-
11 items-center rounded-full
transition-
colors
',
createForm.is_exclusive ? '
translate-x-6' : 'translate-x-1
'
createForm.is_exclusive ? '
bg-primary-500' : 'bg-gray-300 dark:bg-dark-600
'
]
"
]
"
/>
>
<
/button
>
<
span
<
label
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
:
class
=
"
[
{{
t
(
'
admin.groups.exclusiveHint
'
)
}}
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
<
/label
>
createForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
]
"
/>
<
/button
>
<
span
class
=
"
text-sm text-gray-500 dark:text-gray-400
"
>
{{
createForm
.
is_exclusive
?
t
(
'
admin.groups.exclusive
'
)
:
t
(
'
admin.groups.public
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/div
>
<!--
Subscription
Configuration
-->
<!--
Subscription
Configuration
-->
<
div
class
=
"
mt-4 border-t pt-4
"
>
<
div
class
=
"
mt-4 border-t pt-4
"
>
<
h4
class
=
"
mb-4 text-sm font-medium text-gray-900 dark:text-white
"
>
<
div
>
{{
t
(
'
admin.groups.subscription.title
'
)
}}
<
/h4
>
<
div
class
=
"
mb-4
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.subscription.type
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.subscription.type
'
)
}}
<
/label
>
<
Select
v
-
model
=
"
createForm.subscription_type
"
:
options
=
"
subscriptionTypeOptions
"
/>
<
Select
v
-
model
=
"
createForm.subscription_type
"
:
options
=
"
subscriptionTypeOptions
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.subscription.typeHint
'
)
}}
<
/p
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.subscription.typeHint
'
)
}}
<
/p
>
...
@@ -345,11 +378,19 @@
...
@@ -345,11 +378,19 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/form
>
<
template
#
footer
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
button
@
click
=
"
closeCreateModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
<
button
@
click
=
"
closeCreateModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
create-group-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
<
svg
v
-
if
=
"
submitting
"
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
@@ -373,17 +414,22 @@
...
@@ -373,17 +414,22 @@
{{
submitting
?
t
(
'
admin.groups.creating
'
)
:
t
(
'
common.create
'
)
}}
{{
submitting
?
t
(
'
admin.groups.creating
'
)
:
t
(
'
common.create
'
)
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/
form
>
<
/
template
>
<
/
Modal
>
<
/
BaseDialog
>
<!--
Edit
Group
Modal
-->
<!--
Edit
Group
Modal
-->
<
Modal
<
BaseDialog
:
show
=
"
showEditModal
"
:
show
=
"
showEditModal
"
:
title
=
"
t('admin.groups.editGroup')
"
:
title
=
"
t('admin.groups.editGroup')
"
size
=
"
lg
"
width
=
"
normal
"
@
close
=
"
closeEditModal
"
@
close
=
"
closeEditModal
"
>
>
<
form
v
-
if
=
"
editingGroup
"
@
submit
.
prevent
=
"
handleUpdateGroup
"
class
=
"
space-y-5
"
>
<
form
v
-
if
=
"
editingGroup
"
id
=
"
edit-group-form
"
@
submit
.
prevent
=
"
handleUpdateGroup
"
class
=
"
space-y-5
"
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.name
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.name
'
)
}}
<
/label
>
<
input
v
-
model
=
"
editForm.name
"
type
=
"
text
"
required
class
=
"
input
"
/>
<
input
v
-
model
=
"
editForm.name
"
type
=
"
text
"
required
class
=
"
input
"
/>
...
@@ -408,25 +454,61 @@
...
@@ -408,25 +454,61 @@
class
=
"
input
"
class
=
"
input
"
/>
/>
<
/div
>
<
/div
>
<
div
v
-
if
=
"
editForm.subscription_type !== 'subscription'
"
class
=
"
flex items-center gap-3
"
>
<
div
v
-
if
=
"
editForm.subscription_type !== 'subscription'
"
>
<
button
<
div
class
=
"
mb-1.5 flex items-center gap-1
"
>
type
=
"
button
"
<
label
class
=
"
text-sm font-medium text-gray-700 dark:text-gray-300
"
>
@
click
=
"
editForm.is_exclusive = !editForm.is_exclusive
"
{{
t
(
'
admin.groups.form.exclusive
'
)
}}
:
class
=
"
[
<
/label
>
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
<!--
Help
Tooltip
-->
editForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
<
div
class
=
"
group relative inline-flex
"
>
]
"
<
svg
>
class
=
"
h-3.5 w-3.5 cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400
"
<
span
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
2
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<!--
Tooltip
Popover
-->
<
div
class
=
"
pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100
"
>
<
div
class
=
"
rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800
"
>
<
p
class
=
"
mb-2 text-xs font-medium
"
>
{{
t
(
'
admin.groups.exclusiveTooltip.title
'
)
}}
<
/p
>
<
p
class
=
"
mb-2 text-xs leading-relaxed text-gray-300
"
>
{{
t
(
'
admin.groups.exclusiveTooltip.description
'
)
}}
<
/p
>
<
div
class
=
"
rounded bg-gray-800 p-2 dark:bg-gray-700
"
>
<
p
class
=
"
text-xs leading-relaxed text-gray-300
"
>
<
span
class
=
"
text-primary-400
"
>
💡
{{
t
(
'
admin.groups.exclusiveTooltip.example
'
)
}}
<
/span
>
{{
t
(
'
admin.groups.exclusiveTooltip.exampleContent
'
)
}}
<
/p
>
<
/div
>
<!--
Arrow
-->
<
div
class
=
"
absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800
"
><
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center gap-3
"
>
<
button
type
=
"
button
"
@
click
=
"
editForm.is_exclusive = !editForm.is_exclusive
"
:
class
=
"
[
:
class
=
"
[
'inline-
block
h-
4
w-
4 transform rounded-full bg-white shadow
transition-
transform
',
'
relative
inline-
flex
h-
6
w-
11 items-center rounded-full
transition-
colors
',
editForm.is_exclusive ? '
translate-x-6' : 'translate-x-1
'
editForm.is_exclusive ? '
bg-primary-500' : 'bg-gray-300 dark:bg-dark-600
'
]
"
]
"
/>
>
<
/button
>
<
span
<
label
class
=
"
text-sm text-gray-700 dark:text-gray-300
"
>
:
class
=
"
[
{{
t
(
'
admin.groups.exclusiveHint
'
)
}}
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
<
/label
>
editForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
]
"
/>
<
/button
>
<
span
class
=
"
text-sm text-gray-500 dark:text-gray-400
"
>
{{
editForm
.
is_exclusive
?
t
(
'
admin.groups.exclusive
'
)
:
t
(
'
admin.groups.public
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.status
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.status
'
)
}}
<
/label
>
...
@@ -435,11 +517,7 @@
...
@@ -435,11 +517,7 @@
<!--
Subscription
Configuration
-->
<!--
Subscription
Configuration
-->
<
div
class
=
"
mt-4 border-t pt-4
"
>
<
div
class
=
"
mt-4 border-t pt-4
"
>
<
h4
class
=
"
mb-4 text-sm font-medium text-gray-900 dark:text-white
"
>
<
div
>
{{
t
(
'
admin.groups.subscription.title
'
)
}}
<
/h4
>
<
div
class
=
"
mb-4
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.subscription.type
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.subscription.type
'
)
}}
<
/label
>
<
Select
<
Select
v
-
model
=
"
editForm.subscription_type
"
v
-
model
=
"
editForm.subscription_type
"
...
@@ -490,11 +568,19 @@
...
@@ -490,11 +568,19 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/form
>
<
template
#
footer
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
button
@
click
=
"
closeEditModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
<
button
@
click
=
"
closeEditModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
edit-group-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
<
svg
v
-
if
=
"
submitting
"
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
@@ -518,8 +604,8 @@
...
@@ -518,8 +604,8 @@
{{
submitting
?
t
(
'
admin.groups.updating
'
)
:
t
(
'
common.update
'
)
}}
{{
submitting
?
t
(
'
admin.groups.updating
'
)
:
t
(
'
common.update
'
)
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/
form
>
<
/
template
>
<
/
Modal
>
<
/
BaseDialog
>
<!--
Delete
Confirmation
Dialog
-->
<!--
Delete
Confirmation
Dialog
-->
<
ConfirmDialog
<
ConfirmDialog
...
@@ -546,7 +632,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
...
@@ -546,7 +632,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Modal
from
'
@/components/common/
Modal
.vue
'
import
BaseDialog
from
'
@/components/common/
BaseDialog
.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -616,6 +702,8 @@ const pagination = reactive({
...
@@ -616,6 +702,8 @@ const pagination = reactive({
pages
:
0
pages
:
0
}
)
}
)
let
abortController
:
AbortController
|
null
=
null
const
showCreateModal
=
ref
(
false
)
const
showCreateModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
...
@@ -660,21 +748,33 @@ const deleteConfirmMessage = computed(() => {
...
@@ -660,21 +748,33 @@ const deleteConfirmMessage = computed(() => {
}
)
}
)
const
loadGroups
=
async
()
=>
{
const
loadGroups
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
}
const
currentController
=
new
AbortController
()
abortController
=
currentController
const
{
signal
}
=
currentController
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
response
=
await
adminAPI
.
groups
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
const
response
=
await
adminAPI
.
groups
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
status
:
filters
.
status
as
any
,
status
:
filters
.
status
as
any
,
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
}
)
}
,
{
signal
}
)
if
(
signal
.
aborted
)
return
groups
.
value
=
response
.
items
groups
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
pagination
.
pages
=
response
.
pages
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
if
(
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
return
}
appStore
.
showError
(
t
(
'
admin.groups.failedToLoad
'
))
appStore
.
showError
(
t
(
'
admin.groups.failedToLoad
'
))
console
.
error
(
'
Error loading groups:
'
,
error
)
console
.
error
(
'
Error loading groups:
'
,
error
)
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
abortController
===
currentController
&&
!
signal
.
aborted
)
{
loading
.
value
=
false
}
}
}
}
}
...
@@ -683,6 +783,12 @@ const handlePageChange = (page: number) => {
...
@@ -683,6 +783,12 @@ const handlePageChange = (page: number) => {
loadGroups
()
loadGroups
()
}
}
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
page_size
=
pageSize
pagination
.
page
=
1
loadGroups
()
}
const
closeCreateModal
=
()
=>
{
const
closeCreateModal
=
()
=>
{
showCreateModal
.
value
=
false
showCreateModal
.
value
=
false
createForm
.
name
=
''
createForm
.
name
=
''
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
3c341947
...
@@ -209,15 +209,16 @@
...
@@ -209,15 +209,16 @@
:total=
"pagination.total"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
@
update:page=
"handlePageChange"
@
update:pageSize=
"handlePageSizeChange"
/>
/>
</
template
>
</
template
>
</TablePageLayout>
</TablePageLayout>
<!-- Create Proxy Modal -->
<!-- Create Proxy Modal -->
<
Modal
<
BaseDialog
:show=
"showCreateModal"
:show=
"showCreateModal"
:title=
"t('admin.proxies.createProxy')"
:title=
"t('admin.proxies.createProxy')"
size=
"lg
"
width=
"normal
"
@
close=
"closeCreateModal"
@
close=
"closeCreateModal"
>
>
<!-- Tab Switch -->
<!-- Tab Switch -->
...
@@ -271,7 +272,12 @@
...
@@ -271,7 +272,12 @@
</div>
</div>
<!-- Standard Add Form -->
<!-- Standard Add Form -->
<form
v-if=
"createMode === 'standard'"
@
submit.prevent=
"handleCreateProxy"
class=
"space-y-5"
>
<form
v-if=
"createMode === 'standard'"
id=
"create-proxy-form"
@
submit.prevent=
"handleCreateProxy"
class=
"space-y-5"
>
<div>
<div>
<label
class=
"input-label"
>
{{ t('admin.proxies.name') }}
</label>
<label
class=
"input-label"
>
{{ t('admin.proxies.name') }}
</label>
<input
<input
...
@@ -329,34 +335,6 @@
...
@@ -329,34 +335,6 @@
/>
/>
</div>
</div>
<div
class=
"flex justify-end gap-3 pt-4"
>
<button
@
click=
"closeCreateModal"
type=
"button"
class=
"btn btn-secondary"
>
{{ t('common.cancel') }}
</button>
<button
type=
"submit"
:disabled=
"submitting"
class=
"btn btn-primary"
>
<svg
v-if=
"submitting"
class=
"-ml-1 mr-2 h-4 w-4 animate-spin"
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>
{{ submitting ? t('admin.proxies.creating') : t('common.create') }}
</button>
</div>
</form>
</form>
<!-- Batch Add Form -->
<!-- Batch Add Form -->
...
@@ -435,11 +413,44 @@
...
@@ -435,11 +413,44 @@
</div>
</div>
</div>
</div>
<div
class=
"flex justify-end gap-3 pt-4"
>
</div>
<
template
#footer
>
<div
class=
"flex justify-end gap-3"
>
<button
@
click=
"closeCreateModal"
type=
"button"
class=
"btn btn-secondary"
>
<button
@
click=
"closeCreateModal"
type=
"button"
class=
"btn btn-secondary"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
</button>
</button>
<button
<button
v-if=
"createMode === 'standard'"
type=
"submit"
form=
"create-proxy-form"
:disabled=
"submitting"
class=
"btn btn-primary"
>
<svg
v-if=
"submitting"
class=
"-ml-1 mr-2 h-4 w-4 animate-spin"
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>
{{
submitting
?
t
(
'
admin.proxies.creating
'
)
:
t
(
'
common.create
'
)
}}
</button>
<button
v-else
@
click=
"handleBatchCreate"
@
click=
"handleBatchCreate"
type=
"button"
type=
"button"
:disabled=
"submitting || batchParseResult.valid === 0"
:disabled=
"submitting || batchParseResult.valid === 0"
...
@@ -472,17 +483,22 @@
...
@@ -472,17 +483,22 @@
}}
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
</
div
>
<
/
template
>
</
Modal
>
<
/
BaseDialog
>
<!--
Edit
Proxy
Modal
-->
<!--
Edit
Proxy
Modal
-->
<
Modal
<
BaseDialog
:
show
=
"
showEditModal
"
:
show
=
"
showEditModal
"
:
title
=
"
t('admin.proxies.editProxy')
"
:
title
=
"
t('admin.proxies.editProxy')
"
size=
"lg
"
width
=
"
normal
"
@
close
=
"
closeEditModal
"
@
close
=
"
closeEditModal
"
>
>
<form
v-if=
"editingProxy"
@
submit.prevent=
"handleUpdateProxy"
class=
"space-y-5"
>
<
form
v
-
if
=
"
editingProxy
"
id
=
"
edit-proxy-form
"
@
submit
.
prevent
=
"
handleUpdateProxy
"
class
=
"
space-y-5
"
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.proxies.name
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.proxies.name
'
)
}}
<
/label
>
<
input
v
-
model
=
"
editForm.name
"
type
=
"
text
"
required
class
=
"
input
"
/>
<
input
v
-
model
=
"
editForm.name
"
type
=
"
text
"
required
class
=
"
input
"
/>
...
@@ -526,11 +542,20 @@
...
@@ -526,11 +542,20 @@
<
Select
v
-
model
=
"
editForm.status
"
:
options
=
"
editStatusOptions
"
/>
<
Select
v
-
model
=
"
editForm.status
"
:
options
=
"
editStatusOptions
"
/>
<
/div
>
<
/div
>
<div
class=
"flex justify-end gap-3 pt-4"
>
<
/form
>
<
template
#
footer
>
<
div
class
=
"
flex justify-end gap-3
"
>
<
button
@
click
=
"
closeEditModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
<
button
@
click
=
"
closeEditModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
/button
>
<button
type=
"submit"
:disabled=
"submitting"
class=
"btn btn-primary"
>
<
button
v
-
if
=
"
editingProxy
"
type
=
"
submit
"
form
=
"
edit-proxy-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
<
svg
v
-
if
=
"
submitting
"
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
@@ -554,8 +579,8 @@
...
@@ -554,8 +579,8 @@
{{
submitting
?
t
(
'
admin.proxies.updating
'
)
:
t
(
'
common.update
'
)
}}
{{
submitting
?
t
(
'
admin.proxies.updating
'
)
:
t
(
'
common.update
'
)
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
</
form
>
<
/
template
>
</
Modal
>
<
/
BaseDialog
>
<!--
Delete
Confirmation
Dialog
-->
<!--
Delete
Confirmation
Dialog
-->
<
ConfirmDialog
<
ConfirmDialog
...
@@ -582,7 +607,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
...
@@ -582,7 +607,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Modal
from
'
@/components/common/
Modal
.vue
'
import
BaseDialog
from
'
@/components/common/
BaseDialog
.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -682,22 +707,44 @@ const editForm = reactive({
...
@@ -682,22 +707,44 @@ const editForm = reactive({
status
:
'
active
'
as
'
active
'
|
'
inactive
'
status
:
'
active
'
as
'
active
'
|
'
inactive
'
}
)
}
)
let
abortController
:
AbortController
|
null
=
null
const
isAbortError
=
(
error
:
unknown
)
=>
{
if
(
!
error
||
typeof
error
!==
'
object
'
)
return
false
const
maybeError
=
error
as
{
name
?:
string
;
code
?:
string
}
return
maybeError
.
name
===
'
AbortError
'
||
maybeError
.
code
===
'
ERR_CANCELED
'
}
const
loadProxies
=
async
()
=>
{
const
loadProxies
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
}
const
currentAbortController
=
new
AbortController
()
abortController
=
currentAbortController
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
response
=
await
adminAPI
.
proxies
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
const
response
=
await
adminAPI
.
proxies
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
protocol
:
filters
.
protocol
||
undefined
,
protocol
:
filters
.
protocol
||
undefined
,
status
:
filters
.
status
as
any
,
status
:
filters
.
status
as
any
,
search
:
searchQuery
.
value
||
undefined
search
:
searchQuery
.
value
||
undefined
})
}
,
{
signal
:
currentAbortController
.
signal
}
)
if
(
currentAbortController
.
signal
.
aborted
||
abortController
!==
currentAbortController
)
{
return
}
proxies
.
value
=
response
.
items
proxies
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
pagination
.
pages
=
response
.
pages
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
isAbortError
(
error
))
{
return
}
appStore
.
showError
(
t
(
'
admin.proxies.failedToLoad
'
))
appStore
.
showError
(
t
(
'
admin.proxies.failedToLoad
'
))
console
.
error
(
'
Error loading proxies:
'
,
error
)
console
.
error
(
'
Error loading proxies:
'
,
error
)
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
abortController
===
currentAbortController
)
{
loading
.
value
=
false
abortController
=
null
}
}
}
}
}
...
@@ -715,6 +762,12 @@ const handlePageChange = (page: number) => {
...
@@ -715,6 +762,12 @@ const handlePageChange = (page: number) => {
loadProxies
()
loadProxies
()
}
}
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
page_size
=
pageSize
pagination
.
page
=
1
loadProxies
()
}
const
closeCreateModal
=
()
=>
{
const
closeCreateModal
=
()
=>
{
showCreateModal
.
value
=
false
showCreateModal
.
value
=
false
createMode
.
value
=
'
standard
'
createMode
.
value
=
'
standard
'
...
...
frontend/src/views/admin/RedeemView.vue
View file @
3c341947
...
@@ -186,6 +186,7 @@
...
@@ -186,6 +186,7 @@
:
total
=
"
pagination.total
"
:
total
=
"
pagination.total
"
:
page
-
size
=
"
pagination.page_size
"
:
page
-
size
=
"
pagination.page_size
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
/>
<!--
Batch
Actions
-->
<!--
Batch
Actions
-->
...
@@ -542,6 +543,8 @@ const pagination = reactive({
...
@@ -542,6 +543,8 @@ const pagination = reactive({
pages
:
0
pages
:
0
}
)
}
)
let
abortController
:
AbortController
|
null
=
null
const
showDeleteDialog
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
const
showDeleteUnusedDialog
=
ref
(
false
)
const
showDeleteUnusedDialog
=
ref
(
false
)
const
deletingCode
=
ref
<
RedeemCode
|
null
>
(
null
)
const
deletingCode
=
ref
<
RedeemCode
|
null
>
(
null
)
...
@@ -556,21 +559,46 @@ const generateForm = reactive({
...
@@ -556,21 +559,46 @@ const generateForm = reactive({
}
)
}
)
const
loadCodes
=
async
()
=>
{
const
loadCodes
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
}
const
currentController
=
new
AbortController
()
abortController
=
currentController
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
response
=
await
adminAPI
.
redeem
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
const
response
=
await
adminAPI
.
redeem
.
list
(
type
:
filters
.
type
as
RedeemCodeType
,
pagination
.
page
,
status
:
filters
.
status
as
any
,
pagination
.
page_size
,
search
:
searchQuery
.
value
||
undefined
{
}
)
type
:
filters
.
type
as
RedeemCodeType
,
status
:
filters
.
status
as
any
,
search
:
searchQuery
.
value
||
undefined
}
,
{
signal
:
currentController
.
signal
}
)
if
(
currentController
.
signal
.
aborted
)
{
return
}
codes
.
value
=
response
.
items
codes
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
pagination
.
pages
=
response
.
pages
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
if
(
currentController
.
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
return
}
appStore
.
showError
(
t
(
'
admin.redeem.failedToLoad
'
))
appStore
.
showError
(
t
(
'
admin.redeem.failedToLoad
'
))
console
.
error
(
'
Error loading redeem codes:
'
,
error
)
console
.
error
(
'
Error loading redeem codes:
'
,
error
)
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
abortController
===
currentController
&&
!
currentController
.
signal
.
aborted
)
{
loading
.
value
=
false
abortController
=
null
}
}
}
}
}
...
@@ -588,6 +616,12 @@ const handlePageChange = (page: number) => {
...
@@ -588,6 +616,12 @@ const handlePageChange = (page: number) => {
loadCodes
()
loadCodes
()
}
}
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
page_size
=
pageSize
pagination
.
page
=
1
loadCodes
()
}
const
handleGenerateCodes
=
async
()
=>
{
const
handleGenerateCodes
=
async
()
=>
{
// 订阅类型必须选择分组
// 订阅类型必须选择分组
if
(
generateForm
.
type
===
'
subscription
'
&&
!
generateForm
.
group_id
)
{
if
(
generateForm
.
type
===
'
subscription
'
&&
!
generateForm
.
group_id
)
{
...
...
frontend/src/views/admin/SubscriptionsView.vue
View file @
3c341947
...
@@ -316,18 +316,23 @@
...
@@ -316,18 +316,23 @@
:
total
=
"
pagination.total
"
:
total
=
"
pagination.total
"
:
page
-
size
=
"
pagination.page_size
"
:
page
-
size
=
"
pagination.page_size
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
/>
<
/template
>
<
/template
>
<
/TablePageLayout
>
<
/TablePageLayout
>
<!--
Assign
Subscription
Modal
-->
<!--
Assign
Subscription
Modal
-->
<
Modal
<
BaseDialog
:
show
=
"
showAssignModal
"
:
show
=
"
showAssignModal
"
:
title
=
"
t('admin.subscriptions.assignSubscription')
"
:
title
=
"
t('admin.subscriptions.assignSubscription')
"
size
=
"
lg
"
width
=
"
normal
"
@
close
=
"
closeAssignModal
"
@
close
=
"
closeAssignModal
"
>
>
<
form
@
submit
.
prevent
=
"
handleAssignSubscription
"
class
=
"
space-y-5
"
>
<
form
id
=
"
assign-subscription-form
"
@
submit
.
prevent
=
"
handleAssignSubscription
"
class
=
"
space-y-5
"
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.user
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.user
'
)
}}
<
/label
>
<
Select
<
Select
...
@@ -351,12 +356,18 @@
...
@@ -351,12 +356,18 @@
<
input
v
-
model
.
number
=
"
assignForm.validity_days
"
type
=
"
number
"
min
=
"
1
"
class
=
"
input
"
/>
<
input
v
-
model
.
number
=
"
assignForm.validity_days
"
type
=
"
number
"
min
=
"
1
"
class
=
"
input
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.subscriptions.validityHint
'
)
}}
<
/p
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.subscriptions.validityHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/form
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
template
#
footer
>
<
div
class
=
"
flex justify-end gap-3
"
>
<
button
@
click
=
"
closeAssignModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
<
button
@
click
=
"
closeAssignModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
assign-subscription-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
<
svg
v
-
if
=
"
submitting
"
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
@@ -380,18 +391,19 @@
...
@@ -380,18 +391,19 @@
{{
submitting
?
t
(
'
admin.subscriptions.assigning
'
)
:
t
(
'
admin.subscriptions.assign
'
)
}}
{{
submitting
?
t
(
'
admin.subscriptions.assigning
'
)
:
t
(
'
admin.subscriptions.assign
'
)
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/
form
>
<
/
template
>
<
/
Modal
>
<
/
BaseDialog
>
<!--
Extend
Subscription
Modal
-->
<!--
Extend
Subscription
Modal
-->
<
Modal
<
BaseDialog
:
show
=
"
showExtendModal
"
:
show
=
"
showExtendModal
"
:
title
=
"
t('admin.subscriptions.extendSubscription')
"
:
title
=
"
t('admin.subscriptions.extendSubscription')
"
size
=
"
md
"
width
=
"
narrow
"
@
close
=
"
closeExtendModal
"
@
close
=
"
closeExtendModal
"
>
>
<
form
<
form
v
-
if
=
"
extendingSubscription
"
v
-
if
=
"
extendingSubscription
"
id
=
"
extend-subscription-form
"
@
submit
.
prevent
=
"
handleExtendSubscription
"
@
submit
.
prevent
=
"
handleExtendSubscription
"
class
=
"
space-y-5
"
class
=
"
space-y-5
"
>
>
...
@@ -417,17 +429,23 @@
...
@@ -417,17 +429,23 @@
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.extendDays
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.extendDays
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
extendForm.days
"
type
=
"
number
"
min
=
"
1
"
required
class
=
"
input
"
/>
<
input
v
-
model
.
number
=
"
extendForm.days
"
type
=
"
number
"
min
=
"
1
"
required
class
=
"
input
"
/>
<
/div
>
<
/div
>
<
/form
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
template
#
footer
>
<
div
v
-
if
=
"
extendingSubscription
"
class
=
"
flex justify-end gap-3
"
>
<
button
@
click
=
"
closeExtendModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
<
button
@
click
=
"
closeExtendModal
"
type
=
"
button
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.cancel
'
)
}}
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
extend-subscription-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
{{
submitting
?
t
(
'
admin.subscriptions.extending
'
)
:
t
(
'
admin.subscriptions.extend
'
)
}}
{{
submitting
?
t
(
'
admin.subscriptions.extending
'
)
:
t
(
'
admin.subscriptions.extend
'
)
}}
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/
form
>
<
/
template
>
<
/
Modal
>
<
/
BaseDialog
>
<!--
Revoke
Confirmation
Dialog
-->
<!--
Revoke
Confirmation
Dialog
-->
<
ConfirmDialog
<
ConfirmDialog
...
@@ -455,7 +473,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
...
@@ -455,7 +473,7 @@ import AppLayout from '@/components/layout/AppLayout.vue'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
Modal
from
'
@/components/common/
Modal
.vue
'
import
BaseDialog
from
'
@/components/common/
BaseDialog
.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -485,6 +503,7 @@ const subscriptions = ref<UserSubscription[]>([])
...
@@ -485,6 +503,7 @@ const subscriptions = ref<UserSubscription[]>([])
const
groups
=
ref
<
Group
[]
>
([])
const
groups
=
ref
<
Group
[]
>
([])
const
users
=
ref
<
User
[]
>
([])
const
users
=
ref
<
User
[]
>
([])
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
const
filters
=
reactive
({
const
filters
=
reactive
({
status
:
''
,
status
:
''
,
group_id
:
''
group_id
:
''
...
@@ -530,20 +549,36 @@ const subscriptionGroupOptions = computed(() =>
...
@@ -530,20 +549,36 @@ const subscriptionGroupOptions = computed(() =>
const
userOptions
=
computed
(()
=>
users
.
value
.
map
((
u
)
=>
({
value
:
u
.
id
,
label
:
u
.
email
}
)))
const
userOptions
=
computed
(()
=>
users
.
value
.
map
((
u
)
=>
({
value
:
u
.
id
,
label
:
u
.
email
}
)))
const
loadSubscriptions
=
async
()
=>
{
const
loadSubscriptions
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
}
const
requestController
=
new
AbortController
()
abortController
=
requestController
const
{
signal
}
=
requestController
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
response
=
await
adminAPI
.
subscriptions
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
const
response
=
await
adminAPI
.
subscriptions
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
status
:
(
filters
.
status
as
any
)
||
undefined
,
status
:
(
filters
.
status
as
any
)
||
undefined
,
group_id
:
filters
.
group_id
?
parseInt
(
filters
.
group_id
)
:
undefined
group_id
:
filters
.
group_id
?
parseInt
(
filters
.
group_id
)
:
undefined
}
,
{
signal
}
)
}
)
if
(
signal
.
aborted
||
abortController
!==
requestController
)
return
subscriptions
.
value
=
response
.
items
subscriptions
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
pagination
.
pages
=
response
.
pages
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
if
(
signal
.
aborted
||
error
?.
name
===
'
AbortError
'
||
error
?.
code
===
'
ERR_CANCELED
'
)
{
return
}
appStore
.
showError
(
t
(
'
admin.subscriptions.failedToLoad
'
))
appStore
.
showError
(
t
(
'
admin.subscriptions.failedToLoad
'
))
console
.
error
(
'
Error loading subscriptions:
'
,
error
)
console
.
error
(
'
Error loading subscriptions:
'
,
error
)
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
abortController
===
requestController
)
{
loading
.
value
=
false
abortController
=
null
}
}
}
}
}
...
@@ -569,6 +604,12 @@ const handlePageChange = (page: number) => {
...
@@ -569,6 +604,12 @@ const handlePageChange = (page: number) => {
loadSubscriptions
()
loadSubscriptions
()
}
}
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
page_size
=
pageSize
pagination
.
page
=
1
loadSubscriptions
()
}
const
closeAssignModal
=
()
=>
{
const
closeAssignModal
=
()
=>
{
showAssignModal
.
value
=
false
showAssignModal
.
value
=
false
assignForm
.
user_id
=
null
assignForm
.
user_id
=
null
...
...
frontend/src/views/admin/UsageView.vue
View file @
3c341947
...
@@ -224,7 +224,7 @@
...
@@ -224,7 +224,7 @@
v-model=
"filters.api_key_id"
v-model=
"filters.api_key_id"
:options=
"apiKeyOptions"
:options=
"apiKeyOptions"
:placeholder=
"t('usage.allApiKeys')"
:placeholder=
"t('usage.allApiKeys')"
:disabled=
"!selectedUser && apiKeys.length === 0"
searchable
@
change=
"applyFilters"
@
change=
"applyFilters"
/>
/>
</div>
</div>
...
@@ -236,6 +236,7 @@
...
@@ -236,6 +236,7 @@
v-model=
"filters.model"
v-model=
"filters.model"
:options=
"modelOptions"
:options=
"modelOptions"
:placeholder=
"t('admin.usage.allModels')"
:placeholder=
"t('admin.usage.allModels')"
searchable
@
change=
"applyFilters"
@
change=
"applyFilters"
/>
/>
</div>
</div>
...
@@ -534,6 +535,7 @@
...
@@ -534,6 +535,7 @@
:total=
"pagination.total"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
@
update:page=
"handlePageChange"
@
update:pageSize=
"handlePageSizeChange"
/>
/>
</div>
</div>
</AppLayout>
</AppLayout>
...
@@ -666,6 +668,7 @@ const models = ref<string[]>([])
...
@@ -666,6 +668,7 @@ const models = ref<string[]>([])
const
accounts
=
ref
<
any
[]
>
([])
const
accounts
=
ref
<
any
[]
>
([])
const
groups
=
ref
<
any
[]
>
([])
const
groups
=
ref
<
any
[]
>
([])
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
// User search state
// User search state
const
userSearchKeyword
=
ref
(
''
)
const
userSearchKeyword
=
ref
(
''
)
...
@@ -675,7 +678,7 @@ const showUserDropdown = ref(false)
...
@@ -675,7 +678,7 @@ const showUserDropdown = ref(false)
const
selectedUser
=
ref
<
SimpleUser
|
null
>
(
null
)
const
selectedUser
=
ref
<
SimpleUser
|
null
>
(
null
)
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
// API Key options computed from
selected user's
keys
// API Key options computed from
loaded
keys
const
apiKeyOptions
=
computed
(()
=>
{
const
apiKeyOptions
=
computed
(()
=>
{
return
[
return
[
{
value
:
null
,
label
:
t
(
'
usage.allApiKeys
'
)
},
{
value
:
null
,
label
:
t
(
'
usage.allApiKeys
'
)
},
...
@@ -733,9 +736,19 @@ const groupOptions = computed(() => {
...
@@ -733,9 +736,19 @@ const groupOptions = computed(() => {
]
]
})
})
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
return
`
${
date
.
getFullYear
()}
-
${
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
)}
-
${
String
(
date
.
getDate
()).
padStart
(
2
,
'
0
'
)}
`
}
// Initialize date range immediately
const
now
=
new
Date
()
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
// Date range state
// Date range state
const
startDate
=
ref
(
''
)
const
startDate
=
ref
(
formatLocalDate
(
weekAgo
)
)
const
endDate
=
ref
(
''
)
const
endDate
=
ref
(
formatLocalDate
(
now
)
)
const
filters
=
ref
<
AdminUsageQueryParams
>
({
const
filters
=
ref
<
AdminUsageQueryParams
>
({
user_id
:
undefined
,
user_id
:
undefined
,
...
@@ -749,18 +762,9 @@ const filters = ref<AdminUsageQueryParams>({
...
@@ -749,18 +762,9 @@ const filters = ref<AdminUsageQueryParams>({
end_date
:
undefined
end_date
:
undefined
})
})
// Initialize default date range (last 7 days)
// Initialize filters with date range
const
initializeDateRange
=
()
=>
{
filters
.
value
.
start_date
=
startDate
.
value
const
now
=
new
Date
()
filters
.
value
.
end_date
=
endDate
.
value
const
today
=
now
.
toISOString
().
split
(
'
T
'
)[
0
]
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
startDate
.
value
=
weekAgo
.
toISOString
().
split
(
'
T
'
)[
0
]
endDate
.
value
=
today
filters
.
value
.
start_date
=
startDate
.
value
filters
.
value
.
end_date
=
endDate
.
value
}
// User search with debounce
// User search with debounce
const
debounceSearchUsers
=
()
=>
{
const
debounceSearchUsers
=
()
=>
{
...
@@ -796,7 +800,7 @@ const selectUser = async (user: SimpleUser) => {
...
@@ -796,7 +800,7 @@ const selectUser = async (user: SimpleUser) => {
filters
.
value
.
api_key_id
=
undefined
filters
.
value
.
api_key_id
=
undefined
// Load API keys for selected user
// Load API keys for selected user
await
loadApiKeys
ForUser
(
user
.
id
)
await
loadApiKeys
(
user
.
id
)
applyFilters
()
applyFilters
()
}
}
...
@@ -807,10 +811,11 @@ const clearUserFilter = () => {
...
@@ -807,10 +811,11 @@ const clearUserFilter = () => {
filters
.
value
.
user_id
=
undefined
filters
.
value
.
user_id
=
undefined
filters
.
value
.
api_key_id
=
undefined
filters
.
value
.
api_key_id
=
undefined
apiKeys
.
value
=
[]
apiKeys
.
value
=
[]
loadApiKeys
()
applyFilters
()
applyFilters
()
}
}
const
loadApiKeys
ForUser
=
async
(
userId
:
number
)
=>
{
const
loadApiKeys
=
async
(
userId
?
:
number
)
=>
{
try
{
try
{
apiKeys
.
value
=
await
adminAPI
.
usage
.
searchApiKeys
(
userId
)
apiKeys
.
value
=
await
adminAPI
.
usage
.
searchApiKeys
(
userId
)
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -863,7 +868,24 @@ const formatCacheTokens = (value: number): string => {
...
@@ -863,7 +868,24 @@ const formatCacheTokens = (value: number): string => {
return
value
.
toLocaleString
()
return
value
.
toLocaleString
()
}
}
const
isAbortError
=
(
error
:
unknown
):
boolean
=>
{
if
(
error
instanceof
DOMException
&&
error
.
name
===
'
AbortError
'
)
{
return
true
}
if
(
typeof
error
===
'
object
'
&&
error
!==
null
)
{
const
maybeError
=
error
as
{
code
?:
string
;
name
?:
string
}
return
maybeError
.
code
===
'
ERR_CANCELED
'
||
maybeError
.
name
===
'
CanceledError
'
}
return
false
}
const
loadUsageLogs
=
async
()
=>
{
const
loadUsageLogs
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
}
const
controller
=
new
AbortController
()
abortController
=
controller
const
{
signal
}
=
controller
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
params
:
AdminUsageQueryParams
=
{
const
params
:
AdminUsageQueryParams
=
{
...
@@ -872,17 +894,23 @@ const loadUsageLogs = async () => {
...
@@ -872,17 +894,23 @@ const loadUsageLogs = async () => {
...
filters
.
value
...
filters
.
value
}
}
const
response
=
await
adminAPI
.
usage
.
list
(
params
)
const
response
=
await
adminAPI
.
usage
.
list
(
params
,
{
signal
})
if
(
signal
.
aborted
)
{
return
}
usageLogs
.
value
=
response
.
items
usageLogs
.
value
=
response
.
items
pagination
.
value
.
total
=
response
.
total
pagination
.
value
.
total
=
response
.
total
pagination
.
value
.
pages
=
response
.
pages
pagination
.
value
.
pages
=
response
.
pages
// Extract models from loaded logs for filter options
extractModelsFromLogs
()
}
catch
(
error
)
{
}
catch
(
error
)
{
if
(
signal
.
aborted
||
isAbortError
(
error
))
{
return
}
appStore
.
showError
(
t
(
'
usage.failedToLoad
'
))
appStore
.
showError
(
t
(
'
usage.failedToLoad
'
))
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
!
signal
.
aborted
&&
abortController
===
controller
)
{
loading
.
value
=
false
}
}
}
}
}
...
@@ -944,27 +972,40 @@ const applyFilters = () => {
...
@@ -944,27 +972,40 @@ const applyFilters = () => {
// Load filter options
// Load filter options
const
loadFilterOptions
=
async
()
=>
{
const
loadFilterOptions
=
async
()
=>
{
try
{
try
{
// Load accounts
const
[
accountsResponse
,
groupsResponse
]
=
await
Promise
.
all
([
const
accountsResponse
=
await
adminAPI
.
accounts
.
list
(
1
,
1000
)
adminAPI
.
accounts
.
list
(
1
,
1000
),
adminAPI
.
groups
.
list
(
1
,
1000
)
])
accounts
.
value
=
accountsResponse
.
items
||
[]
accounts
.
value
=
accountsResponse
.
items
||
[]
// Load groups
const
groupsResponse
=
await
adminAPI
.
groups
.
list
(
1
,
1000
)
groups
.
value
=
groupsResponse
.
items
||
[]
groups
.
value
=
groupsResponse
.
items
||
[]
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Failed to load filter options:
'
,
error
)
console
.
error
(
'
Failed to load filter options:
'
,
error
)
}
}
await
loadModelOptions
()
}
}
// Extract unique models from usage logs
const
loadModelOptions
=
async
()
=>
{
const
extractModelsFromLogs
=
()
=>
{
try
{
const
uniqueModels
=
new
Set
<
string
>
()
const
endDate
=
new
Date
()
usageLogs
.
value
.
forEach
(
log
=>
{
const
startDateRange
=
new
Date
(
endDate
)
if
(
log
.
model
)
{
startDateRange
.
setDate
(
startDateRange
.
getDate
()
-
29
)
uniqueModels
.
add
(
log
.
model
)
// Use local timezone instead of UTC
}
const
endDateStr
=
`
${
endDate
.
getFullYear
()}
-
${
String
(
endDate
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
)}
-
${
String
(
endDate
.
getDate
()).
padStart
(
2
,
'
0
'
)}
`
})
const
startDateStr
=
`
${
startDateRange
.
getFullYear
()}
-
${
String
(
startDateRange
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
)}
-
${
String
(
startDateRange
.
getDate
()).
padStart
(
2
,
'
0
'
)}
`
models
.
value
=
Array
.
from
(
uniqueModels
).
sort
()
const
response
=
await
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDateStr
,
end_date
:
endDateStr
})
const
uniqueModels
=
new
Set
<
string
>
()
response
.
models
?.
forEach
((
stat
)
=>
{
if
(
stat
.
model
)
{
uniqueModels
.
add
(
stat
.
model
)
}
})
models
.
value
=
Array
.
from
(
uniqueModels
).
sort
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to load model options:
'
,
error
)
}
}
}
const
resetFilters
=
()
=>
{
const
resetFilters
=
()
=>
{
...
@@ -985,8 +1026,15 @@ const resetFilters = () => {
...
@@ -985,8 +1026,15 @@ const resetFilters = () => {
}
}
granularity
.
value
=
'
day
'
granularity
.
value
=
'
day
'
// Reset date range to default (last 7 days)
// Reset date range to default (last 7 days)
initializeDateRange
()
const
now
=
new
Date
()
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
startDate
.
value
=
formatLocalDate
(
weekAgo
)
endDate
.
value
=
formatLocalDate
(
now
)
filters
.
value
.
start_date
=
startDate
.
value
filters
.
value
.
end_date
=
endDate
.
value
pagination
.
value
.
page
=
1
pagination
.
value
.
page
=
1
loadApiKeys
()
loadUsageLogs
()
loadUsageLogs
()
loadUsageStats
()
loadUsageStats
()
loadChartData
()
loadChartData
()
...
@@ -997,6 +1045,12 @@ const handlePageChange = (page: number) => {
...
@@ -997,6 +1045,12 @@ const handlePageChange = (page: number) => {
loadUsageLogs
()
loadUsageLogs
()
}
}
const
handlePageSizeChange
=
(
pageSize
:
number
)
=>
{
pagination
.
value
.
page_size
=
pageSize
pagination
.
value
.
page
=
1
loadUsageLogs
()
}
const
exportToCSV
=
()
=>
{
const
exportToCSV
=
()
=>
{
if
(
usageLogs
.
value
.
length
===
0
)
{
if
(
usageLogs
.
value
.
length
===
0
)
{
appStore
.
showWarning
(
t
(
'
usage.noDataToExport
'
))
appStore
.
showWarning
(
t
(
'
usage.noDataToExport
'
))
...
@@ -1070,8 +1124,8 @@ const hideTooltip = () => {
...
@@ -1070,8 +1124,8 @@ const hideTooltip = () => {
}
}
onMounted
(()
=>
{
onMounted
(()
=>
{
initializeDateRange
()
loadFilterOptions
()
loadFilterOptions
()
loadApiKeys
()
loadUsageLogs
()
loadUsageLogs
()
loadUsageStats
()
loadUsageStats
()
loadChartData
()
loadChartData
()
...
@@ -1083,5 +1137,8 @@ onUnmounted(() => {
...
@@ -1083,5 +1137,8 @@ onUnmounted(() => {
if
(
searchTimeout
)
{
if
(
searchTimeout
)
{
clearTimeout
(
searchTimeout
)
clearTimeout
(
searchTimeout
)
}
}
if
(
abortController
)
{
abortController
.
abort
()
}
})
})
</
script
>
</
script
>
frontend/src/views/admin/UsersView.vue
View file @
3c341947
This diff is collapsed.
Click to expand it.
frontend/src/views/auth/LoginView.vue
View file @
3c341947
...
@@ -39,6 +39,7 @@
...
@@ -39,6 +39,7 @@
v-model=
"formData.email"
v-model=
"formData.email"
type=
"email"
type=
"email"
required
required
autofocus
autocomplete=
"email"
autocomplete=
"email"
:disabled=
"isLoading"
:disabled=
"isLoading"
class=
"input pl-11"
class=
"input pl-11"
...
...
frontend/src/views/auth/RegisterView.vue
View file @
3c341947
...
@@ -66,6 +66,7 @@
...
@@ -66,6 +66,7 @@
v
-
model
=
"
formData.email
"
v
-
model
=
"
formData.email
"
type
=
"
email
"
type
=
"
email
"
required
required
autofocus
autocomplete
=
"
email
"
autocomplete
=
"
email
"
:
disabled
=
"
isLoading
"
:
disabled
=
"
isLoading
"
class
=
"
input pl-11
"
class
=
"
input pl-11
"
...
...
frontend/src/views/setup/SetupWizardView.vue
View file @
3c341947
...
@@ -563,13 +563,13 @@ const installing = ref(false)
...
@@ -563,13 +563,13 @@ const installing = ref(false)
const
confirmPassword
=
ref
(
''
)
const
confirmPassword
=
ref
(
''
)
const
serviceReady
=
ref
(
false
)
const
serviceReady
=
ref
(
false
)
//
Get curren
t server port
from browser location (set by install.sh)
//
Defaul
t server port
const
getCurrentPort
=
():
number
=>
{
const
getCurrentPort
=
():
number
=>
{
const
port
=
window
.
location
.
port
const
port
=
window
.
location
.
port
if
(
port
)
{
if
(
port
)
{
return
parseInt
(
port
,
10
)
return
parseInt
(
port
,
10
)
}
}
// Default port based on protocol
return
window
.
location
.
protocol
===
'
https:
'
?
443
:
80
return
window
.
location
.
protocol
===
'
https:
'
?
443
:
80
}
}
...
@@ -674,42 +674,35 @@ async function performInstall() {
...
@@ -674,42 +674,35 @@ async function performInstall() {
// Wait for service to restart and become available
// Wait for service to restart and become available
async
function
waitForServiceRestart
()
{
async
function
waitForServiceRestart
()
{
const
maxAttempts
=
3
0
//
3
0 attempts, ~
3
0 seconds max
const
maxAttempts
=
6
0
//
Increase to 6
0 attempts, ~
6
0 seconds max
const
interval
=
1000
// 1 second between attempts
const
interval
=
1000
// 1 second between attempts
// Wait a moment for the service to start restarting
// Wait a moment for the service to start restarting
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
2
000
))
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
3
000
))
for
(
let
attempt
=
0
;
attempt
<
maxAttempts
;
attempt
++
)
{
for
(
let
attempt
=
0
;
attempt
<
maxAttempts
;
attempt
++
)
{
try
{
try
{
// Try to access the health endpoint
// Use setup status endpoint as it tells us the real mode
const
response
=
await
fetch
(
'
/health
'
,
{
// Service might return 404 or connection refused while restarting
const
response
=
await
fetch
(
'
/setup/status
'
,
{
method
:
'
GET
'
,
method
:
'
GET
'
,
cache
:
'
no-store
'
cache
:
'
no-store
'
})
})
if
(
response
.
ok
)
{
if
(
response
.
ok
)
{
// Service is up, check if setup is no longer needed
const
data
=
await
response
.
json
()
const
statusResponse
=
await
fetch
(
'
/setup/status
'
,
{
// If needs_setup is false, service has restarted in normal mode
method
:
'
GET
'
,
if
(
data
.
data
&&
!
data
.
data
.
needs_setup
)
{
cache
:
'
no-store
'
serviceReady
.
value
=
true
})
// Redirect to login page after a short delay
setTimeout
(()
=>
{
if
(
statusResponse
.
ok
)
{
window
.
location
.
href
=
'
/login
'
const
data
=
await
statusResponse
.
json
()
},
1500
)
// If needs_setup is false, service has restarted in normal mode
return
if
(
data
.
data
&&
!
data
.
data
.
needs_setup
)
{
serviceReady
.
value
=
true
// Redirect to login page after a short delay
setTimeout
(()
=>
{
window
.
location
.
href
=
'
/login
'
},
1500
)
return
}
}
}
}
}
}
catch
{
}
catch
{
// Service not ready
ye
t, continue polling
// Service not ready
or network error during restar
t, continue polling
}
}
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
interval
))
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
interval
))
...
...
frontend/src/views/user/DashboardView.vue
View file @
3c341947
This diff is collapsed.
Click to expand it.
frontend/src/views/user/KeysView.vue
View file @
3c341947
This diff is collapsed.
Click to expand it.
frontend/src/views/user/ProfileView.vue
View file @
3c341947
...
@@ -244,6 +244,12 @@
...
@@ -244,6 +244,12 @@
autocomplete=
"new-password"
autocomplete=
"new-password"
class=
"input"
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>
<div
class=
"flex justify-end pt-4"
>
<div
class=
"flex justify-end pt-4"
>
...
@@ -392,6 +398,12 @@ const handleChangePassword = async () => {
...
@@ -392,6 +398,12 @@ const handleChangePassword = async () => {
}
}
const
handleUpdateProfile
=
async
()
=>
{
const
handleUpdateProfile
=
async
()
=>
{
// Basic validation
if
(
!
profileForm
.
value
.
username
.
trim
())
{
appStore
.
showError
(
t
(
'
profile.usernameRequired
'
))
return
}
updatingProfile
.
value
=
true
updatingProfile
.
value
=
true
try
{
try
{
const
updatedUser
=
await
userAPI
.
updateProfile
({
const
updatedUser
=
await
userAPI
.
updateProfile
({
...
...
frontend/src/views/user/RedeemView.vue
View file @
3c341947
...
@@ -445,6 +445,7 @@ import { ref, computed, onMounted } from 'vue'
...
@@ -445,6 +445,7 @@ import { ref, computed, onMounted } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useSubscriptionStore
}
from
'
@/stores/subscriptions
'
import
{
redeemAPI
,
authAPI
,
type
RedeemHistoryItem
}
from
'
@/api
'
import
{
redeemAPI
,
authAPI
,
type
RedeemHistoryItem
}
from
'
@/api
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
{
formatDateTime
}
from
'
@/utils/format
'
...
@@ -452,6 +453,7 @@ import { formatDateTime } from '@/utils/format'
...
@@ -452,6 +453,7 @@ import { formatDateTime } from '@/utils/format'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
subscriptionStore
=
useSubscriptionStore
()
const
user
=
computed
(()
=>
authStore
.
user
)
const
user
=
computed
(()
=>
authStore
.
user
)
...
@@ -544,6 +546,16 @@ const handleRedeem = async () => {
...
@@ -544,6 +546,16 @@ const handleRedeem = async () => {
// Refresh user data to get updated balance/concurrency
// Refresh user data to get updated balance/concurrency
await
authStore
.
refreshUser
()
await
authStore
.
refreshUser
()
// If subscription type, immediately refresh subscription status
if
(
result
.
type
===
'
subscription
'
)
{
try
{
await
subscriptionStore
.
fetchActiveSubscriptions
(
true
)
// force refresh
}
catch
(
error
)
{
console
.
error
(
'
Failed to refresh subscriptions after redeem:
'
,
error
)
appStore
.
showWarning
(
t
(
'
redeem.subscriptionRefreshFailed
'
))
}
}
// Clear the input
// Clear the input
redeemCode
.
value
=
''
redeemCode
.
value
=
''
...
...
frontend/src/views/user/UsageView.vue
View file @
3c341947
This diff is collapsed.
Click to expand it.
Prev
1
2
3
4
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