From be40f55bd9d14692905a2d26fa66f54182899c52 Mon Sep 17 00:00:00 2001 From: PKC278 <52959804+PKC278@users.noreply.github.com> Date: Thu, 15 Jan 2026 03:04:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(search):=20=E6=B7=BB=E5=8A=A0AI=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 + src/@iconify/build-icons.ts | 11 +- src/App.vue | 6 +- src/components/cards/TorrentCard.vue | 6 +- src/components/cards/TorrentItem.vue | 6 +- src/components/dialog/AddDownloadDialog.vue | 5 - src/components/dialog/SearchBarDialog.vue | 2 +- src/components/dialog/SiteAddEditDialog.vue | 10 +- src/components/filter/TorrentFilterBar.vue | 812 +++++++++++++++++ src/composables/useInfiniteScroll.ts | 60 ++ src/composables/useTorrentFilter.ts | 502 +++++++++++ src/locales/en-US.ts | 13 +- src/locales/zh-CN.ts | 11 + src/locales/zh-TW.ts | 13 +- src/pages/login.vue | 7 +- src/pages/resource.vue | 764 +++++++++++++--- src/stores/global.ts | 14 + src/views/setting/AccountSettingSystem.vue | 32 + src/views/torrent/TorrentCardListView.vue | 918 -------------------- src/views/torrent/TorrentRowListView.vue | 910 ------------------- yarn.lock | 21 + 21 files changed, 2175 insertions(+), 1952 deletions(-) create mode 100644 src/components/filter/TorrentFilterBar.vue create mode 100644 src/composables/useInfiniteScroll.ts create mode 100644 src/composables/useTorrentFilter.ts delete mode 100644 src/views/torrent/TorrentCardListView.vue delete mode 100644 src/views/torrent/TorrentRowListView.vue diff --git a/package.json b/package.json index 1c7c8d4c..251cef8c 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "bin": "dist/service.js", "scripts": { "dev": "vite --host", + "prebuild": "npm run build:icons", "build": "vite build", "preview": "vite preview --port 5050", "typecheck": "vue-tsc --noEmit", @@ -71,6 +72,9 @@ "webfontloader": "^1.6.28" }, "devDependencies": { + "@iconify-json/line-md": "^1.2.13", + "@iconify-json/lucide": "^1.2.85", + "@iconify-json/material-symbols": "^1.2.51", "@iconify-json/mdi": "^1.1.52", "@iconify/tools": "^4.0.4", "@iconify/vue": "^4.3.0", diff --git a/src/@iconify/build-icons.ts b/src/@iconify/build-icons.ts index 1c6913ce..519407d8 100644 --- a/src/@iconify/build-icons.ts +++ b/src/@iconify/build-icons.ts @@ -92,6 +92,9 @@ const sources: BundleScriptConfig = { // 'mdi:logout', // 'octicon:book-24', // 'octicon:code-square-24', + 'lucide:sparkles', + 'material-symbols:passkey', + 'line-md:loading-twotone-loop', ], json: [ @@ -154,7 +157,13 @@ const target = join(__dirname, 'icons-bundle.js'); // Sort icons by prefix const organizedList = organizeIconsList(sources.icons) for (const prefix in organizedList) { - const filename = require.resolve(`@iconify/json/json/${prefix}.json`) + let filename + try { + filename = require.resolve(`@iconify-json/${prefix}/icons.json`) + } + catch (err) { + filename = require.resolve(`@iconify/json/json/${prefix}.json`) + } sourcesJSON.push({ filename, diff --git a/src/App.vue b/src/App.vue index 29da4893..ccc5e112 100644 --- a/src/App.vue +++ b/src/App.vue @@ -192,7 +192,11 @@ async function removeLoadingWithStateCheck() { // 并行加载关键资源 await Promise.all([ - globalSettingsStore.initialize().then(() => { + globalSettingsStore.initialize().then(async () => { + // 如果已登录,加载用户相关设置 + if (isLogin.value) { + await globalSettingsStore.loadUserSettings() + } globalLoadingStateManager.setLoadingState('global-settings', false) }), new Promise(resolve => { diff --git a/src/components/cards/TorrentCard.vue b/src/components/cards/TorrentCard.vue index 29e3a9b7..e4cba49a 100644 --- a/src/components/cards/TorrentCard.vue +++ b/src/components/cards/TorrentCard.vue @@ -137,7 +137,7 @@ onMounted(() => {
- + {{ media?.title ?? meta?.name }} { -
+
{{ torrent?.title }}
{{ meta?.subtitle || torrent?.description }} diff --git a/src/components/cards/TorrentItem.vue b/src/components/cards/TorrentItem.vue index 2b1f85c5..7556b31d 100644 --- a/src/components/cards/TorrentItem.vue +++ b/src/components/cards/TorrentItem.vue @@ -140,7 +140,7 @@ onMounted(() => {
- +
{{ media?.title ?? meta?.name }} {
-
+
{{ torrent?.title }}
{{ meta?.subtitle || torrent?.description || '暂无描述' }} diff --git a/src/components/dialog/AddDownloadDialog.vue b/src/components/dialog/AddDownloadDialog.vue index 7e877696..bcc4ba34 100644 --- a/src/components/dialog/AddDownloadDialog.vue +++ b/src/components/dialog/AddDownloadDialog.vue @@ -223,7 +223,6 @@ onMounted(() => { { v-model="selectedDirectory" :items="targetDirectories" :label="t('dialog.addDownload.saveDirectory')" - size="small" :placeholder="t('dialog.addDownload.autoPlaceholder')" variant="underlined" density="comfortable" @@ -248,7 +246,6 @@ onMounted(() => { @@ -272,7 +269,6 @@ onMounted(() => { :hint="t('dialog.reorganize.mediaIdHint')" persistent-hint prepend-inner-icon="mdi-identifier" - size="small" variant="underlined" density="comfortable" @click:append-inner="mediaSelectorDialog = true" @@ -287,7 +283,6 @@ onMounted(() => { :hint="t('dialog.reorganize.mediaIdHint')" persistent-hint prepend-inner-icon="mdi-identifier" - size="small" variant="underlined" density="comfortable" @click:append-inner="mediaSelectorDialog = true" diff --git a/src/components/dialog/SearchBarDialog.vue b/src/components/dialog/SearchBarDialog.vue index 4ef28ed0..2236c14f 100644 --- a/src/components/dialog/SearchBarDialog.vue +++ b/src/components/dialog/SearchBarDialog.vue @@ -138,7 +138,7 @@ function getMenus(): NavMenu[] { item => item && menus.push({ - title: t('setting') + ' -> ' + item.title, + title: t('navItems.setting') + ' -> ' + item.title, icon: item.icon, to: `/setting?tab=${item.tab}`, header: '', diff --git a/src/components/dialog/SiteAddEditDialog.vue b/src/components/dialog/SiteAddEditDialog.vue index 1b402fb0..077f8750 100644 --- a/src/components/dialog/SiteAddEditDialog.vue +++ b/src/components/dialog/SiteAddEditDialog.vue @@ -140,7 +140,7 @@ onMounted(async () => { await fetchSiteInfo() if (siteForm.value.limit_interval || siteForm.value.limit_count || siteForm.value.limit_seconds) isLimit.value = true - if (siteForm.value.apikey) siteType.value = 'api' + if (siteForm.value.apikey || siteForm.value.token) siteType.value = 'api' } await loadDownloaderSetting() }) @@ -224,15 +224,15 @@ onMounted(async () => { - +
- + Cookie
- +
- + API
diff --git a/src/components/filter/TorrentFilterBar.vue b/src/components/filter/TorrentFilterBar.vue new file mode 100644 index 00000000..9b37a20f --- /dev/null +++ b/src/components/filter/TorrentFilterBar.vue @@ -0,0 +1,812 @@ + + + + + diff --git a/src/composables/useInfiniteScroll.ts b/src/composables/useInfiniteScroll.ts new file mode 100644 index 00000000..592d59b1 --- /dev/null +++ b/src/composables/useInfiniteScroll.ts @@ -0,0 +1,60 @@ +import type { Ref } from 'vue' + +type InfiniteScrollStatus = 'ok' | 'empty' | 'loading' | 'error' + +/** + * 无限滚动 composable + * 用于管理分页显示和无限滚动加载 + * @param sourceData - 源数据(响应式引用) + * @param pageSize - 每页显示数量,默认20 + */ +export function useInfiniteScroll( + sourceData: Ref, + pageSize: number = 20 +) { + // 显示用的数据列表 + const displayDataList = ref([]) + + // 剩余数据列表(用于无限滚动) + const remainingDataList = ref([]) as Ref + + // 初始化数据 + function initData() { + if (sourceData.value?.length) { + // 显示前 pageSize 个 + displayDataList.value = sourceData.value.slice(0, pageSize) as T[] + // 保存剩余数据 + remainingDataList.value = sourceData.value.slice(pageSize) as T[] + } else { + displayDataList.value = [] + remainingDataList.value = [] + } + } + + // 加载更多 + function loadMore({ done }: { done: (status: InfiniteScrollStatus) => void }) { + // 从 remainingDataList 中获取最前面的 pageSize 个元素 + const itemsToMove = remainingDataList.value.splice(0, pageSize) as T[] + ;(displayDataList.value as T[]).push(...itemsToMove) + done('ok') + } + + // 重置数据 + function reset() { + displayDataList.value = [] + remainingDataList.value = [] + } + + // 监听源数据变化,重新初始化 + watch(sourceData, () => { + initData() + }, { deep: true, immediate: true }) + + return { + displayDataList, + remainingDataList, + initData, + loadMore, + reset, + } +} diff --git a/src/composables/useTorrentFilter.ts b/src/composables/useTorrentFilter.ts new file mode 100644 index 00000000..710fb378 --- /dev/null +++ b/src/composables/useTorrentFilter.ts @@ -0,0 +1,502 @@ +import type { Context } from '@/api/types' +import { cloneDeepWith } from 'lodash-es' +import { useI18n } from 'vue-i18n' + +// 卡片视图的分组数据类型 +interface SearchTorrent extends Context { + more?: Array +} + +interface GroupedItem { + data: SearchTorrent + originalIndex: number +} + +// 筛选状态类型 +export interface FilterState { + filterForm: Record + filterOptions: Record + sortField: string + sortType: 'asc' | 'desc' +} + +// useTorrentFilter composable +export function useTorrentFilter() { + const { t } = useI18n() + + // 过滤表单 + const filterForm: Record = reactive({ + site: [] as string[], + season: [] as string[], + releaseGroup: [] as string[], + videoCode: [] as string[], + freeState: [] as string[], + edition: [] as string[], + resolution: [] as string[], + }) + + // 统一存储过滤选项 + const filterOptions: Record = reactive({ + site: [] as string[], + season: [] as string[], + freeState: [] as string[], + edition: [] as string[], + resolution: [] as string[], + videoCode: [] as string[], + releaseGroup: [] as string[], + }) + + // 排序字段 + const sortField = ref('default') + // 排序方向 + const sortType = ref<'asc' | 'desc'>('desc') + + // 过滤项映射 + const filterTitles: Record = { + site: t('torrent.filterSite'), + season: t('torrent.filterSeason'), + freeState: t('torrent.filterFreeState'), + videoCode: t('torrent.filterVideoCode'), + edition: t('torrent.filterEdition'), + resolution: t('torrent.filterResolution'), + releaseGroup: t('torrent.filterReleaseGroup'), + } + + // 排序中文名 + const sortTitles: Record = { + default: t('torrent.sortDefault'), + site: t('torrent.sortSite'), + size: t('torrent.sortSize'), + seeder: t('torrent.sortSeeder'), + publishTime: t('torrent.sortPublishTime'), + } + + // 筛选后数据的原始索引列表 + const filteredIndices = ref([]) + + // 筛选后的总数量 + const totalFilteredCount = ref(0) + + // 初始化过滤选项 + function initOptions(data: Context) { + const { torrent_info, meta_info } = data + const optionValue = (options: Array, value: string | undefined) => { + if (value && !options.includes(value)) { + options.push(value) + // 如果是season选项,立即触发重新计算 + if (options === filterOptions.season) { + sortSeasonOptions() + } + } + } + + optionValue(filterOptions.site, torrent_info?.site_name) + optionValue(filterOptions.season, meta_info?.season_episode) + optionValue(filterOptions.releaseGroup, meta_info?.resource_team) + optionValue(filterOptions.videoCode, meta_info?.video_encode) + optionValue(filterOptions.freeState, torrent_info?.volume_factor) + optionValue(filterOptions.edition, meta_info?.edition) + optionValue(filterOptions.resolution, meta_info?.resource_pix) + } + + // 直接对季集选项进行排序的函数 + function sortSeasonOptions() { + if (filterOptions.season.length <= 1) { + return + } + + const parsedOptions = filterOptions.season.map((option, index) => { + const match = option.match(/^S(\d+)(?:-S(\d+))?\s*(?:E(\d+)(?:-E(\d+))?)?$/) + + if (!match) { + return { + original: option, + seasonNum: 0, + episodeNum: 0, + maxEpisodeNum: 0, + isWholeSeason: false, + index, + } + } + + const seasonNum = parseInt(match[1], 10) + const episodeNum = match[3] ? parseInt(match[3], 10) : 0 + const maxEpisodeNum = match[4] ? parseInt(match[4], 10) : episodeNum + const isWholeSeason = !match[3] + + return { + original: option, + seasonNum, + episodeNum, + maxEpisodeNum, + isWholeSeason, + index, + } + }) + + const wholeSeasons = parsedOptions.filter(item => item.isWholeSeason) + const episodes = parsedOptions.filter(item => !item.isWholeSeason) + + wholeSeasons.sort((a, b) => { + if (a.seasonNum !== b.seasonNum) { + return b.seasonNum - a.seasonNum + } + return a.index - b.index + }) + + episodes.sort((a, b) => { + if (a.seasonNum !== b.seasonNum) { + return b.seasonNum - a.seasonNum + } + const aMaxEp = a.maxEpisodeNum || a.episodeNum + const bMaxEp = b.maxEpisodeNum || b.episodeNum + if (aMaxEp !== bMaxEp) { + return bMaxEp - aMaxEp + } + if (a.episodeNum !== b.episodeNum) { + return b.episodeNum - a.episodeNum + } + return a.index - b.index + }) + + const sortedOptions = [...wholeSeasons, ...episodes].map(item => item.original) + filterOptions.season = sortedOptions + } + + // 匹配过滤函数 + const match = (filter: Array, value: string | undefined) => + filter.length === 0 || (value && filter.includes(value)) + + // 筛选列表视图数据(不分组) + function filterRowData(items: Context[] | undefined): Context[] { + // 重置状态 + filteredIndices.value = [] + + // 清空并重新初始化过滤选项 + for (const key in filterOptions) { + filterOptions[key] = [] + } + + if (!items?.length) { + totalFilteredCount.value = 0 + return [] + } + + // 首先收集所有过滤选项 + items.forEach(data => { + initOptions(data) + }) + + // 筛选数据 + let filteredData: Context[] = [] + + items.forEach((data, index) => { + const { meta_info, torrent_info } = data + if ( + match(filterForm.site, torrent_info.site_name) && + match(filterForm.freeState, torrent_info.volume_factor) && + match(filterForm.season, meta_info.season_episode) && + match(filterForm.releaseGroup, meta_info.resource_team) && + match(filterForm.videoCode, meta_info.video_encode) && + match(filterForm.resolution, meta_info.resource_pix) && + match(filterForm.edition, meta_info.edition) + ) { + filteredData.push(data) + filteredIndices.value.push(index) + } + }) + + totalFilteredCount.value = filteredData.length + + // 排序 + filteredData = sortData(filteredData) + + // 确保季集选项排序 + if (filterOptions.season.length > 0) { + sortSeasonOptions() + } + + return filteredData + } + + // 筛选卡片视图数据(分组) + function filterCardData(items: Context[] | undefined): SearchTorrent[] { + // 重置状态 + filteredIndices.value = [] + + // 清空并重新初始化过滤选项 + for (const key in filterOptions) { + filterOptions[key] = [] + } + + if (!items?.length) { + totalFilteredCount.value = 0 + return [] + } + + // 数据分组 + const groupMap = new Map() + + items.forEach((item, index) => { + const { torrent_info, meta_info } = item + // init options + initOptions(item) + // group data + const key = `${meta_info.name}_${meta_info.resource_pix}_${meta_info.edition}_${meta_info.resource_team}_${meta_info.season_episode}_${torrent_info.size}` + const groupedItem = { data: item, originalIndex: index } + if (groupMap.has(key)) { + const group = groupMap.get(key) + group?.push(groupedItem) + } else { + groupMap.set(key, [groupedItem]) + } + }) + + // 筛选数据 + const filteredData: SearchTorrent[] = [] + let matchCount = 0 + // 临时存储:每个分组的第一个原始索引 + const groupIndexMap = new Map() + + groupMap.forEach(value => { + if (value.length > 0) { + const matchData = value.filter(item => { + const { meta_info, torrent_info } = item.data + return ( + match(filterForm.site, torrent_info.site_name) && + match(filterForm.freeState, torrent_info.volume_factor) && + match(filterForm.season, meta_info.season_episode) && + match(filterForm.releaseGroup, meta_info.resource_team) && + match(filterForm.videoCode, meta_info.video_encode) && + match(filterForm.resolution, meta_info.resource_pix) && + match(filterForm.edition, meta_info.edition) + ) + }) + if (matchData.length > 0) { + matchCount += matchData.length + const firstItem = matchData[0] + const firstData = cloneDeepWith(firstItem.data) as SearchTorrent + if (matchData.length > 1) firstData.more = matchData.slice(1).map(x => x.data) + filteredData.push(firstData) + // 存储该分组的第一个原始索引 + groupIndexMap.set(firstData, firstItem.originalIndex) + } + } + }) + + totalFilteredCount.value = matchCount + + // 排序数据 + const sortedData = sortCardData(filteredData) + + // 在排序后重新构建 filteredIndices,保持与排序后顺序一致 + filteredIndices.value = sortedData.map(item => groupIndexMap.get(item) || 0) + + // 确保季集选项排序 + if (filterOptions.season.length > 0) { + sortSeasonOptions() + } + + return sortedData + } + + // 排序列表数据 + function sortData(data: Context[]): Context[] { + const sortOrder = sortType.value === 'asc' ? 1 : -1 + + return data.sort((a, b) => { + let result = 0 + switch (sortField.value) { + case 'site': + result = (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || '') + break + case 'size': + result = a.torrent_info.size - b.torrent_info.size + break + case 'seeder': + result = a.torrent_info.seeders - b.torrent_info.seeders + break + case 'publishTime': + result = new Date(a.torrent_info.pubdate || 0).getTime() - new Date(b.torrent_info.pubdate || 0).getTime() + break + case 'default': + default: + result = a.torrent_info.pri_order - b.torrent_info.pri_order + break + } + return result * sortOrder + }) + } + + // 排序卡片数据 + function sortCardData(data: SearchTorrent[]): SearchTorrent[] { + if (sortField.value === 'default') { + return data + } + const sortOrder = sortType.value === 'asc' ? 1 : -1 + return data.sort((a, b) => { + let result = 0 + switch (sortField.value) { + case 'site': + result = (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || '') + break + case 'size': + result = (Number(a.torrent_info.size) || 0) - (Number(b.torrent_info.size) || 0) + break + case 'seeder': + result = (Number(a.torrent_info.seeders) || 0) - (Number(b.torrent_info.seeders) || 0) + break + case 'publishTime': + result = new Date(a.torrent_info.pubdate || 0).getTime() - new Date(b.torrent_info.pubdate || 0).getTime() + break + } + return result * sortOrder + }) + } + + // 计算已选择的过滤条件数量 + const getFilterCount = computed(() => { + let count = 0 + for (const key in filterForm) { + count += filterForm[key].length + } + return count + }) + + // 计算已选择的过滤条件 + const getSelectedFilters = computed(() => { + const filters: Record = {} + for (const key in filterForm) { + if (filterForm[key].length > 0) { + filters[key] = [...filterForm[key]] + } + } + return filters + }) + + // 移除单个过滤条件 + function removeFilter(key: string, value: string) { + const index = filterForm[key].indexOf(value) + if (index !== -1) { + filterForm[key].splice(index, 1) + } + } + + // 清除所有过滤条件 + function clearAllFilters() { + for (const key in filterForm) { + filterForm[key] = [] + } + } + + // 清除某个过滤项 + function clearFilter(key: string) { + filterForm[key] = [] + } + + // 全选某个过滤项 + function selectAll(key: string) { + filterForm[key] = [...filterOptions[key]] + } + + // 给定过滤类型返回不同图标 + function getFilterIcon(key: string) { + const icons: Record = { + site: 'mdi-server-network', + season: 'mdi-television-classic', + freeState: 'mdi-gift-outline', + resolution: 'mdi-monitor-screenshot', + videoCode: 'mdi-video-vintage', + edition: 'mdi-quality-high', + releaseGroup: 'mdi-account-group-outline', + } + return icons[key] || 'mdi-filter-variant' + } + + // 处理排序图标点击 + const handleSortIconClick = () => { + sortType.value = sortType.value === 'asc' ? 'desc' : 'asc' + } + + // 获取筛选后的原始索引列表 + function getFilteredIndices() { + return filteredIndices.value + } + + // 检查是否有活动的筛选条件 + function hasActiveFilters() { + for (const key in filterForm) { + if (filterForm[key] && filterForm[key].length > 0) { + return true + } + } + return false + } + + // 获取当前筛选条件 + function getFilterForm() { + const filters: Record = {} + for (const key in filterForm) { + filters[key] = [...filterForm[key]] + } + return filters + } + + // 设置筛选条件 + function setFilterForm(filters: Record) { + for (const key in filterForm) { + filterForm[key] = filters[key] ? [...filters[key]] : [] + } + } + + // 获取完整的筛选状态 + function getFilterState(): FilterState { + return { + filterForm: getFilterForm(), + filterOptions: { ...filterOptions }, + sortField: sortField.value, + sortType: sortType.value, + } + } + + // 设置完整的筛选状态 + function setFilterState(state: FilterState) { + setFilterForm(state.filterForm) + sortField.value = state.sortField + sortType.value = state.sortType + } + + return { + // 状态 + filterForm, + filterOptions, + sortField, + sortType, + filteredIndices, + totalFilteredCount, + // 标题映射 + filterTitles, + sortTitles, + // 计算属性 + getFilterCount, + getSelectedFilters, + // 筛选方法 + filterRowData, + filterCardData, + // 操作方法 + removeFilter, + clearAllFilters, + clearFilter, + selectAll, + getFilterIcon, + handleSortIconClick, + // 状态管理方法 + getFilteredIndices, + hasActiveFilters, + getFilterForm, + setFilterForm, + getFilterState, + setFilterState, + sortSeasonOptions, + } +} diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 9fb2b3b4..d29c69e2 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -69,7 +69,9 @@ export default { preset: 'Preset', refresh: 'Refresh', swUpdateReady: 'New version is ready, please refresh the page to get the latest features', - versionMismatch: 'Browser cache version does not match server version, please try clearing cache', + ascending: 'Ascending', + descending: 'Descending', + versionMismatch: 'The browser cache version is inconsistent with the server version, please try to clear the cache', clearCache: 'Clear Cache', }, mediaType: { @@ -958,6 +960,9 @@ export default { searching: 'Searching, please wait...', noData: 'No Data', noResourceFound: 'No resources found', + aiRecommend: 'AI Recommendation', + reRecommend: 'Regenerate Recommendation', + aiRecommendError: 'AI Recommendation Failed', }, browse: { actor: 'Actor', @@ -1298,6 +1303,12 @@ export default { advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases', downloaders: 'Downloaders', downloadersDesc: 'Only the default downloader will be used by default.', + aiRecommendEnabled: 'AI Search Recommendation', + aiRecommendEnabledHint: 'Enable AI search recommendation. When enabled, an AI recommendation button will be displayed on the search result page, recommending resources based on user preferences.', + aiRecommendUserPreference: 'User Preference', + aiRecommendUserPreferenceHint: 'Set user preferences for AI recommendation, e.g., 4K WEB-DL Dolby Vision', + aiRecommendMaxItems: 'AI Recommendation Analysis Limit', + aiRecommendMaxItemsHint: 'Limit the number of search results sent to the AI assistant for analysis. More items mean slower analysis and more token consumption. It is recommended to manually filter to a general range before using AI recommendation.', mediaServers: 'Media Servers', mediaServersDesc: 'All enabled media servers will be used.', trimeMedia: 'TrimeMedia', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c7393b95..942f4eae 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -69,6 +69,8 @@ export default { preset: '预设', refresh: '刷新', swUpdateReady: '新版本已就绪,请刷新页面以获取最新功能', + ascending: '升序', + descending: '降序', versionMismatch: '浏览器缓存版本与服务端版本不一致,请尝试清除缓存', clearCache: '清除缓存', }, @@ -955,6 +957,9 @@ export default { searching: '正在搜索,请稍候...', noData: '没有数据', noResourceFound: '未搜索到任何资源', + aiRecommend: '智能推荐', + reRecommend: '重新生成推荐', + aiRecommendError: '智能推荐失败', }, browse: { actor: '演员', @@ -1294,6 +1299,12 @@ export default { advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整', downloaders: '下载器', downloadersDesc: '只有默认下载器才会被默认使用。', + aiRecommendEnabled: '搜索结果智能推荐', + aiRecommendEnabledHint: '启用搜索结果智能推荐功能,开启后将在搜索结果页面显示智能推荐按钮,可根据用户偏好智能推荐资源', + aiRecommendUserPreference: '用户偏好', + aiRecommendUserPreferenceHint: '设置智能推荐时的用户偏好,例如:4K WEB-DL Dolby Vision', + aiRecommendMaxItems: '智能推荐分析条目上限', + aiRecommendMaxItemsHint: '限制发送给智能助手进行分析的搜索结果数量,数量越多分析越慢且消耗 Token 越多,建议先手动筛选,筛选出大致范围后再进行智能推荐', mediaServers: '媒体服务器', mediaServersDesc: '所有启用的媒体服务器都会被使用。', trimeMedia: '飞牛影视', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index d265131d..940cd705 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -69,7 +69,9 @@ export default { preset: '預設', refresh: '刷新', swUpdateReady: '新版本已就緒,請刷新頁面以獲取最新功能', - versionMismatch: '瀏覽器快取版本與伺服器版本不一致,請嘗試清除快取', + ascending: '升序', + descending: '降序', + versionMismatch: '瀏覽器快取版本與服務端版本不一致,請嘗試清除快取', clearCache: '清除快取', }, mediaType: { @@ -942,6 +944,9 @@ export default { searching: '正在搜索,請稍候...', noData: '沒有數據', noResourceFound: '未搜索到任何資源', + aiRecommend: '智能推薦', + reRecommend: '重新生成推薦', + aiRecommendError: '智能推薦失敗', }, browse: { actor: '演員', @@ -1282,6 +1287,12 @@ export default { advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整', downloaders: '下載器', downloadersDesc: '只有默認下載器才會被默認使用。', + aiRecommendEnabled: '搜索結果智能推薦', + aiRecommendEnabledHint: '啟用搜索結果智能推薦功能,開啟後將在搜索結果頁面顯示智能推薦按鈕,可根據用戶偏好智能推薦資源', + aiRecommendUserPreference: '用戶偏好', + aiRecommendUserPreferenceHint: '設置智能推薦時的用戶偏好,例如:4K WEB-DL Dolby Vision', + aiRecommendMaxItems: '智能推薦分析條目上限', + aiRecommendMaxItemsHint: '限制發送給智能助手進行分析的搜索結果數量,數量越多分析越慢且消耗 Token 越多,建議先手動篩選,篩選出大致範圍後再進行智能推薦', mediaServers: '媒體服務器', mediaServersDesc: '所有啟用的媒體服務器都會被使用。', trimeMedia: '飛牛影視', diff --git a/src/pages/login.vue b/src/pages/login.vue index 0f31d23e..b99f2613 100644 --- a/src/pages/login.vue +++ b/src/pages/login.vue @@ -1,6 +1,6 @@ @@ -215,9 +581,10 @@ onUnmounted(() => {
- {{ t('resource.searchResults') }} + {{ t('resource.searchResults') }} + {{ t('navItems.searchResult') }}
-
+
{{ t('resource.keyword') }}: {{ keyword }} @@ -232,10 +599,62 @@ onUnmounted(() => {
+ + + +
+
+ + + + {{ t('resource.aiRecommend') }} + + + + +
+
+ + + + {{ t('resource.reRecommend') }} + + +
+
+
+
+
+
@@ -246,39 +665,91 @@ onUnmounted(() => {
- - -
-
-
-
-
-
-
-
{{ t('resource.switchingView') }}
-
-
-
- -
- - -
- -
-
+
+ + - - -
- + + + +
+ + +