openlist增加路径元信息配置分类和自动刷新链接功能

This commit is contained in:
mtvpls
2026-07-18 15:51:30 +08:00
parent f742787044
commit d9e1b343e3
14 changed files with 586 additions and 50 deletions
+211 -1
View File
@@ -212,7 +212,7 @@ const AlertModal = ({
return createPortal(
<div
className={`fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 transition-opacity duration-200 ${
className={`fixed inset-0 bg-black bg-opacity-50 z-[10050] flex items-center justify-center p-4 transition-opacity duration-200 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
>
@@ -3476,6 +3476,9 @@ const OpenListConfigComponent = ({
'hybrid'
);
const [disableVideoPreview, setDisableVideoPreview] = useState(false);
const [pathMetaRows, setPathMetaRows] = useState<
Array<{ path: string; category: string; refresh14m: boolean }>
>([]);
const [videos, setVideos] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [scanProgress, setScanProgress] = useState<{
@@ -3485,6 +3488,7 @@ const OpenListConfigComponent = ({
} | null>(null);
const [correctDialogOpen, setCorrectDialogOpen] = useState(false);
const [selectedVideo, setSelectedVideo] = useState<any | null>(null);
const [pathMetaDialogOpen, setPathMetaDialogOpen] = useState(false);
useEffect(() => {
if (config?.OpenListConfig) {
@@ -3514,6 +3518,14 @@ const OpenListConfigComponent = ({
setDisableVideoPreview(
config.OpenListConfig.DisableVideoPreview || false
);
const pathMeta = config.OpenListConfig.PathMeta || {};
setPathMetaRows(
Object.entries(pathMeta).map(([path, meta]) => ({
path,
category: meta?.category || '',
refresh14m: Boolean(meta?.refresh14m),
}))
);
}
}, [config]);
@@ -3548,6 +3560,22 @@ const OpenListConfigComponent = ({
const handleSave = async () => {
await withLoading('saveOpenList', async () => {
try {
// 路径元信息:序列化为 map(匹配时按最长前缀)
if (pathMetaRows.some((row) => !(row.path || '').trim())) {
throw new Error('路径元信息中的路径不能为空');
}
const pathMetaPayload: Record<
string,
{ category: string; refresh14m: boolean }
> = {};
for (const row of pathMetaRows) {
const p = (row.path || '').trim();
pathMetaPayload[p] = {
category: (row.category || '').trim(),
refresh14m: Boolean(row.refresh14m),
};
}
const response = await fetch('/api/admin/openlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -3566,6 +3594,7 @@ const OpenListConfigComponent = ({
ScanInterval: scanInterval,
ScanMode: scanMode,
DisableVideoPreview: disableVideoPreview,
PathMeta: pathMetaPayload,
}),
});
@@ -4070,6 +4099,187 @@ const OpenListConfigComponent = ({
</button>
</div>
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
<div>
<h3 className='text-sm font-medium text-gray-900 dark:text-white'>
</h3>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
14
{pathMetaRows.length > 0
? ` · 已配置 ${pathMetaRows.length}`
: ''}
</p>
</div>
<button
type='button'
onClick={() => setPathMetaDialogOpen(true)}
disabled={!enabled}
className={`${buttonStyles.primary} text-sm ${
!enabled ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
</button>
</div>
{pathMetaDialogOpen &&
createPortal(
<div
className='fixed inset-0 bg-black bg-opacity-50 z-[10002] flex items-center justify-center p-4'
onClick={() => setPathMetaDialogOpen(false)}
onTouchMove={(e) => e.preventDefault()}
onWheel={(e) => e.preventDefault()}
style={{ touchAction: 'none' }}
>
<div
className='w-full max-w-3xl max-h-[85vh] flex flex-col rounded-xl bg-white dark:bg-gray-900 shadow-xl border border-gray-200 dark:border-gray-700'
onClick={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
onWheel={(e) => e.stopPropagation()}
style={{ touchAction: 'auto' }}
>
<div className='flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700'>
<h3 className='text-base font-medium text-gray-900 dark:text-white'>
</h3>
<button
type='button'
onClick={() => setPathMetaDialogOpen(false)}
className='text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-sm px-2 py-1'
>
</button>
</div>
<div className='px-5 py-3 text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800'>
/videos
</div>
<div className='flex-1 overflow-y-auto px-5 py-4 space-y-2'>
{pathMetaRows.length === 0 ? (
<p className='text-sm text-gray-400 dark:text-gray-500 text-center py-8'>
</p>
) : (
pathMetaRows.map((row, index) => (
<div
key={index}
className='grid grid-cols-1 md:grid-cols-12 gap-2 items-center'
>
<div className='md:col-span-5'>
<input
type='text'
value={row.path}
onChange={(e) => {
const value = e.target.value;
setPathMetaRows((rows) =>
rows.map((r, i) =>
i === index ? { ...r, path: value } : r
)
);
}}
placeholder='路径,如 /videos'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/>
</div>
<div className='md:col-span-3'>
<input
type='text'
value={row.category}
onChange={(e) => {
const value = e.target.value;
setPathMetaRows((rows) =>
rows.map((r, i) =>
i === index ? { ...r, category: value } : r
)
);
}}
placeholder='分类,如 动漫'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/>
</div>
<div className='md:col-span-3 flex items-center gap-2'>
<span className='text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap'>
</span>
<button
type='button'
onClick={() =>
setPathMetaRows((rows) =>
rows.map((r, i) =>
i === index
? { ...r, refresh14m: !r.refresh14m }
: r
)
)
}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
row.refresh14m
? 'bg-blue-600'
: 'bg-gray-200 dark:bg-gray-700'
}`}
aria-label='播放自动刷新'
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
row.refresh14m
? 'translate-x-6'
: 'translate-x-1'
}`}
/>
</button>
</div>
<div className='md:col-span-1 flex justify-end'>
<button
type='button'
onClick={() =>
setPathMetaRows((rows) =>
rows.filter((_, i) => i !== index)
)
}
className='px-2 py-1 text-sm text-red-600 hover:text-red-700 dark:text-red-400'
>
</button>
</div>
</div>
))
)}
</div>
<div className='flex items-center justify-between gap-3 px-5 py-4 border-t border-gray-200 dark:border-gray-700'>
<button
type='button'
onClick={() =>
setPathMetaRows((rows) => [
...rows,
{ path: '', category: '', refresh14m: false },
])
}
className={buttonStyles.primary}
>
</button>
<button
type='button'
onClick={() => {
if (pathMetaRows.some((row) => !(row.path || '').trim())) {
showError('路径不能为空', showAlert);
return;
}
setPathMetaDialogOpen(false);
}}
className={buttonStyles.success}
>
</button>
</div>
</div>
</div>,
document.body
)}
<div className='flex gap-3'>
<button
onClick={handleCheckConnectivity}