mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 15:10:56 +08:00
fix(search): 搜索框媒体数据源支持多选并默认跟随全局配置 (#6270)
- 搜索来源由单选改为多选,多个来源以逗号分隔传给后端合并展示 - 默认勾选跟随基础设置-媒体搜索数据源,用户手动调整后不再被覆盖 - 更新来源选择相关测试
This commit is contained in:
@@ -3,7 +3,7 @@ import api from '@/api'
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { getNavMenus, getSettingTabs } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useUserStore, useGlobalSettingsStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
@@ -33,6 +33,9 @@ const router = useRouter()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 全局设置 Store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 当前用户名
|
||||
const userName = userStore.userName
|
||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||
@@ -112,14 +115,61 @@ interface MediaSearchAction {
|
||||
description: string
|
||||
}
|
||||
|
||||
// 三类搜索各自维护来源选择,首次使用均默认 TheMovieDB。
|
||||
const selectedMediaSearchSources = reactive<Record<MediaSearchType, MediaSearchSource>>({
|
||||
media: 'themoviedb',
|
||||
music: 'musicbrainz',
|
||||
collection: 'themoviedb',
|
||||
person: 'themoviedb',
|
||||
// 三类搜索的媒体数据源可多选,为空数组时由后端按“基础设置-媒体搜索数据源”全局配置执行。
|
||||
const selectedMediaSearchSources = reactive<Record<MediaSearchType, MediaSearchSource[]>>({
|
||||
media: [],
|
||||
music: ['musicbrainz'],
|
||||
collection: [],
|
||||
person: [],
|
||||
})
|
||||
|
||||
// 全局“媒体搜索数据源”配置(SEARCH_SOURCE),用于搜索框默认勾选,保证基础设置生效。
|
||||
const configuredMediaSearchSources = computed((): MediaSearchSource[] => {
|
||||
const raw = globalSettingsStore.get('SEARCH_SOURCE')
|
||||
if (!raw) return []
|
||||
return String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim() as MediaSearchSource)
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
// 标记用户是否已手动调整过来源,手动调整后不再被全局配置覆盖。
|
||||
let sourcesTouched = false
|
||||
// 标记是否为默认勾选初始化引起的变更,避免误判为用户操作。
|
||||
let isInitializingSources = false
|
||||
|
||||
/**
|
||||
* 依据全局媒体搜索数据源配置初始化各类搜索的来源勾选;
|
||||
* 配置项不包含该类型可用来源时回退到 TheMovieDB,保证至少一个来源被选中。
|
||||
*/
|
||||
function initializeMediaSearchSources() {
|
||||
isInitializingSources = true
|
||||
try {
|
||||
;(['media', 'collection', 'person'] as const).forEach(searchType => {
|
||||
const configured = configuredMediaSearchSources.value.filter(source =>
|
||||
mediaSearchSourceOptions.value[searchType].some(option => option.value === source),
|
||||
)
|
||||
selectedMediaSearchSources[searchType] = configured.length > 0 ? configured : ['themoviedb']
|
||||
})
|
||||
} finally {
|
||||
isInitializingSources = false
|
||||
}
|
||||
}
|
||||
|
||||
// 全局配置异步加载完成后,若用户尚未手动调整,则按配置补充一次默认勾选。
|
||||
watch(configuredMediaSearchSources, () => {
|
||||
if (!sourcesTouched) initializeMediaSearchSources()
|
||||
})
|
||||
|
||||
// 用户手动改动来源后记录标记,后续全局配置再变化也不覆盖用户选择。
|
||||
watch(
|
||||
selectedMediaSearchSources,
|
||||
() => {
|
||||
if (!isInitializingSources) sourcesTouched = true
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 按后端实际能力限定每类搜索可选的数据源。
|
||||
const mediaSearchSourceOptions = computed<Record<MediaSearchType, MediaSearchSourceOption[]>>(() => {
|
||||
const themoviedb = {
|
||||
@@ -398,7 +448,7 @@ function searchSubtitle() {
|
||||
closeSearch()
|
||||
}
|
||||
|
||||
/** 跳转到指定类型的媒体搜索结果页。 */
|
||||
/** 使用当前关键词搜索指定类型的媒体信息。 */
|
||||
function searchMedia(searchType: MediaSearchType) {
|
||||
if (!searchWord.value || !hasDiscoveryPermission.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
@@ -410,13 +460,16 @@ function searchMedia(searchType: MediaSearchType) {
|
||||
closeSearch()
|
||||
return
|
||||
}
|
||||
const query: Record<string, string> = {
|
||||
title: searchWord.value,
|
||||
type: searchType,
|
||||
}
|
||||
// 多个来源以逗号分隔传给后端,未勾选任何来源时不传,由后端按全局配置搜索。
|
||||
const sources = selectedMediaSearchSources[searchType]
|
||||
if (sources.length > 0) query.source = sources.join(',')
|
||||
router.push({
|
||||
path: '/browse/media/search',
|
||||
query: {
|
||||
title: searchWord.value,
|
||||
type: searchType,
|
||||
source: selectedMediaSearchSources[searchType],
|
||||
},
|
||||
query,
|
||||
})
|
||||
closeSearch()
|
||||
}
|
||||
@@ -512,6 +565,8 @@ watch(dialog, async isOpen => {
|
||||
defineExpose({ focusSearchInput })
|
||||
|
||||
onMounted(() => {
|
||||
// 搜索来源默认跟随全局媒体搜索数据源配置,保证基础设置生效
|
||||
initializeMediaSearchSources()
|
||||
// 根据权限加载不同的数据
|
||||
if (hasAdminPermission.value) {
|
||||
fetchInstalledPlugins()
|
||||
@@ -618,6 +673,7 @@ onMounted(() => {
|
||||
v-model="selectedMediaSearchSources[action.type]"
|
||||
class="search-item-source-toggle"
|
||||
density="compact"
|
||||
multiple
|
||||
mandatory
|
||||
role="group"
|
||||
selected-class="media-source-button--active"
|
||||
|
||||
@@ -14,7 +14,11 @@ const IconStub = defineComponent({
|
||||
template: '<i :data-icon="icon" />',
|
||||
})
|
||||
|
||||
async function renderSearchBar() {
|
||||
async function renderSearchBar(
|
||||
overrides: {
|
||||
searchSource?: string
|
||||
} = {},
|
||||
) {
|
||||
return renderWithProviders(SearchBarDialog, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
@@ -32,7 +36,13 @@ async function renderSearchBar() {
|
||||
},
|
||||
superUser: false,
|
||||
},
|
||||
// 全局设置走真实 action,便于断言“媒体搜索数据源”配置对搜索框默认勾选的影响。
|
||||
globalSettings: {
|
||||
data: overrides.searchSource ? { SEARCH_SOURCE: overrides.searchSource } : {},
|
||||
initialized: true,
|
||||
},
|
||||
},
|
||||
stubActions: false,
|
||||
global: {
|
||||
stubs: {
|
||||
VIcon: IconStub,
|
||||
@@ -52,7 +62,7 @@ describe('SearchBarDialog media source selection', () => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('defaults media searches to TheMovieDB', async () => {
|
||||
it('defaults media searches to TheMovieDB when no global search source is configured', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { router } = await renderSearchBar()
|
||||
const input = await screen.findByPlaceholderText('搜索电影、剧集以及更多...')
|
||||
@@ -72,7 +82,38 @@ describe('SearchBarDialog media source selection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('places supported sources inside each search item and uses the selected source', async () => {
|
||||
it('follows the global media search source configuration and passes multiple sources', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { router } = await renderSearchBar({ searchSource: 'themoviedb,douban' })
|
||||
const input = await screen.findByPlaceholderText('搜索电影、剧集以及更多...')
|
||||
|
||||
await user.type(input, '芙莉莲')
|
||||
const mediaItem = getSearchItem('电影、电视剧')
|
||||
|
||||
const mediaGroup = within(mediaItem).getByRole('group', { name: '电影、电视剧搜索数据源' })
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 TheMovieDb 搜索' })).toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 豆瓣 搜索' })).toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 Bangumi 搜索' })).not.toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
|
||||
await user.click(input)
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(router.currentRoute.value.query).toEqual({
|
||||
source: 'themoviedb,douban',
|
||||
title: '芙莉莲',
|
||||
type: 'media',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('places supported sources inside each search item and supports multi-source selection', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { router } = await renderSearchBar()
|
||||
const input = await screen.findByPlaceholderText('搜索电影、剧集以及更多...')
|
||||
@@ -93,7 +134,19 @@ describe('SearchBarDialog media source selection', () => {
|
||||
'media-source-button--active',
|
||||
)
|
||||
|
||||
// 追加选择 AniList 后再取消 TheMovieDb,来源应以逗号分隔传给后端。
|
||||
await user.click(within(mediaGroup).getByRole('button', { name: '使用 AniList 搜索' }))
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 AniList 搜索' })).toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
await user.click(within(mediaGroup).getByRole('button', { name: '使用 TheMovieDb 搜索' }))
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 AniList 搜索' })).toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
expect(within(mediaGroup).getByRole('button', { name: '使用 TheMovieDb 搜索' })).not.toHaveClass(
|
||||
'media-source-button--active',
|
||||
)
|
||||
|
||||
await user.click(input)
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
@@ -127,7 +180,9 @@ describe('SearchBarDialog media source selection', () => {
|
||||
const personItem = getSearchItem('演员')
|
||||
|
||||
const personGroup = within(personItem).getByRole('group', { name: '演员搜索数据源' })
|
||||
// 追加选择豆瓣并取消默认的 TheMovieDb,仅保留豆瓣来源。
|
||||
await user.click(within(personGroup).getByRole('button', { name: '使用 豆瓣 搜索' }))
|
||||
await user.click(within(personGroup).getByRole('button', { name: '使用 TheMovieDb 搜索' }))
|
||||
await user.click(personItem)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -138,4 +193,4 @@ describe('SearchBarDialog media source selection', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user