legado初步支持

This commit is contained in:
mtvpls
2026-05-19 19:25:56 +08:00
parent b949b6da91
commit 87c3da046b
22 changed files with 1433 additions and 66 deletions
+236 -16
View File
@@ -12217,6 +12217,8 @@ const OPDSConfigComponent = ({
const [cacheTTL, setCacheTTL] = useState(10 * 60 * 1000);
const [sources, setSources] = useState<BookSource[]>([]);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [legadoImportText, setLegadoImportText] = useState('');
const [legadoRuleDrafts, setLegadoRuleDrafts] = useState<Record<number, string>>({});
useEffect(() => {
if (config?.OPDSConfig) {
@@ -12226,6 +12228,7 @@ const OPDSConfigComponent = ({
(config.OPDSConfig.Sources || []).map((item, index) => ({
id: item.id || `source_${index + 1}`,
name: item.name || `书源 ${index + 1}`,
type: item.type || 'opds',
url: item.url || '',
enabled: item.enabled !== false,
authMode: item.authMode || 'none',
@@ -12236,9 +12239,11 @@ const OPDSConfigComponent = ({
searchTemplate: item.searchTemplate || '',
preferFormat: item.preferFormat || ['epub', 'pdf'],
language: item.language || '',
legado: item.legado,
}))
);
setEditingIndex(null);
setLegadoRuleDrafts({});
}
}, [config]);
@@ -12264,6 +12269,7 @@ const OPDSConfigComponent = ({
{
id: `source_${prev.length + 1}`,
name: `书源 ${prev.length + 1}`,
type: 'opds',
url: '',
enabled: true,
authMode: 'none',
@@ -12274,13 +12280,76 @@ const OPDSConfigComponent = ({
searchTemplate: '',
preferFormat: ['epub', 'pdf'],
language: '',
legado: undefined,
},
];
});
};
const makeLegadoSourceId = (name: string, url: string, index: number) => {
const raw = `${name}|${url}|${index}`;
let hash = 0;
for (let i = 0; i < raw.length; i += 1) {
hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0;
}
return `legado_${Math.abs(hash).toString(36)}`;
};
const importLegadoSources = () => {
try {
const parsed = JSON.parse(legadoImportText);
const list = Array.isArray(parsed) ? parsed : [parsed];
const imported = list
.filter((item) => item && typeof item === 'object')
.map((rule: any, index) => {
const name = rule.bookSourceName || `Legado 书源 ${index + 1}`;
const url = rule.bookSourceUrl || '';
return {
id: makeLegadoSourceId(name, url, index),
name,
type: 'legado' as const,
url,
enabled: rule.enabled !== false,
authMode: 'none' as const,
username: '',
password: '',
headerName: '',
headerValue: '',
searchTemplate: '',
preferFormat: ['epub' as const],
language: '',
legado: rule,
} satisfies BookSource;
})
.filter((source) => !!source.url);
if (imported.length === 0) {
throw new Error('没有识别到有效 Legado 书源,请确认 JSON 内含 bookSourceUrl');
}
setSources((prev) => {
const existed = new Set(prev.map((item) => `${item.type || 'opds'}|${item.url}|${item.name}`));
const next = imported.filter((item) => !existed.has(`${item.type}|${item.url}|${item.name}`));
return [...prev, ...next];
});
setLegadoImportText('');
showSuccess(`已导入 ${imported.length} 个 Legado 书源`, showAlert);
} catch (error) {
showError(error instanceof Error ? error.message : 'Legado JSON 解析失败', showAlert);
}
};
const removeSource = (index: number) => {
setSources((prev) => prev.filter((_, idx) => idx !== index));
setLegadoRuleDrafts((prev) => {
const next: Record<number, string> = {};
Object.entries(prev).forEach(([key, value]) => {
const numericKey = Number(key);
if (numericKey < index) next[numericKey] = value;
if (numericKey > index) next[numericKey - 1] = value;
});
return next;
});
setEditingIndex((prev) => {
if (prev === null) return prev;
if (prev === index) return null;
@@ -12291,6 +12360,7 @@ const OPDSConfigComponent = ({
const normalizeSource = (source: BookSource, index: number) => ({
id: source.id?.trim() || `source_${index + 1}`,
name: source.name?.trim() || `书源 ${index + 1}`,
type: source.type || 'opds',
url: source.url?.trim() || '',
enabled: source.enabled !== false,
authMode: source.authMode || 'none',
@@ -12299,13 +12369,28 @@ const OPDSConfigComponent = ({
headerName:
source.authMode === 'header' ? source.headerName?.trim() || '' : '',
headerValue: source.authMode === 'header' ? source.headerValue || '' : '',
searchTemplate: source.searchTemplate?.trim() || '',
searchTemplate: source.type === 'legado' ? '' : source.searchTemplate?.trim() || '',
preferFormat: source.preferFormat?.length
? source.preferFormat
: ['epub', 'pdf'],
language: source.language?.trim() || '',
legado: source.type === 'legado' ? source.legado : undefined,
});
const updateLegadoRuleJson = (index: number, value: string) => {
setLegadoRuleDrafts((prev) => ({ ...prev, [index]: value }));
try {
const rule = JSON.parse(value);
updateSource(index, {
legado: rule,
name: rule.bookSourceName || sources[index]?.name,
url: rule.bookSourceUrl || sources[index]?.url,
});
} catch {
// 允许用户继续编辑尚未完成的 JSON,保存前需修正为合法 JSON
}
};
const buildConfig = () => ({
Enabled: enabled,
CacheTTL: Math.max(60_000, cacheTTL || 10 * 60 * 1000),
@@ -12316,6 +12401,14 @@ const OPDSConfigComponent = ({
await withLoading('saveOPDSConfig', async () => {
try {
if (!config) throw new Error('配置未加载');
for (const [index, draft] of Object.entries(legadoRuleDrafts)) {
if (!draft.trim()) continue;
try {
JSON.parse(draft);
} catch {
throw new Error(`${Number(index) + 1} 个 Legado 书源 JSON 格式不正确`);
}
}
const response = await fetch('/api/admin/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -12385,10 +12478,10 @@ const OPDSConfigComponent = ({
<div className='space-y-6'>
<div className='bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4'>
<h3 className='text-sm font-medium text-amber-900 dark:text-amber-100 mb-2'>
/ OPDS
/ OPDS / Legado
</h3>
<div className='text-sm text-amber-800 dark:text-amber-200 space-y-1'>
<p> </p>
<p> OPDS Legado</p>
<p>
</p>
@@ -12449,6 +12542,34 @@ const OPDSConfigComponent = ({
</button>
</div>
<div className='rounded-xl border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20'>
<div className='mb-2 flex items-center justify-between gap-3'>
<div>
<h4 className='text-sm font-medium text-amber-900 dark:text-amber-100'>
Legado
</h4>
<p className='mt-1 text-xs text-amber-800 dark:text-amber-200'>
/Legado JSON
</p>
</div>
<button
type='button'
onClick={importLegadoSources}
disabled={!legadoImportText.trim()}
className={buttonStyles.primarySmall}
>
Legado
</button>
</div>
<textarea
value={legadoImportText}
onChange={(e) => setLegadoImportText(e.target.value)}
placeholder='[{ "bookSourceName": "...", "bookSourceUrl": "...", "searchUrl": "...", "ruleSearch": { ... } }]'
rows={5}
className='w-full rounded-lg border border-amber-200 bg-white px-3 py-2 font-mono text-xs text-gray-900 dark:border-amber-800 dark:bg-gray-900 dark:text-gray-100'
/>
</div>
{sources.length === 0 && (
<div className='rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-4 text-sm text-gray-500 dark:text-gray-400'>
OPDS
@@ -12499,6 +12620,14 @@ const OPDSConfigComponent = ({
</div>
<div className='space-y-2 text-xs text-gray-600 dark:text-gray-300'>
<div className='flex items-start justify-between gap-3'>
<span className='shrink-0 text-gray-500 dark:text-gray-400'>
</span>
<span className='min-w-0 text-right'>
{source.type === 'legado' ? 'Legado' : 'OPDS'}
</span>
</div>
<div className='flex items-start justify-between gap-3'>
<span className='shrink-0 text-gray-500 dark:text-gray-400'>
@@ -12524,7 +12653,11 @@ const OPDSConfigComponent = ({
</span>
<span>
{source.searchTemplate?.trim()
{source.type === 'legado'
? source.legado?.searchUrl
? '已配置'
: '未配置'
: source.searchTemplate?.trim()
? '已配置'
: '未配置'}
</span>
@@ -12585,6 +12718,27 @@ const OPDSConfigComponent = ({
</div>
<div className='grid grid-cols-1 gap-4'>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<select
value={source.type || 'opds'}
onChange={(e) => {
const nextType = e.target.value as BookSource['type'];
updateSource(index, {
type: nextType,
legado: nextType === 'legado'
? source.legado || { bookSourceName: source.name, bookSourceUrl: source.url }
: undefined,
});
}}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
>
<option value='opds'>OPDS</option>
<option value='legado'>Legado</option>
</select>
</div>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
ID
@@ -12620,14 +12774,35 @@ const OPDSConfigComponent = ({
<input
type='text'
value={source.url}
onChange={(e) =>
updateSource(index, { url: e.target.value })
}
placeholder='https://example.com/opds'
onChange={(e) => {
const url = e.target.value;
updateSource(index, {
url,
legado: source.type === 'legado' ? { ...(source.legado || {}), bookSourceUrl: url } : source.legado,
});
}}
placeholder={source.type === 'legado' ? 'https://example.com' : 'https://example.com/opds'}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
/>
</div>
{source.type === 'legado' ? (
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
Legado JSON
</label>
<textarea
value={legadoRuleDrafts[index] ?? JSON.stringify(source.legado || {}, null, 2)}
onChange={(e) => updateLegadoRuleJson(index, e.target.value)}
rows={10}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
JSON/
</p>
</div>
) : (
<>
<div className='grid grid-cols-1 gap-4'>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
@@ -12751,6 +12926,8 @@ const OPDSConfigComponent = ({
</div>
</div>
)}
</>
)}
</div>
)}
</div>
@@ -12823,7 +13000,7 @@ const OPDSConfigComponent = ({
{source.name || `书源 ${index + 1}`}
</div>
<div className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
{source.language || '未设置语言'}
{source.type === 'legado' ? 'Legado' : 'OPDS'} · {source.language || '未设置语言'}
</div>
</td>
<td className='px-4 py-3 text-sm text-gray-600 dark:text-gray-300'>
@@ -12845,7 +13022,11 @@ const OPDSConfigComponent = ({
: '自定义 Header'}
</td>
<td className='px-4 py-3 text-sm text-gray-600 dark:text-gray-300'>
{source.searchTemplate?.trim()
{source.type === 'legado'
? source.legado?.searchUrl
? '已配置'
: '未配置'
: source.searchTemplate?.trim()
? '已配置'
: '未配置'}
</td>
@@ -12933,6 +13114,27 @@ const OPDSConfigComponent = ({
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<select
value={source.type || 'opds'}
onChange={(e) => {
const nextType = e.target.value as BookSource['type'];
updateSource(index, {
type: nextType,
legado: nextType === 'legado'
? source.legado || { bookSourceName: source.name, bookSourceUrl: source.url }
: undefined,
});
}}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
>
<option value='opds'>OPDS</option>
<option value='legado'>Legado</option>
</select>
</div>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
ID
@@ -12972,16 +13174,32 @@ const OPDSConfigComponent = ({
<input
type='text'
value={source.url}
onChange={(e) =>
onChange={(e) => {
const url = e.target.value;
updateSource(index, {
url: e.target.value,
})
}
placeholder='https://example.com/opds'
url,
legado: source.type === 'legado' ? { ...(source.legado || {}), bookSourceUrl: url } : source.legado,
});
}}
placeholder={source.type === 'legado' ? 'https://example.com' : 'https://example.com/opds'}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
/>
</div>
{source.type === 'legado' ? (
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
Legado JSON
</label>
<textarea
value={legadoRuleDrafts[index] ?? JSON.stringify(source.legado || {}, null, 2)}
onChange={(e) => updateLegadoRuleJson(index, e.target.value)}
rows={12}
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100'
/>
</div>
) : (
<>
<div className='grid grid-cols-1 gap-4 md:grid-cols-3'>
<div>
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
@@ -13109,6 +13327,8 @@ const OPDSConfigComponent = ({
</div>
</div>
)}
</>
)}
</div>
</td>
</tr>
@@ -13130,7 +13350,7 @@ const OPDSConfigComponent = ({
disabled={isLoading('saveOPDSConfig')}
className={buttonStyles.success}
>
{isLoading('saveOPDSConfig') ? '保存中...' : '保存 OPDS 配置'}
{isLoading('saveOPDSConfig') ? '保存中...' : '保存电子书源配置'}
</button>
</div>