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
You need to sign in or sign up before continuing.
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 @@
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.user
'
)
}}
<
/label
>
<
Select
v
-
model
=
"
assignForm.user_id
"
:
options
=
"
userOptions
"
:
placeholder
=
"
t('admin.subscriptions.selectUser')
"
searchable
/>
<
div
class
=
"
relative
"
>
<
input
v
-
model
=
"
userSearchKeyword
"
type
=
"
text
"
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
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.group
'
)
}}
<
/label
>
...
...
@@ -462,11 +509,12 @@
<
/template
>
<
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
{
useAppStore
}
from
'
@/stores/app
'
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
{
formatDateOnly
}
from
'
@/utils/format
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
...
@@ -501,9 +549,17 @@ const statusOptions = computed(() => [
const
subscriptions
=
ref
<
UserSubscription
[]
>
([])
const
groups
=
ref
<
Group
[]
>
([])
const
users
=
ref
<
User
[]
>
([])
const
loading
=
ref
(
false
)
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
({
status
:
''
,
group_id
:
''
...
...
@@ -545,9 +601,6 @@ const subscriptionGroupOptions = computed(() =>
.
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
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
...
...
@@ -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
{
const
response
=
await
adminAPI
.
users
.
list
(
1
,
1000
)
users
.
value
=
response
.
items
userSearchResults
.
value
=
await
adminAPI
.
usage
.
searchUsers
(
keyword
)
}
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
)
=>
{
pagination
.
page
=
page
loadSubscriptions
()
...
...
@@ -615,6 +706,11 @@ const closeAssignModal = () => {
assignForm
.
user_id
=
null
assignForm
.
group_id
=
null
assignForm
.
validity_days
=
30
// Clear user search state
selectedUser
.
value
=
null
userSearchKeyword
.
value
=
''
userSearchResults
.
value
=
[]
showUserDropdown
.
value
=
false
}
const
handleAssignSubscription
=
async
()
=>
{
...
...
@@ -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
(()
=>
{
loadSubscriptions
()
loadGroups
()
loadUsers
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
}
)
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
if
(
userSearchTimeout
)
{
clearTimeout
(
userSearchTimeout
)
}
}
)
<
/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