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
64b82192
Commit
64b82192
authored
Dec 30, 2025
by
shaw
Browse files
fix: 分配订阅的用户搜索改为后端搜索
parent
2004230b
Changes
1
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/SubscriptionsView.vue
View file @
64b82192
...
@@ -335,12 +335,59 @@
...
@@ -335,12 +335,59 @@
>
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.user
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.user
'
)
}}
<
/label
>
<
Select
<
div
class
=
"
relative
"
>
v
-
model
=
"
assignForm.user_id
"
<
input
:
options
=
"
userOptions
"
v
-
model
=
"
userSearchKeyword
"
:
placeholder
=
"
t('admin.subscriptions.selectUser')
"
type
=
"
text
"
searchable
class
=
"
input pr-8
"
/>
:
placeholder
=
"
t('admin.usage.searchUserPlaceholder')
"
@
input
=
"
debounceSearchUsers
"
@
focus
=
"
showUserDropdown = true
"
/>
<
button
v
-
if
=
"
selectedUser
"
@
click
=
"
clearUserSelection
"
type
=
"
button
"
class
=
"
absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300
"
>
<
svg
class
=
"
h-4 w-4
"
fill
=
"
none
"
stroke
=
"
currentColor
"
viewBox
=
"
0 0 24 24
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M6 18L18 6M6 6l12 12
"
/>
<
/svg
>
<
/button
>
<!--
User
Dropdown
-->
<
div
v
-
if
=
"
showUserDropdown && (userSearchResults.length > 0 || userSearchKeyword)
"
class
=
"
absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800
"
>
<
div
v
-
if
=
"
userSearchLoading
"
class
=
"
px-4 py-3 text-sm text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.loading
'
)
}}
<
/div
>
<
div
v
-
else
-
if
=
"
userSearchResults.length === 0 && userSearchKeyword
"
class
=
"
px-4 py-3 text-sm text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.noOptionsFound
'
)
}}
<
/div
>
<
button
v
-
for
=
"
user in userSearchResults
"
:
key
=
"
user.id
"
type
=
"
button
"
@
click
=
"
selectUser(user)
"
class
=
"
w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700
"
>
<
span
class
=
"
font-medium text-gray-900 dark:text-white
"
>
{{
user
.
email
}}
<
/span
>
<
span
class
=
"
ml-2 text-gray-500 dark:text-gray-400
"
>
#
{{
user
.
id
}}
<
/span
>
<
/button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.group
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.group
'
)
}}
<
/label
>
...
@@ -462,11 +509,12 @@
...
@@ -462,11 +509,12 @@
<
/template
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
UserSubscription
,
Group
,
User
}
from
'
@/types
'
import
type
{
UserSubscription
,
Group
}
from
'
@/types
'
import
type
{
SimpleUser
}
from
'
@/api/admin/usage
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
{
formatDateOnly
}
from
'
@/utils/format
'
import
{
formatDateOnly
}
from
'
@/utils/format
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
@@ -501,9 +549,17 @@ const statusOptions = computed(() => [
...
@@ -501,9 +549,17 @@ const statusOptions = computed(() => [
const
subscriptions
=
ref
<
UserSubscription
[]
>
([])
const
subscriptions
=
ref
<
UserSubscription
[]
>
([])
const
groups
=
ref
<
Group
[]
>
([])
const
groups
=
ref
<
Group
[]
>
([])
const
users
=
ref
<
User
[]
>
([])
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
let
abortController
:
AbortController
|
null
=
null
let
abortController
:
AbortController
|
null
=
null
// User search state
const
userSearchKeyword
=
ref
(
''
)
const
userSearchResults
=
ref
<
SimpleUser
[]
>
([])
const
userSearchLoading
=
ref
(
false
)
const
showUserDropdown
=
ref
(
false
)
const
selectedUser
=
ref
<
SimpleUser
|
null
>
(
null
)
let
userSearchTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
filters
=
reactive
({
const
filters
=
reactive
({
status
:
''
,
status
:
''
,
group_id
:
''
group_id
:
''
...
@@ -545,9 +601,6 @@ const subscriptionGroupOptions = computed(() =>
...
@@ -545,9 +601,6 @@ const subscriptionGroupOptions = computed(() =>
.
map
((
g
)
=>
({
value
:
g
.
id
,
label
:
g
.
name
}
))
.
map
((
g
)
=>
({
value
:
g
.
id
,
label
:
g
.
name
}
))
)
)
// User options for assign
const
userOptions
=
computed
(()
=>
users
.
value
.
map
((
u
)
=>
({
value
:
u
.
id
,
label
:
u
.
email
}
)))
const
loadSubscriptions
=
async
()
=>
{
const
loadSubscriptions
=
async
()
=>
{
if
(
abortController
)
{
if
(
abortController
)
{
abortController
.
abort
()
abortController
.
abort
()
...
@@ -590,15 +643,53 @@ const loadGroups = async () => {
...
@@ -590,15 +643,53 @@ const loadGroups = async () => {
}
}
}
}
const
loadUsers
=
async
()
=>
{
// User search with debounce
const
debounceSearchUsers
=
()
=>
{
if
(
userSearchTimeout
)
{
clearTimeout
(
userSearchTimeout
)
}
userSearchTimeout
=
setTimeout
(
searchUsers
,
300
)
}
const
searchUsers
=
async
()
=>
{
const
keyword
=
userSearchKeyword
.
value
.
trim
()
// Clear selection if user modified the search keyword
if
(
selectedUser
.
value
&&
keyword
!==
selectedUser
.
value
.
email
)
{
selectedUser
.
value
=
null
assignForm
.
user_id
=
null
}
if
(
!
keyword
)
{
userSearchResults
.
value
=
[]
return
}
userSearchLoading
.
value
=
true
try
{
try
{
const
response
=
await
adminAPI
.
users
.
list
(
1
,
1000
)
userSearchResults
.
value
=
await
adminAPI
.
usage
.
searchUsers
(
keyword
)
users
.
value
=
response
.
items
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Error loading users:
'
,
error
)
console
.
error
(
'
Failed to search users:
'
,
error
)
userSearchResults
.
value
=
[]
}
finally
{
userSearchLoading
.
value
=
false
}
}
}
}
const
selectUser
=
(
user
:
SimpleUser
)
=>
{
selectedUser
.
value
=
user
userSearchKeyword
.
value
=
user
.
email
showUserDropdown
.
value
=
false
assignForm
.
user_id
=
user
.
id
}
const
clearUserSelection
=
()
=>
{
selectedUser
.
value
=
null
userSearchKeyword
.
value
=
''
userSearchResults
.
value
=
[]
assignForm
.
user_id
=
null
}
const
handlePageChange
=
(
page
:
number
)
=>
{
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
pagination
.
page
=
page
loadSubscriptions
()
loadSubscriptions
()
...
@@ -615,6 +706,11 @@ const closeAssignModal = () => {
...
@@ -615,6 +706,11 @@ const closeAssignModal = () => {
assignForm
.
user_id
=
null
assignForm
.
user_id
=
null
assignForm
.
group_id
=
null
assignForm
.
group_id
=
null
assignForm
.
validity_days
=
30
assignForm
.
validity_days
=
30
// Clear user search state
selectedUser
.
value
=
null
userSearchKeyword
.
value
=
''
userSearchResults
.
value
=
[]
showUserDropdown
.
value
=
false
}
}
const
handleAssignSubscription
=
async
()
=>
{
const
handleAssignSubscription
=
async
()
=>
{
...
@@ -754,10 +850,25 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
...
@@ -754,10 +850,25 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
}
}
}
}
// Handle click outside to close user dropdown
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
if
(
!
target
.
closest
(
'
.relative
'
))
{
showUserDropdown
.
value
=
false
}
}
onMounted
(()
=>
{
onMounted
(()
=>
{
loadSubscriptions
()
loadSubscriptions
()
loadGroups
()
loadGroups
()
loadUsers
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
}
)
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
if
(
userSearchTimeout
)
{
clearTimeout
(
userSearchTimeout
)
}
}
)
}
)
<
/script
>
<
/script
>
...
...
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