@@ -1028,11 +1156,12 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
{(() => {
// 检查是否有权限操作任何用户
- const hasAnyPermission = config?.UserConfig?.Users?.some(user =>
- (role === 'owner' ||
- (role === 'admin' &&
- (user.role === 'user' ||
- user.username === currentUsername)))
+ const hasAnyPermission = config?.UserConfig?.Users?.some(
+ (user) =>
+ role === 'owner' ||
+ (role === 'admin' &&
+ (user.role === 'user' ||
+ user.username === currentUsername))
);
return hasAnyPermission ? (
@@ -1126,14 +1255,19 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
>
| |
- {(role === 'owner' ||
- (role === 'admin' &&
- (user.role === 'user' ||
- user.username === currentUsername))) ? (
+ {role === 'owner' ||
+ (role === 'admin' &&
+ (user.role === 'user' ||
+ user.username === currentUsername)) ? (
handleSelectUser(user.username, e.target.checked)}
+ onChange={(e) =>
+ handleSelectUser(
+ user.username,
+ e.target.checked
+ )
+ }
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
) : (
@@ -1145,26 +1279,28 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
{user.role === 'owner'
? '站长'
: user.role === 'admin'
- ? '管理员'
- : '普通用户'}
+ ? '管理员'
+ : '普通用户'}
|
{!user.banned ? '正常' : '已封禁'}
@@ -1181,13 +1317,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername))) && (
-
- )}
+
+ )}
|
@@ -1202,13 +1338,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername))) && (
-
- )}
+
+ )}
|
@@ -1229,8 +1365,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{user.role === 'user' && (
@@ -1240,8 +1382,16 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick={() =>
handleRemoveAdmin(user.username)
}
- disabled={isLoading(`removeAdmin_${user.username}`)}
- className={`${buttonStyles.roundedSecondary} ${isLoading(`removeAdmin_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
+ disabled={isLoading(
+ `removeAdmin_${user.username}`
+ )}
+ className={`${
+ buttonStyles.roundedSecondary
+ } ${
+ isLoading(`removeAdmin_${user.username}`)
+ ? 'opacity-50 cursor-not-allowed'
+ : ''
+ }`}
>
取消管理
@@ -1250,8 +1400,14 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
(!user.banned ? (
@@ -1260,8 +1416,16 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
onClick={() =>
handleUnbanUser(user.username)
}
- disabled={isLoading(`unbanUser_${user.username}`)}
- className={`${buttonStyles.roundedSuccess} ${isLoading(`unbanUser_${user.username}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
+ disabled={isLoading(
+ `unbanUser_${user.username}`
+ )}
+ className={`${
+ buttonStyles.roundedSuccess
+ } ${
+ isLoading(`unbanUser_${user.username}`)
+ ? 'opacity-50 cursor-not-allowed'
+ : ''
+ }`}
>
解封
@@ -1289,199 +1453,97 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{/* 配置用户采集源权限弹窗 */}
- {showConfigureApisModal && selectedUser && createPortal(
- {
- setShowConfigureApisModal(false);
- setSelectedUser(null);
- setSelectedApis([]);
- }}>
- e.stopPropagation()}>
-
-
-
- 配置用户采集源权限 - {selectedUser.username}
-
-
-
-
-
-
-
-
- 提示:全不选为无限制,选中的采集源将限制用户只能访问这些源
-
-
-
-
- {/* 采集源选择 - 多列布局 */}
-
-
- 选择可用的采集源:
-
-
- {config?.SourceConfig?.map((source) => (
-
- ))}
-
-
-
- {/* 快速操作按钮 */}
-
-
-
+ {showConfigureApisModal &&
+ selectedUser &&
+ createPortal(
+ {
+ setShowConfigureApisModal(false);
+ setSelectedUser(null);
+ setSelectedApis([]);
+ }}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ 配置用户采集源权限 - {selectedUser.username}
+
-
- 已选择:
- {selectedApis.length > 0 ? `${selectedApis.length} 个源` : '无限制'}
-
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
- ,
- document.body
- )}
-
- {/* 添加用户组弹窗 */}
- {showAddUserGroupForm && createPortal(
- {
- setShowAddUserGroupForm(false);
- setNewUserGroup({ name: '', enabledApis: [] });
- }}>
- e.stopPropagation()}>
-
-
-
- 添加新用户组
-
-
-
-
-
- {/* 用户组名称 */}
-
-
-
- setNewUserGroup((prev) => ({ ...prev, name: e.target.value }))
- }
- 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 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
- />
+
+
+
+
+ 提示:全不选为无限制,选中的采集源将限制用户只能访问这些源
+
+
- {/* 可用视频源 */}
-
-
-
+ {/* 采集源选择 - 多列布局 */}
+
+
+ 选择可用的采集源:
+
+
{config?.SourceConfig?.map((source) => (
-
+
- {/* 快速操作按钮 */}
-
+ {/* 快速操作按钮 */}
+
+
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
-
- ,
- document.body
- )}
-
- {/* 编辑用户组弹窗 */}
- {showEditUserGroupForm && editingUserGroup && createPortal(
- {
- setShowEditUserGroupForm(false);
- setEditingUserGroup(null);
- }}>
- e.stopPropagation()}>
-
-
-
- 编辑用户组 - {editingUserGroup.name}
-
-
-
-
-
- {/* 可用视频源 */}
-
-
- 可用视频源
-
-
- {config?.SourceConfig?.map((source) => (
-
- {
- if (e.target.checked) {
- setEditingUserGroup(prev => prev ? {
- ...prev,
- enabledApis: [...prev.enabledApis, source.key]
- } : null);
- } else {
- setEditingUserGroup(prev => prev ? {
- ...prev,
- enabledApis: prev.enabledApis.filter(api => api !== source.key)
- } : null);
- }
- }}
- className='rounded border-gray-300 text-purple-600 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700'
- />
-
-
- {source.name}
-
- {source.api && (
-
- {extractDomain(source.api)}
-
- )}
-
-
- ))}
-
-
- {/* 快速操作按钮 */}
-
-
-
-
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
-
- ,
- document.body
- )}
-
- {/* 配置用户组弹窗 */}
- {showConfigureUserGroupModal && selectedUserForGroup && createPortal(
- {
- setShowConfigureUserGroupModal(false);
- setSelectedUserForGroup(null);
- setSelectedUserGroups([]);
- }}>
- e.stopPropagation()}>
-
-
-
- 配置用户组 - {selectedUserForGroup.username}
-
-
-
-
-
-
-
-
-
- 配置说明
+
+ 已选择:
+
+ {selectedApis.length > 0
+ ? `${selectedApis.length} 个源`
+ : '无限制'}
-
- 提示:选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源
-
-
-
-
- {/* 用户组选择 - 下拉选择器 */}
-
-
- 选择用户组:
-
-
-
- 选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源
-
-
-
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
- ,
- document.body
- )}
-
- {/* 删除用户组确认弹窗 */}
- {showDeleteUserGroupModal && deletingUserGroup && createPortal(
- {
- setShowDeleteUserGroupModal(false);
- setDeletingUserGroup(null);
- }}>
- e.stopPropagation()}>
-
-
-
- 确认删除用户组
-
-
-
-
-
-
-
-
- 删除用户组 {deletingUserGroup.name} 将影响所有使用该组的用户,此操作不可恢复!
-
-
-
- {deletingUserGroup.affectedUsers.length > 0 ? (
-
-
-
-
- ⚠️ 将影响 {deletingUserGroup.affectedUsers.length} 个用户:
-
-
-
- {deletingUserGroup.affectedUsers.map((user, index) => (
-
- • {user.username} ({user.role})
-
- ))}
-
-
- 这些用户的用户组将被自动移除
-
-
- ) : (
-
-
-
-
- ✅ 当前没有用户使用此用户组
-
-
-
- )}
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
- ,
- document.body
- )}
-
- {/* 删除用户确认弹窗 */}
- {showDeleteUserModal && deletingUser && createPortal(
- {
- setShowDeleteUserModal(false);
- setDeletingUser(null);
- }}>
- e.stopPropagation()}>
-
-
-
- 确认删除用户
-
-
-
-
-
-
-
-
- 删除用户 {deletingUser} 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复!
-
{/* 操作按钮 */}
-
- ,
- document.body
- )}
+ ,
+ document.body
+ )}
- {/* 批量设置用户组弹窗 */}
- {showBatchUserGroupModal && createPortal(
- {
- setShowBatchUserGroupModal(false);
- setSelectedUserGroup('');
- }}>
- e.stopPropagation()}>
-
-
-
- 批量设置用户组
-
-
-
-
-
-
-
- ,
- document.body
- )}
+ ,
+ document.body
+ )}
+
+ {/* 删除用户组确认弹窗 */}
+ {showDeleteUserGroupModal &&
+ deletingUserGroup &&
+ createPortal(
+ {
+ setShowDeleteUserGroupModal(false);
+ setDeletingUserGroup(null);
+ }}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ 确认删除用户组
+
+
+
+
+
+
+
+
+ 删除用户组 {deletingUserGroup.name}{' '}
+ 将影响所有使用该组的用户,此操作不可恢复!
+
+
+
+ {deletingUserGroup.affectedUsers.length > 0 ? (
+
+
+
+
+
+
+ ⚠️ 将影响 {deletingUserGroup.affectedUsers.length}{' '}
+ 个用户:
+
+
+
+ {deletingUserGroup.affectedUsers.map((user, index) => (
+
+ • {user.username} ({user.role})
+
+ ))}
+
+
+ 这些用户的用户组将被自动移除
+
+
+ ) : (
+
+
+
+
+
+
+ ✅ 当前没有用户使用此用户组
+
+
+
+ )}
+
+
+ {/* 操作按钮 */}
+
+
+
+
+
+
+ ,
+ document.body
+ )}
+
+ {/* 删除用户确认弹窗 */}
+ {showDeleteUserModal &&
+ deletingUser &&
+ createPortal(
+ {
+ setShowDeleteUserModal(false);
+ setDeletingUser(null);
+ }}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ 确认删除用户
+
+
+
+
+
+
+
+
+ 删除用户 {deletingUser}{' '}
+ 将同时删除其搜索历史、播放记录和收藏夹,此操作不可恢复!
+
+
+
+ {/* 操作按钮 */}
+
+
+
+
+
+
+
+ ,
+ document.body
+ )}
+
+ {/* 批量设置用户组弹窗 */}
+ {showBatchUserGroupModal &&
+ createPortal(
+ {
+ setShowBatchUserGroupModal(false);
+ setSelectedUserGroup('');
+ }}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ 批量设置用户组
+
+
+
+
+
+
+
+
+ 将为选中的 {selectedUsers.size} 个用户{' '}
+ 设置用户组,选择"无用户组"为无限制
+
+
+
+
+
+ 选择用户组:
+
+
+
+ 选择"无用户组"为无限制,选择特定用户组将限制用户只能访问该用户组允许的采集源
+
+
+
+
+ {/* 操作按钮 */}
+
+
+
+
+
+
+ ,
+ document.body
+ )}
{/* 通用弹窗组件 */}
{
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
-
-
);
-}
+};
// 视频源配置组件
const VideoSourceConfig = ({
@@ -2045,7 +2526,9 @@ const VideoSourceConfig = ({
});
// 批量操作相关状态
- const [selectedSources, setSelectedSources] = useState >(new Set());
+ const [selectedSources, setSelectedSources] = useState>(
+ new Set()
+ );
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
const selectAll = useMemo(() => {
@@ -2063,21 +2546,23 @@ const VideoSourceConfig = ({
isOpen: false,
title: '',
message: '',
- onConfirm: () => { },
- onCancel: () => { }
+ onConfirm: () => {},
+ onCancel: () => {},
});
// 有效性检测相关状态
const [showValidationModal, setShowValidationModal] = useState(false);
const [searchKeyword, setSearchKeyword] = useState('');
const [isValidating, setIsValidating] = useState(false);
- const [validationResults, setValidationResults] = useState>([]);
+ const [validationResults, setValidationResults] = useState<
+ Array<{
+ key: string;
+ name: string;
+ status: 'valid' | 'no_results' | 'invalid' | 'validating';
+ message: string;
+ resultCount: number;
+ }>
+ >([]);
// dnd-kit 传感器
const sensors = useSensors(
@@ -2131,13 +2616,17 @@ const VideoSourceConfig = ({
const target = sources.find((s) => s.key === key);
if (!target) return;
const action = target.disabled ? 'enable' : 'disable';
- withLoading(`toggleSource_${key}`, () => callSourceApi({ action, key })).catch(() => {
+ withLoading(`toggleSource_${key}`, () =>
+ callSourceApi({ action, key })
+ ).catch(() => {
console.error('操作失败', action, key);
});
};
const handleDelete = (key: string) => {
- withLoading(`deleteSource_${key}`, () => callSourceApi({ action: 'delete', key })).catch(() => {
+ withLoading(`deleteSource_${key}`, () =>
+ callSourceApi({ action: 'delete', key })
+ ).catch(() => {
console.error('操作失败', 'delete', key);
});
};
@@ -2177,7 +2666,9 @@ const VideoSourceConfig = ({
const handleSaveOrder = () => {
const order = sources.map((s) => s.key);
- withLoading('saveSourceOrder', () => callSourceApi({ action: 'sort', order }))
+ withLoading('saveSourceOrder', () =>
+ callSourceApi({ action: 'sort', order })
+ )
.then(() => {
setOrderChanged(false);
})
@@ -2189,7 +2680,11 @@ const VideoSourceConfig = ({
// 有效性检测函数
const handleValidateSources = async () => {
if (!searchKeyword.trim()) {
- showAlert({ type: 'warning', title: '请输入搜索关键词', message: '搜索关键词不能为空' });
+ showAlert({
+ type: 'warning',
+ title: '请输入搜索关键词',
+ message: '搜索关键词不能为空',
+ });
return;
}
@@ -2199,18 +2694,22 @@ const VideoSourceConfig = ({
setShowValidationModal(false); // 立即关闭弹窗
// 初始化所有视频源为检测中状态
- const initialResults = sources.map(source => ({
+ const initialResults = sources.map((source) => ({
key: source.key,
name: source.name,
status: 'validating' as const,
message: '检测中...',
- resultCount: 0
+ resultCount: 0,
}));
setValidationResults(initialResults);
try {
// 使用EventSource接收流式数据
- const eventSource = new EventSource(`/api/admin/source/validate?q=${encodeURIComponent(searchKeyword.trim())}`);
+ const eventSource = new EventSource(
+ `/api/admin/source/validate?q=${encodeURIComponent(
+ searchKeyword.trim()
+ )}`
+ );
eventSource.onmessage = (event) => {
try {
@@ -2224,32 +2723,53 @@ const VideoSourceConfig = ({
case 'source_result':
case 'source_error':
// 更新验证结果
- setValidationResults(prev => {
- const existing = prev.find(r => r.key === data.source);
+ setValidationResults((prev) => {
+ const existing = prev.find((r) => r.key === data.source);
if (existing) {
- return prev.map(r => r.key === data.source ? {
- key: data.source,
- name: sources.find(s => s.key === data.source)?.name || data.source,
- status: data.status,
- message: data.status === 'valid' ? '搜索正常' :
- data.status === 'no_results' ? '无法搜索到结果' : '连接失败',
- resultCount: data.status === 'valid' ? 1 : 0
- } : r);
+ return prev.map((r) =>
+ r.key === data.source
+ ? {
+ key: data.source,
+ name:
+ sources.find((s) => s.key === data.source)
+ ?.name || data.source,
+ status: data.status,
+ message:
+ data.status === 'valid'
+ ? '搜索正常'
+ : data.status === 'no_results'
+ ? '无法搜索到结果'
+ : '连接失败',
+ resultCount: data.status === 'valid' ? 1 : 0,
+ }
+ : r
+ );
} else {
- return [...prev, {
- key: data.source,
- name: sources.find(s => s.key === data.source)?.name || data.source,
- status: data.status,
- message: data.status === 'valid' ? '搜索正常' :
- data.status === 'no_results' ? '无法搜索到结果' : '连接失败',
- resultCount: data.status === 'valid' ? 1 : 0
- }];
+ return [
+ ...prev,
+ {
+ key: data.source,
+ name:
+ sources.find((s) => s.key === data.source)?.name ||
+ data.source,
+ status: data.status,
+ message:
+ data.status === 'valid'
+ ? '搜索正常'
+ : data.status === 'no_results'
+ ? '无法搜索到结果'
+ : '连接失败',
+ resultCount: data.status === 'valid' ? 1 : 0,
+ },
+ ];
}
});
break;
case 'complete':
- console.log(`检测完成,共检测 ${data.completedSources} 个视频源`);
+ console.log(
+ `检测完成,共检测 ${data.completedSources} 个视频源`
+ );
eventSource.close();
setIsValidating(false);
break;
@@ -2263,7 +2783,11 @@ const VideoSourceConfig = ({
console.error('EventSource错误:', error);
eventSource.close();
setIsValidating(false);
- showAlert({ type: 'error', title: '验证失败', message: '连接错误,请重试' });
+ showAlert({
+ type: 'error',
+ title: '验证失败',
+ message: '连接错误,请重试',
+ });
};
// 设置超时,防止长时间等待
@@ -2271,13 +2795,20 @@ const VideoSourceConfig = ({
if (eventSource.readyState === EventSource.OPEN) {
eventSource.close();
setIsValidating(false);
- showAlert({ type: 'warning', title: '验证超时', message: '检测超时,请重试' });
+ showAlert({
+ type: 'warning',
+ title: '验证超时',
+ message: '检测超时,请重试',
+ });
}
}, 60000); // 60秒超时
-
} catch (error) {
setIsValidating(false);
- showAlert({ type: 'error', title: '验证失败', message: error instanceof Error ? error.message : '未知错误' });
+ showAlert({
+ type: 'error',
+ title: '验证失败',
+ message: error instanceof Error ? error.message : '未知错误',
+ });
throw error;
}
});
@@ -2285,37 +2816,41 @@ const VideoSourceConfig = ({
// 获取有效性状态显示
const getValidationStatus = (sourceKey: string) => {
- const result = validationResults.find(r => r.key === sourceKey);
+ const result = validationResults.find((r) => r.key === sourceKey);
if (!result) return null;
switch (result.status) {
case 'validating':
return {
text: '检测中',
- className: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300',
+ className:
+ 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300',
icon: '⟳',
- message: result.message
+ message: result.message,
};
case 'valid':
return {
text: '有效',
- className: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300',
+ className:
+ 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300',
icon: '✓',
- message: result.message
+ message: result.message,
};
case 'no_results':
return {
text: '无法搜索',
- className: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300',
+ className:
+ 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300',
icon: '⚠',
- message: result.message
+ message: result.message,
};
case 'invalid':
return {
text: '无效',
- className: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300',
+ className:
+ 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300',
icon: '✗',
- message: result.message
+ message: result.message,
};
default:
return null;
@@ -2374,10 +2909,11 @@ const VideoSourceConfig = ({
|
{!source.disabled ? '启用中' : '已禁用'}
@@ -2393,7 +2929,10 @@ const VideoSourceConfig = ({
);
}
return (
-
+
{status.icon} {status.text}
);
@@ -2403,10 +2942,15 @@ const VideoSourceConfig = ({
@@ -2414,7 +2958,11 @@ const VideoSourceConfig = ({
@@ -2425,18 +2973,21 @@ const VideoSourceConfig = ({
};
// 全选/取消全选
- const handleSelectAll = useCallback((checked: boolean) => {
- if (checked) {
- const allKeys = sources.map(s => s.key);
- setSelectedSources(new Set(allKeys));
- } else {
- setSelectedSources(new Set());
- }
- }, [sources]);
+ const handleSelectAll = useCallback(
+ (checked: boolean) => {
+ if (checked) {
+ const allKeys = sources.map((s) => s.key);
+ setSelectedSources(new Set(allKeys));
+ } else {
+ setSelectedSources(new Set());
+ }
+ },
+ [sources]
+ );
// 单个选择
const handleSelectSource = useCallback((key: string, checked: boolean) => {
- setSelectedSources(prev => {
+ setSelectedSources((prev) => {
const newSelected = new Set(prev);
if (checked) {
newSelected.add(key);
@@ -2448,9 +2999,15 @@ const VideoSourceConfig = ({
}, []);
// 批量操作
- const handleBatchOperation = async (action: 'batch_enable' | 'batch_disable' | 'batch_delete') => {
+ const handleBatchOperation = async (
+ action: 'batch_enable' | 'batch_disable' | 'batch_delete'
+ ) => {
if (selectedSources.size === 0) {
- showAlert({ type: 'warning', title: '请先选择要操作的视频源', message: '请选择至少一个视频源' });
+ showAlert({
+ type: 'warning',
+ title: '请先选择要操作的视频源',
+ message: '请选择至少一个视频源',
+ });
return;
}
@@ -2480,18 +3037,41 @@ const VideoSourceConfig = ({
message: confirmMessage,
onConfirm: async () => {
try {
- await withLoading(`batchSource_${action}`, () => callSourceApi({ action, keys }));
- showAlert({ type: 'success', title: `${actionName}成功`, message: `${actionName}了 ${keys.length} 个视频源`, timer: 2000 });
+ await withLoading(`batchSource_${action}`, () =>
+ callSourceApi({ action, keys })
+ );
+ showAlert({
+ type: 'success',
+ title: `${actionName}成功`,
+ message: `${actionName}了 ${keys.length} 个视频源`,
+ timer: 2000,
+ });
// 重置选择状态
setSelectedSources(new Set());
} catch (err) {
- showAlert({ type: 'error', title: `${actionName}失败`, message: err instanceof Error ? err.message : '操作失败' });
+ showAlert({
+ type: 'error',
+ title: `${actionName}失败`,
+ message: err instanceof Error ? err.message : '操作失败',
+ });
}
- setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } });
+ setConfirmModal({
+ isOpen: false,
+ title: '',
+ message: '',
+ onConfirm: () => {},
+ onCancel: () => {},
+ });
},
onCancel: () => {
- setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } });
- }
+ setConfirmModal({
+ isOpen: false,
+ title: '',
+ message: '',
+ onConfirm: () => {},
+ onCancel: () => {},
+ });
+ },
});
};
@@ -2517,28 +3097,48 @@ const VideoSourceConfig = ({
已选 {selectedSources.size}
- 已选择 {selectedSources.size} 个视频源
+
+ 已选择 {selectedSources.size} 个视频源
+
@@ -2548,10 +3148,9 @@ const VideoSourceConfig = ({
@@ -2615,8 +3216,20 @@ const VideoSourceConfig = ({
@@ -2624,10 +3237,11 @@ const VideoSourceConfig = ({
)}
-
-
{/* 视频源表格 */}
-
+
@@ -2690,7 +3304,11 @@ const VideoSourceConfig = ({
@@ -2698,44 +3316,57 @@ const VideoSourceConfig = ({
)}
{/* 有效性检测弹窗 */}
- {showValidationModal && createPortal(
- setShowValidationModal(false)}>
- e.stopPropagation()}>
-
- 视频源有效性检测
-
-
- 请输入检测用的搜索关键词
-
-
- setSearchKeyword(e.target.value)}
- 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'
- onKeyPress={(e) => e.key === 'Enter' && handleValidateSources()}
- />
-
-
-
+ {showValidationModal &&
+ createPortal(
+ setShowValidationModal(false)}
+ >
+ e.stopPropagation()}
+ >
+
+ 视频源有效性检测
+
+
+ 请输入检测用的搜索关键词
+
+
+ setSearchKeyword(e.target.value)}
+ 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'
+ onKeyPress={(e) =>
+ e.key === 'Enter' && handleValidateSources()
+ }
+ />
+
+
+
+
-
- ,
- document.body
- )}
+ ,
+ document.body
+ )}
{/* 通用弹窗组件 */}
{/* 批量操作确认弹窗 */}
- {confirmModal.isOpen && createPortal(
-
- e.stopPropagation()}>
-
-
-
- {confirmModal.title}
-
-
-
+ {confirmModal.isOpen &&
+ createPortal(
+
+ e.stopPropagation()}
+ >
+
+
+
+ {confirmModal.title}
+
+
+
-
-
- {confirmModal.message}
-
-
+
+
+ {confirmModal.message}
+
+
- {/* 操作按钮 */}
-
-
-
+ {/* 操作按钮 */}
+
+
+
+
-
- ,
- document.body
- )}
+ ,
+ document.body
+ )}
);
};
@@ -2869,13 +3531,17 @@ const CategoryConfig = ({
const target = categories.find((c) => c.query === query && c.type === type);
if (!target) return;
const action = target.disabled ? 'enable' : 'disable';
- withLoading(`toggleCategory_${query}_${type}`, () => callCategoryApi({ action, query, type })).catch(() => {
+ withLoading(`toggleCategory_${query}_${type}`, () =>
+ callCategoryApi({ action, query, type })
+ ).catch(() => {
console.error('操作失败', action, query, type);
});
};
const handleDelete = (query: string, type: 'movie' | 'tv') => {
- withLoading(`deleteCategory_${query}_${type}`, () => callCategoryApi({ action: 'delete', query, type })).catch(() => {
+ withLoading(`deleteCategory_${query}_${type}`, () =>
+ callCategoryApi({ action: 'delete', query, type })
+ ).catch(() => {
console.error('操作失败', 'delete', query, type);
});
};
@@ -2917,7 +3583,9 @@ const CategoryConfig = ({
const handleSaveOrder = () => {
const order = categories.map((c) => `${c.query}:${c.type}`);
- withLoading('saveCategoryOrder', () => callCategoryApi({ action: 'sort', order }))
+ withLoading('saveCategoryOrder', () =>
+ callCategoryApi({ action: 'sort', order })
+ )
.then(() => {
setOrderChanged(false);
})
@@ -2943,7 +3611,7 @@ const CategoryConfig = ({
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
>
@@ -2954,10 +3622,11 @@ const CategoryConfig = ({
|
{category.type === 'movie' ? '电影' : '电视剧'}
@@ -2970,32 +3639,44 @@ const CategoryConfig = ({
|
{!category.disabled ? '启用中' : '已禁用'}
|
{category.from !== 'config' && (
@@ -3022,7 +3703,9 @@ const CategoryConfig = ({
@@ -3066,8 +3749,18 @@ const CategoryConfig = ({
@@ -3128,7 +3821,11 @@ const CategoryConfig = ({
@@ -3150,7 +3847,13 @@ const CategoryConfig = ({
};
// 新增配置文件组件
-const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => {
+const ConfigFileComponent = ({
+ config,
+ refreshConfig,
+}: {
+ config: AdminConfig | null;
+ refreshConfig: () => Promise;
+}) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [configContent, setConfigContent] = useState('');
@@ -3158,8 +3861,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
const [autoUpdate, setAutoUpdate] = useState(false);
const [lastCheckTime, setLastCheckTime] = useState('');
-
-
useEffect(() => {
if (config?.ConfigFile) {
setConfigContent(config.ConfigFile);
@@ -3171,8 +3872,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
}
}, [config]);
-
-
// 拉取订阅配置
const handleFetchConfig = async () => {
if (!subscriptionUrl.trim()) {
@@ -3211,7 +3910,9 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
};
// 处理文件上传
- const handleFileUpload = async (event: React.ChangeEvent) => {
+ const handleFileUpload = async (
+ event: React.ChangeEvent
+ ) => {
const file = event.target.files?.[0];
if (!file) return;
@@ -3224,7 +3925,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
await withLoading('uploadConfig', async () => {
try {
const fileContent = await file.text();
-
+
// 验证JSON格式
let parsedConfig;
try {
@@ -3241,9 +3942,11 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
}
// 根据api字段进行去重
- const existingConfig = configContent ? JSON.parse(configContent) : { api_site: {} };
+ const existingConfig = configContent
+ ? JSON.parse(configContent)
+ : { api_site: {} };
const existingApis = new Set();
-
+
// 收集现有配置中的所有api
Object.values(existingConfig.api_site || {}).forEach((site: any) => {
if (site.api) {
@@ -3254,31 +3957,36 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
// 合并新配置,去重处理
const mergedApiSite = { ...existingConfig.api_site };
let duplicateCount = 0;
-
- Object.entries(parsedConfig.api_site || {}).forEach(([key, site]: [string, any]) => {
- if (site.api && existingApis.has(site.api)) {
- duplicateCount++;
- // 跳过重复的api
- return;
+
+ Object.entries(parsedConfig.api_site || {}).forEach(
+ ([key, site]: [string, any]) => {
+ if (site.api && existingApis.has(site.api)) {
+ duplicateCount++;
+ // 跳过重复的api
+ return;
+ }
+ mergedApiSite[key] = site;
}
- mergedApiSite[key] = site;
- });
+ );
const mergedConfig = {
...parsedConfig,
- api_site: mergedApiSite
+ api_site: mergedApiSite,
};
// 更新配置内容
setConfigContent(JSON.stringify(mergedConfig, null, 2));
-
- const message = duplicateCount > 0
- ? `配置上传成功,跳过了 ${duplicateCount} 个重复的API`
- : '配置上传成功';
- showSuccess(message, showAlert);
+ const message =
+ duplicateCount > 0
+ ? `配置上传成功,跳过了 ${duplicateCount} 个重复的API`
+ : '配置上传成功';
+ showSuccess(message, showAlert);
} catch (err) {
- showError(err instanceof Error ? err.message : '文件上传失败', showAlert);
+ showError(
+ err instanceof Error ? err.message : '文件上传失败',
+ showAlert
+ );
throw err;
}
});
@@ -3298,7 +4006,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
configFile: configContent,
subscriptionUrl,
autoUpdate,
- lastCheckTime: lastCheckTime || new Date().toISOString()
+ lastCheckTime: lastCheckTime || new Date().toISOString(),
}),
});
@@ -3316,8 +4024,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
});
};
-
-
if (!config) {
return (
@@ -3335,7 +4041,10 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
配置订阅
- 最后更新: {lastCheckTime ? new Date(lastCheckTime).toLocaleString('zh-CN') : '从未更新'}
+ 最后更新:{' '}
+ {lastCheckTime
+ ? new Date(lastCheckTime).toLocaleString('zh-CN')
+ : '从未更新'}
@@ -3363,10 +4072,11 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
- {liveSource.channelNumber && liveSource.channelNumber > 0 ? liveSource.channelNumber : '-'}
+ {liveSource.channelNumber && liveSource.channelNumber > 0
+ ? liveSource.channelNumber
+ : '-'}
|
{!liveSource.disabled ? '启用中' : '已禁用'}
@@ -4482,10 +5492,15 @@ const LiveSourceConfig = ({
handleToggleEnable(liveSource.key)}
disabled={isLoading(`toggleLiveSource_${liveSource.key}`)}
- className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!liveSource.disabled
- ? buttonStyles.roundedDanger
- : buttonStyles.roundedSuccess
- } transition-colors ${isLoading(`toggleLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
+ className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
+ !liveSource.disabled
+ ? buttonStyles.roundedDanger
+ : buttonStyles.roundedSuccess
+ } transition-colors ${
+ isLoading(`toggleLiveSource_${liveSource.key}`)
+ ? 'opacity-50 cursor-not-allowed'
+ : ''
+ }`}
>
{!liveSource.disabled ? '禁用' : '启用'}
@@ -4494,14 +5509,22 @@ const LiveSourceConfig = ({
setEditingLiveSource(liveSource)}
disabled={isLoading(`editLiveSource_${liveSource.key}`)}
- className={`${buttonStyles.roundedPrimary} ${isLoading(`editLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
+ className={`${buttonStyles.roundedPrimary} ${
+ isLoading(`editLiveSource_${liveSource.key}`)
+ ? 'opacity-50 cursor-not-allowed'
+ : ''
+ }`}
>
编辑
handleDelete(liveSource.key)}
disabled={isLoading(`deleteLiveSource_${liveSource.key}`)}
- className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
+ className={`${buttonStyles.roundedSecondary} ${
+ isLoading(`deleteLiveSource_${liveSource.key}`)
+ ? 'opacity-50 cursor-not-allowed'
+ : ''
+ }`}
>
删除
@@ -4531,16 +5554,23 @@ const LiveSourceConfig = ({
- {isRefreshing || isLoading('refreshLiveSources') ? '刷新中...' : '刷新直播源'}
+
+ {isRefreshing || isLoading('refreshLiveSources')
+ ? '刷新中...'
+ : '刷新直播源'}
+
setShowAddForm(!showAddForm)}
- className={showAddForm ? buttonStyles.secondary : buttonStyles.success}
+ className={
+ showAddForm ? buttonStyles.secondary : buttonStyles.success
+ }
>
{showAddForm ? '取消' : '添加直播源'}
@@ -4595,13 +5625,24 @@ const LiveSourceConfig = ({
}
className='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'
/>
-
{isLoading('addLiveSource') ? '添加中...' : '添加'}
@@ -4632,7 +5673,9 @@ const LiveSourceConfig = ({
type='text'
value={editingLiveSource.name}
onChange={(e) =>
- setEditingLiveSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null)
+ setEditingLiveSource((prev) =>
+ prev ? { ...prev, name: e.target.value } : null
+ )
}
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'
/>
@@ -4656,7 +5699,9 @@ const LiveSourceConfig = ({
type='text'
value={editingLiveSource.url}
onChange={(e) =>
- setEditingLiveSource((prev) => prev ? ({ ...prev, url: e.target.value }) : null)
+ setEditingLiveSource((prev) =>
+ prev ? { ...prev, url: e.target.value } : null
+ )
}
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'
/>
@@ -4669,7 +5714,9 @@ const LiveSourceConfig = ({
type='text'
value={editingLiveSource.epg}
onChange={(e) =>
- setEditingLiveSource((prev) => prev ? ({ ...prev, epg: e.target.value }) : null)
+ setEditingLiveSource((prev) =>
+ prev ? { ...prev, epg: e.target.value } : null
+ )
}
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'
/>
@@ -4682,7 +5729,9 @@ const LiveSourceConfig = ({
type='text'
value={editingLiveSource.ua}
onChange={(e) =>
- setEditingLiveSource((prev) => prev ? ({ ...prev, ua: e.target.value }) : null)
+ setEditingLiveSource((prev) =>
+ prev ? { ...prev, ua: e.target.value } : null
+ )
}
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'
/>
@@ -4697,8 +5746,18 @@ const LiveSourceConfig = ({
{isLoading('editLiveSource') ? '保存中...' : '保存'}
@@ -4707,7 +5766,10 @@ const LiveSourceConfig = ({
)}
{/* 直播源表格 */}
-
+
@@ -4765,7 +5827,11 @@ const LiveSourceConfig = ({
{isLoading('saveLiveSourceOrder') ? '保存中...' : '保存排序'}
@@ -4782,8 +5848,6 @@ const LiveSourceConfig = ({
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
-
-
);
};
@@ -4804,6 +5868,7 @@ function AdminPageClient() {
categoryConfig: false,
configFile: false,
dataMigration: false,
+ customAdFilter: false,
});
// 获取管理员配置
@@ -4929,7 +5994,10 @@ function AdminPageClient() {
isExpanded={expandedTabs.configFile}
onToggle={() => toggleTab('configFile')}
>
-
+
)}
@@ -5004,6 +6072,34 @@ function AdminPageClient() {
+ {/* 自定义去广告标签 */}
+
+
+
+
+ }
+ isExpanded={expandedTabs.customAdFilter}
+ onToggle={() => toggleTab('customAdFilter')}
+ >
+
+
+
{/* 数据迁移标签 - 仅站长可见 */}
{role === 'owner' && (
{/* 重置配置确认弹窗 */}
- {showResetConfigModal && createPortal(
- setShowResetConfigModal(false)}>
- e.stopPropagation()}>
-
-
-
- 确认重置配置
-
- setShowResetConfigModal(false)}
- className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
- >
-
-
-
-
-
-
-
-
-
-
-
+ {showResetConfigModal &&
+ createPortal(
+ setShowResetConfigModal(false)}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ 确认重置配置
+
+ setShowResetConfigModal(false)}
+ className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
+ >
+
+
-
- ⚠️ 危险操作警告
-
+
+
+
+
+
+
+
+ 此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续?
+
-
- 此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续?
-
+
+
+ {/* 操作按钮 */}
+
+ setShowResetConfigModal(false)}
+ className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.secondary}`}
+ >
+ 取消
+
+
+ {isLoading('resetConfig') ? '重置中...' : '确认重置'}
+
-
- {/* 操作按钮 */}
-
- setShowResetConfigModal(false)}
- className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.secondary}`}
- >
- 取消
-
-
- {isLoading('resetConfig') ? '重置中...' : '确认重置'}
-
-
-
- ,
- document.body
- )}
+ ,
+ document.body
+ )}
);
}
diff --git a/src/app/api/ad-filter/route.ts b/src/app/api/ad-filter/route.ts
new file mode 100644
index 0000000..33a7033
--- /dev/null
+++ b/src/app/api/ad-filter/route.ts
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server';
+
+import { getConfig } from '@/lib/config';
+
+export const runtime = 'nodejs';
+
+/**
+ * GET /api/ad-filter
+ * 获取自定义去广告代码配置(公开接口,无需认证)
+ * 返回代码内容和版本号,客户端通过版本号判断是否需要更新缓存
+ */
+export async function GET() {
+ try {
+ const config = await getConfig();
+
+ // 返回自定义去广告代码和版本号
+ return NextResponse.json({
+ code: config.SiteConfig?.CustomAdFilterCode || '',
+ version: config.SiteConfig?.CustomAdFilterVersion || 0,
+ });
+ } catch (error) {
+ console.error('获取去广告代码配置失败:', error);
+ return NextResponse.json(
+ { error: '获取配置失败', details: (error as Error).message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts
index 19e68da..6a0e421 100644
--- a/src/app/api/admin/site/route.ts
+++ b/src/app/api/admin/site/route.ts
@@ -42,6 +42,8 @@ export async function POST(request: NextRequest) {
DanmakuApiBase,
DanmakuApiToken,
EnableComments,
+ CustomAdFilterCode,
+ CustomAdFilterVersion,
} = body as {
SiteName: string;
Announcement: string;
@@ -56,6 +58,8 @@ export async function POST(request: NextRequest) {
DanmakuApiBase: string;
DanmakuApiToken: string;
EnableComments: boolean;
+ CustomAdFilterCode?: string;
+ CustomAdFilterVersion?: number;
};
// 参数校验
@@ -72,7 +76,9 @@ export async function POST(request: NextRequest) {
typeof FluidSearch !== 'boolean' ||
typeof DanmakuApiBase !== 'string' ||
typeof DanmakuApiToken !== 'string' ||
- typeof EnableComments !== 'boolean'
+ typeof EnableComments !== 'boolean' ||
+ (CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') ||
+ (CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number')
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -105,6 +111,8 @@ export async function POST(request: NextRequest) {
DanmakuApiBase,
DanmakuApiToken,
EnableComments,
+ CustomAdFilterCode,
+ CustomAdFilterVersion,
};
// 写入数据库
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index a55c69d..43cfbfa 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -112,6 +112,57 @@ function PlayPageClient() {
blockAdEnabledRef.current = blockAdEnabled;
}, [blockAdEnabled]);
+ // 自定义去广告代码(从服务器获取并缓存)
+ const customAdFilterCodeRef = useRef ('');
+
+ // 初始化时获取自定义去广告代码
+ useEffect(() => {
+ const fetchAdFilterCode = async () => {
+ if (typeof window === 'undefined') return;
+
+ try {
+ // 先从 localStorage 获取缓存的代码,立即可用
+ const cachedCode = localStorage.getItem('custom_ad_filter_code_cache');
+ const cachedVersion = localStorage.getItem('custom_ad_filter_version_cache');
+
+ if (cachedCode) {
+ customAdFilterCodeRef.current = cachedCode;
+ console.log('使用缓存的去广告代码');
+ }
+
+ // 异步从服务器获取最新版本号,检查是否需要更新
+ const response = await fetch('/api/ad-filter');
+ if (!response.ok) {
+ console.warn('获取去广告代码配置失败,使用缓存');
+ return;
+ }
+
+ const { code, version } = await response.json();
+
+ // 如果版本号不一致或没有缓存,更新缓存
+ if (!cachedVersion || parseInt(cachedVersion) !== version) {
+ console.log('检测到去广告代码更新,更新本地缓存');
+ if (code) {
+ localStorage.setItem('custom_ad_filter_code_cache', code);
+ localStorage.setItem('custom_ad_filter_version_cache', version.toString());
+ customAdFilterCodeRef.current = code;
+ } else if (!cachedCode) {
+ // 如果服务器没有代码且本地也没有缓存,清空缓存
+ localStorage.removeItem('custom_ad_filter_code_cache');
+ localStorage.removeItem('custom_ad_filter_version_cache');
+ }
+ } else {
+ console.log('去广告代码已是最新版本');
+ }
+ } catch (error) {
+ console.error('获取去广告代码配置失败:', error);
+ // 失败时已经使用了缓存,无需额外处理
+ }
+ };
+
+ fetchAdFilterCode();
+ }, []);
+
// Anime4K超分相关状态
const [webGPUSupported, setWebGPUSupported] = useState(false);
const [anime4kEnabled, setAnime4kEnabled] = useState(false);
@@ -1067,6 +1118,30 @@ function PlayPageClient() {
};
function filterAdsFromM3U8(type: string, m3u8Content: string): string {
+ // 尝试使用缓存的自定义去广告代码
+ if (customAdFilterCodeRef.current && customAdFilterCodeRef.current.trim()) {
+ try {
+ // 移除 TypeScript 类型注解,转换为纯 JavaScript
+ const jsCode = customAdFilterCodeRef.current
+ // 移除函数参数的类型注解:name: type
+ .replace(/(\w+)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*([,)])/g, '$1$3')
+ // 移除函数返回值类型注解:): type {
+ .replace(/\)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*\{/g, ') {')
+ // 移除变量声明的类型注解:const name: type =
+ .replace(/(const|let|var)\s+(\w+)\s*:\s*(string|number|boolean|any|void|never|unknown|object)\s*=/g, '$1 $2 =');
+
+ // 创建并执行自定义函数
+ const customFunction = new Function('type', 'm3u8Content',
+ jsCode + '\nreturn filterAdsFromM3U8(type, m3u8Content);'
+ );
+ return customFunction(type, m3u8Content);
+ } catch (err) {
+ console.error('执行自定义去广告代码失败,使用默认规则:', err);
+ // 如果自定义代码执行失败,继续使用默认规则
+ }
+ }
+
+ // 默认去广告规则
if (!m3u8Content) return '';
// 按行分割M3U8内容
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index b621cfc..8f6c30a 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -21,6 +21,9 @@ export interface AdminConfig {
DanmakuApiToken: string;
// 评论功能开关
EnableComments: boolean;
+ // 自定义去广告代码
+ CustomAdFilterCode?: string;
+ CustomAdFilterVersion?: number; // 代码版本号(时间戳)
};
UserConfig: {
Users: {
| |