feat: admin config subscription

This commit is contained in:
shinya
2025-08-13 22:07:28 +08:00
parent 214aee1cb4
commit 6d4846d0b1
14 changed files with 227 additions and 259 deletions

View File

@@ -149,11 +149,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 当前登录用户名
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
// 检测存储类型是否为 upstash
const isUpstashStorage =
typeof window !== 'undefined' &&
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash';
useEffect(() => {
if (config?.UserConfig) {
setUserSettings({
@@ -314,26 +309,19 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</h4>
<div className='flex items-center justify-between'>
<label
className={`text-gray-700 dark:text-gray-300 ${isUpstashStorage ? 'opacity-50' : ''
className={`text-gray-700 dark:text-gray-300
}`}
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<button
onClick={() =>
!isUpstashStorage &&
toggleAllowRegister(!userSettings.enableRegistration)
}
disabled={isUpstashStorage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${userSettings.enableRegistration
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${userSettings.enableRegistration
@@ -976,11 +964,6 @@ const CategoryConfig = ({
from: 'config',
});
// 检测存储类型是否为 upstash
const isUpstashStorage =
typeof window !== 'undefined' &&
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash';
// dnd-kit 传感器
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -1066,7 +1049,6 @@ const CategoryConfig = ({
};
const handleDragEnd = (event: any) => {
if (isUpstashStorage) return;
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = categories.findIndex(
@@ -1107,10 +1089,9 @@ const CategoryConfig = ({
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
>
<td
className={`px-2 py-4 ${isUpstashStorage ? 'text-gray-200' : 'cursor-grab text-gray-400'
}`}
className="px-2 py-4 cursor-grab text-gray-400"
style={{ touchAction: 'none' }}
{...(isUpstashStorage ? {} : { ...attributes, ...listeners })}
{...{ ...attributes, ...listeners }}
>
<GripVertical size={16} />
</td>
@@ -1146,20 +1127,16 @@ const CategoryConfig = ({
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button
onClick={() =>
!isUpstashStorage &&
handleToggleEnable(category.query, category.type)
}
disabled={isUpstashStorage}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${isUpstashStorage
? 'bg-gray-400 cursor-not-allowed text-white'
: !category.disabled
? 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60'
: 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/60'
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!category.disabled
? 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60'
: 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/60'
} transition-colors`}
>
{!category.disabled ? '禁用' : '启用'}
</button>
{category.from !== 'config' && !isUpstashStorage && (
{category.from !== 'config' && (
<button
onClick={() => handleDelete(category.query, category.type)}
className='inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700/40 dark:hover:bg-gray-700/60 dark:text-gray-200 transition-colors'
@@ -1186,25 +1163,16 @@ const CategoryConfig = ({
<div className='flex items-center justify-between'>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</h4>
<button
onClick={() => !isUpstashStorage && setShowAddForm(!showAddForm)}
disabled={isUpstashStorage}
className={`px-3 py-1 text-sm rounded-lg transition-colors ${isUpstashStorage
? 'bg-gray-400 cursor-not-allowed text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
onClick={() => setShowAddForm(!showAddForm)}
className="px-3 py-1 text-sm rounded-lg transition-colors bg-green-600 hover:bg-green-700 text-white"
>
{showAddForm ? '取消' : '添加分类'}
</button>
</div>
{showAddForm && !isUpstashStorage && (
{showAddForm && (
<div className='p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 space-y-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<input
@@ -1275,7 +1243,7 @@ const CategoryConfig = ({
</tr>
</thead>
<DndContext
sensors={isUpstashStorage ? [] : sensors}
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
autoScroll={false}
@@ -1299,7 +1267,7 @@ const CategoryConfig = ({
</div>
{/* 保存排序按钮 */}
{orderChanged && !isUpstashStorage && (
{orderChanged && (
<div className='flex justify-end'>
<button
onClick={handleSaveOrder}
@@ -1419,7 +1387,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
</h3>
<div className='text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 px-3 py-1.5 rounded-full'>
<div className='text-sm text-gray-500 dark:text-gray-400 px-3 py-1.5 rounded-full'>
: {lastCheckTime ? new Date(lastCheckTime).toLocaleString('zh-CN') : '从未更新'}
</div>
</div>
@@ -1438,10 +1406,31 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
/>
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
JSON格式
JSON 使 Base58
</p>
</div>
{/* 拉取配置按钮 */}
<div className='pt-2'>
<button
onClick={handleFetchConfig}
disabled={fetching || !subscriptionUrl.trim()}
className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${fetching || !subscriptionUrl.trim()
? 'bg-gray-300 dark:bg-gray-600 cursor-not-allowed text-gray-500 dark:text-gray-400'
: 'bg-green-600 hover:bg-green-700 text-white shadow-sm hover:shadow-md transform hover:-translate-y-0.5'
}`}
>
{fetching ? (
<div className='flex items-center justify-center gap-2'>
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
</div>
) : (
'拉取配置'
)}
</button>
</div>
{/* 自动更新开关 */}
<div className='flex items-center justify-between'>
<div>
@@ -1468,27 +1457,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
/>
</button>
</div>
{/* 拉取配置按钮 */}
<div className='pt-2'>
<button
onClick={handleFetchConfig}
disabled={fetching || !subscriptionUrl.trim()}
className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${fetching || !subscriptionUrl.trim()
? 'bg-gray-300 dark:bg-gray-600 cursor-not-allowed text-gray-500 dark:text-gray-400'
: 'bg-green-600 hover:bg-green-700 text-white shadow-sm hover:shadow-md transform hover:-translate-y-0.5'
}`}
>
{fetching ? (
<div className='flex items-center justify-center gap-2'>
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
</div>
) : (
'拉取配置'
)}
</button>
</div>
</div>
</div>
@@ -1521,7 +1489,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
{saving ? '保存中…' : '保存配置文件'}
{saving ? '保存中…' : '保存'}
</button>
</div>
</div>
@@ -1530,7 +1498,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
};
// 新增站点配置组件
const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
const SiteConfigComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
const [siteSettings, setSiteSettings] = useState<SiteConfig>({
SiteName: '',
Announcement: '',
@@ -1595,11 +1563,6 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}
};
// 检测存储类型是否为 upstash
const isUpstashStorage =
typeof window !== 'undefined' &&
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash';
useEffect(() => {
if (config?.SiteConfig) {
setSiteSettings({
@@ -1651,22 +1614,18 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
// 处理豆瓣数据源变化
const handleDoubanDataSourceChange = (value: string) => {
if (!isUpstashStorage) {
setSiteSettings((prev) => ({
...prev,
DoubanProxyType: value,
}));
}
setSiteSettings((prev) => ({
...prev,
DoubanProxyType: value,
}));
};
// 处理豆瓣图片代理变化
const handleDoubanImageProxyChange = (value: string) => {
if (!isUpstashStorage) {
setSiteSettings((prev) => ({
...prev,
DoubanImageProxyType: value,
}));
}
setSiteSettings((prev) => ({
...prev,
DoubanImageProxyType: value,
}));
};
// 保存站点配置
@@ -1685,6 +1644,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}
showSuccess('保存成功, 请刷新页面');
await refreshConfig();
} catch (err) {
showError(err instanceof Error ? err.message : '保存失败');
} finally {
@@ -1705,55 +1665,37 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 站点名称 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<input
type='text'
value={siteSettings.SiteName}
onChange={(e) =>
!isUpstashStorage &&
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
}
disabled={isUpstashStorage}
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-green-500 focus:border-transparent ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
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-green-500 focus:border-transparent"
/>
</div>
{/* 站点公告 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<textarea
value={siteSettings.Announcement}
onChange={(e) =>
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
Announcement: e.target.value,
}))
}
disabled={isUpstashStorage}
rows={3}
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-green-500 focus:border-transparent ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
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-green-500 focus:border-transparent"
/>
</div>
@@ -1761,24 +1703,16 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='space-y-3'>
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<div className='relative' data-dropdown='douban-datasource'>
{/* 自定义下拉选择框 */}
<button
type='button'
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
className="w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left"
>
{
doubanDataSourceOptions.find(
@@ -1796,7 +1730,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
</div>
{/* 下拉选项列表 */}
{isDoubanDropdownOpen && !isUpstashStorage && (
{isDoubanDropdownOpen && (
<div className='absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto'>
{doubanDataSourceOptions.map((option) => (
<button
@@ -1850,8 +1784,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{siteSettings.DoubanProxyType === 'custom' && (
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
</label>
@@ -1860,15 +1793,12 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
placeholder='例如: https://proxy.example.com/fetch?url='
value={siteSettings.DoubanProxy}
onChange={(e) =>
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
DoubanProxy: e.target.value,
}))
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500"
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
@@ -1881,15 +1811,9 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='space-y-3'>
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<div className='relative' data-dropdown='douban-image-proxy'>
{/* 自定义下拉选择框 */}
@@ -1900,9 +1824,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
!isDoubanImageProxyDropdownOpen
)
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
className="w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left"
>
{
doubanImageProxyTypeOptions.find(
@@ -1920,7 +1842,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
</div>
{/* 下拉选项列表 */}
{isDoubanImageProxyDropdownOpen && !isUpstashStorage && (
{isDoubanImageProxyDropdownOpen && (
<div className='absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto'>
{doubanImageProxyTypeOptions.map((option) => (
<button
@@ -1974,8 +1896,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{siteSettings.DoubanImageProxyType === 'custom' && (
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
</label>
@@ -1984,15 +1905,12 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
placeholder='例如: https://proxy.example.com/fetch?url='
value={siteSettings.DoubanImageProxy}
onChange={(e) =>
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
DoubanImageProxy: e.target.value,
}))
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500"
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
@@ -2043,30 +1961,22 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div>
<div className='flex items-center justify-between'>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<button
type='button'
onClick={() =>
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
DisableYellowFilter: !prev.DisableYellowFilter,
}))
}
disabled={isUpstashStorage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${siteSettings.DisableYellowFilter
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${siteSettings.DisableYellowFilter
@@ -2171,6 +2081,7 @@ function AdminPageClient() {
throw new Error(`重置失败: ${response.status}`);
}
showSuccess('重置成功,请刷新页面!');
await fetchConfig();
} catch (err) {
showError(err instanceof Error ? err.message : '重置失败');
}
@@ -2249,7 +2160,7 @@ function AdminPageClient() {
isExpanded={expandedTabs.siteConfig}
onToggle={() => toggleTab('siteConfig')}
>
<SiteConfigComponent config={config} />
<SiteConfigComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
<div className='space-y-4'>