Skip to content

Commit 1ef19c3

Browse files
committed
feat: 调度任务表单支持热门视频清理年份选择
- 新增热门视频清理年份查询接口(GET /bilibili/popular/years) - 选择 /bilibili/popular/cleanup 时展示「清理年份」下拉,支持不传 year 清理全部 - 支持从端点 query 读取 year,并规范化写入 params.year
1 parent ed14c8c commit 1ef19c3

2 files changed

Lines changed: 236 additions & 1 deletion

File tree

src/api/api.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,19 @@ export const batchDeleteBilibiliHistory = (items) => {
591591
})
592592
}
593593

594+
// =============================
595+
// 热门视频数据清理(按年分库)
596+
// =============================
597+
598+
/**
599+
* 获取热门视频数据库可用年份(降序)
600+
* GET /bilibili/popular/years
601+
* @returns {Promise<any>}
602+
*/
603+
export const getPopularCleanupYears = () => {
604+
return instance.get('/bilibili/popular/years')
605+
}
606+
594607
// 数据库管理相关接口
595608
// 重置数据库
596609
export const resetDatabase = () => {

src/components/tailwind/scheduler/TaskForm.vue

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,45 @@
9393
</div>
9494
</div>
9595

96+
<!-- 热门视频清理设置(仅清理接口显示) -->
97+
<div v-if="isPopularCleanupEndpoint" class="bg-white dark:bg-gray-800 rounded-lg p-1.5 border border-gray-200 dark:border-gray-700">
98+
<h4 class="text-xs font-semibold text-gray-600 uppercase tracking-wider mb-1 flex items-center">
99+
<svg class="w-3 h-3 mr-1 text-[#fb7299]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
100+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" />
101+
</svg>
102+
热门视频清理
103+
</h4>
104+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
105+
<div class="md:col-span-2">
106+
<label for="popularCleanupYear" class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-0.5">清理年份(可选)</label>
107+
<select
108+
id="popularCleanupYear"
109+
v-model="popularCleanupYear"
110+
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-[#fb7299] focus:ring-[#fb7299] text-xs py-1 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed dark:bg-gray-700 dark:text-gray-100 disabled:dark:bg-gray-800 disabled:dark:text-gray-500"
111+
:disabled="popularCleanupYearsLoading"
112+
>
113+
<option :value="null">全部年份(不传 year)</option>
114+
<option
115+
v-for="year in popularCleanupYearOptions"
116+
:key="year"
117+
:value="year"
118+
>
119+
{{ year }}
120+
</option>
121+
</select>
122+
<div v-if="popularCleanupYearsLoading" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
123+
正在获取可用年份...
124+
</div>
125+
<div v-else-if="popularCleanupYearsMessage" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
126+
{{ popularCleanupYearsMessage }}
127+
</div>
128+
<div v-else-if="popularCleanupDefaultYear" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
129+
建议默认年份:{{ popularCleanupDefaultYear }}
130+
</div>
131+
</div>
132+
</div>
133+
</div>
134+
96135
<!-- 调度设置 -->
97136
<div class="bg-white dark:bg-gray-800 rounded-lg p-1.5 border border-gray-200 dark:border-gray-700">
98137
<h4 class="text-xs font-semibold text-gray-600 uppercase tracking-wider mb-1 flex items-center">
@@ -268,7 +307,8 @@ import {
268307
updateSchedulerTask,
269308
getSchedulerTaskDetail,
270309
getAvailableEndpoints,
271-
addSubTask
310+
addSubTask,
311+
getPopularCleanupYears
272312
} from '../../../api/api'
273313
274314
const props = defineProps({
@@ -320,6 +360,34 @@ const form = reactive({
320360
const paramsJson = ref('{}')
321361
const paramsError = ref('')
322362
363+
const POPULAR_CLEANUP_ENDPOINT_PATH = '/bilibili/popular/cleanup'
364+
365+
const parseEndpointUrlParts = (endpoint) => {
366+
const raw = (endpoint || '').trim()
367+
if (!raw) return { path: '', searchParams: new URLSearchParams() }
368+
369+
try {
370+
const url = new URL(raw, 'http://placeholder')
371+
return { path: url.pathname || '', searchParams: url.searchParams || new URLSearchParams() }
372+
} catch {
373+
const [pathPart, queryPart] = raw.split('?')
374+
return { path: (pathPart || raw).trim(), searchParams: new URLSearchParams(queryPart || '') }
375+
}
376+
}
377+
378+
const normalizeEndpointPath = (path) => {
379+
let normalized = (path || '').trim()
380+
if (!normalized) return ''
381+
if (!normalized.startsWith('/')) normalized = `/${normalized}`
382+
normalized = normalized.replace(/\/+$/, '')
383+
return normalized || '/'
384+
}
385+
386+
const isPopularCleanupEndpointPath = (path) => {
387+
const normalized = normalizeEndpointPath(path)
388+
return normalized === POPULAR_CLEANUP_ENDPOINT_PATH || normalized.endsWith(POPULAR_CLEANUP_ENDPOINT_PATH)
389+
}
390+
323391
// 可用的API端点
324392
const availableEndpoints = ref([])
325393
@@ -331,6 +399,122 @@ const expandedTags = reactive({})
331399
const methodFilter = ref('ALL')
332400
const selectedEndpoint = ref('')
333401
402+
const popularCleanupYears = ref([])
403+
const popularCleanupDefaultYear = ref(null)
404+
const popularCleanupYearsMessage = ref('')
405+
const popularCleanupYearsLoading = ref(false)
406+
const popularCleanupYearTouched = ref(false)
407+
const cachedPopularCleanupYear = ref(undefined)
408+
409+
const popularCleanupEndpointParts = computed(() => parseEndpointUrlParts(form.endpoint))
410+
411+
const isPopularCleanupEndpoint = computed(() => {
412+
return isPopularCleanupEndpointPath(popularCleanupEndpointParts.value.path)
413+
})
414+
415+
const popularCleanupYearFromEndpointQuery = computed(() => {
416+
const year = popularCleanupEndpointParts.value.searchParams.get('year')
417+
if (!year) return null
418+
const normalized = Number.parseInt(year, 10)
419+
return Number.isFinite(normalized) ? normalized : null
420+
})
421+
422+
const popularCleanupYear = computed({
423+
get: () => (Object.prototype.hasOwnProperty.call(form.params, 'year') ? form.params.year : null),
424+
set: (year) => {
425+
popularCleanupYearTouched.value = true
426+
if (year === null || year === undefined || year === '') {
427+
if (Object.prototype.hasOwnProperty.call(form.params, 'year')) {
428+
delete form.params.year
429+
}
430+
return
431+
}
432+
form.params.year = year
433+
}
434+
})
435+
436+
const popularCleanupYearOptions = computed(() => {
437+
const years = Array.isArray(popularCleanupYears.value) ? [...popularCleanupYears.value] : []
438+
const currentYear = form.params?.year
439+
const normalizedCurrentYear = typeof currentYear === 'string' ? Number.parseInt(currentYear, 10) : currentYear
440+
441+
if (Number.isFinite(normalizedCurrentYear) && !years.includes(normalizedCurrentYear)) {
442+
years.push(normalizedCurrentYear)
443+
}
444+
445+
return years
446+
.filter((y) => Number.isFinite(y))
447+
.sort((a, b) => b - a)
448+
})
449+
450+
const applyPopularCleanupYearFromEndpointQuery = () => {
451+
if (!isPopularCleanupEndpoint.value) return
452+
const yearFromQuery = popularCleanupYearFromEndpointQuery.value
453+
if (yearFromQuery === null || yearFromQuery === undefined) return
454+
if (!Object.prototype.hasOwnProperty.call(form.params, 'year')) {
455+
form.params.year = yearFromQuery
456+
}
457+
}
458+
459+
const normalizePopularCleanupYearParam = () => {
460+
if (!form.params || typeof form.params !== 'object') return
461+
if (!Object.prototype.hasOwnProperty.call(form.params, 'year')) return
462+
463+
const year = form.params.year
464+
if (year === null || year === undefined || year === '') {
465+
delete form.params.year
466+
return
467+
}
468+
469+
if (typeof year === 'string') {
470+
const normalized = Number.parseInt(year, 10)
471+
if (Number.isFinite(normalized)) {
472+
form.params.year = normalized
473+
} else {
474+
delete form.params.year
475+
}
476+
}
477+
}
478+
479+
const fetchPopularCleanupYears = async () => {
480+
if (popularCleanupYearsLoading.value) return
481+
482+
popularCleanupYearsLoading.value = true
483+
popularCleanupYearsMessage.value = ''
484+
popularCleanupDefaultYear.value = null
485+
486+
try {
487+
const response = await getPopularCleanupYears()
488+
if (response.data && response.data.status === 'success') {
489+
popularCleanupYears.value = Array.isArray(response.data.data) ? response.data.data : []
490+
popularCleanupDefaultYear.value = response.data.default_year ?? null
491+
popularCleanupYearsMessage.value = response.data.message || ''
492+
493+
// 新建任务:当用户尚未手动选择且未设置 year 时,自动使用后端建议默认年份
494+
if (
495+
isPopularCleanupEndpoint.value &&
496+
!props.isEditing &&
497+
!popularCleanupYearTouched.value &&
498+
!Object.prototype.hasOwnProperty.call(form.params, 'year') &&
499+
popularCleanupDefaultYear.value !== null &&
500+
popularCleanupDefaultYear.value !== undefined
501+
) {
502+
form.params.year = popularCleanupDefaultYear.value
503+
}
504+
} else {
505+
popularCleanupYears.value = []
506+
popularCleanupYearsMessage.value = response.data?.message || '获取年份列表失败'
507+
popularCleanupDefaultYear.value = null
508+
}
509+
} catch (error) {
510+
popularCleanupYears.value = []
511+
popularCleanupYearsMessage.value = '获取年份列表出错: ' + (error.message || '未知错误')
512+
popularCleanupDefaultYear.value = null
513+
} finally {
514+
popularCleanupYearsLoading.value = false
515+
}
516+
}
517+
334518
// 按tag对API进行分组
335519
const groupedEndpoints = computed(() => {
336520
const groups = {}
@@ -478,6 +662,8 @@ const resetForm = () => {
478662
selectedEndpoint.value = ''
479663
paramsJson.value = '{}'
480664
paramsError.value = ''
665+
popularCleanupYearTouched.value = false
666+
cachedPopularCleanupYear.value = undefined
481667
showApiSelector.value = false
482668
showDependencySelector.value = false
483669
apiSearchQuery.value = ''
@@ -592,6 +778,11 @@ const loadTaskDetail = async (taskId) => {
592778
form.endpoint = taskInfo.config.endpoint
593779
form.method = taskInfo.config.method
594780
form.params = taskInfo.config.params || {}
781+
if (isPopularCleanupEndpointPath(taskInfo.config.endpoint)) {
782+
popularCleanupYearTouched.value = true
783+
applyPopularCleanupYearFromEndpointQuery()
784+
normalizePopularCleanupYearParam()
785+
}
595786
form.schedule_type = taskInfo.config.schedule_type
596787
form.schedule_time = taskInfo.config.schedule_time
597788
form.enabled = taskInfo.config.enabled
@@ -630,6 +821,37 @@ const loadTaskDetail = async (taskId) => {
630821
}
631822
}
632823
824+
watch(
825+
() => [props.show, isPopularCleanupEndpoint.value],
826+
async ([show, isCleanup]) => {
827+
if (!show || !isCleanup) return
828+
applyPopularCleanupYearFromEndpointQuery()
829+
normalizePopularCleanupYearParam()
830+
await fetchPopularCleanupYears()
831+
}
832+
)
833+
834+
watch(isPopularCleanupEndpoint, (isCleanup, wasCleanup) => {
835+
if (wasCleanup && !isCleanup) {
836+
cachedPopularCleanupYear.value = popularCleanupYear.value
837+
if (Object.prototype.hasOwnProperty.call(form.params, 'year')) {
838+
delete form.params.year
839+
}
840+
return
841+
}
842+
843+
if (!wasCleanup && isCleanup) {
844+
normalizePopularCleanupYearParam()
845+
if (
846+
cachedPopularCleanupYear.value !== undefined &&
847+
!Object.prototype.hasOwnProperty.call(form.params, 'year') &&
848+
cachedPopularCleanupYear.value !== null
849+
) {
850+
form.params.year = cachedPopularCleanupYear.value
851+
}
852+
}
853+
})
854+
633855
// 获取可用的API端点
634856
const fetchAvailableEndpoints = async () => {
635857
try {

0 commit comments

Comments
 (0)