feat: admin config subscription
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
"@upstash/redis": "^1.25.0",
|
||||
"@vidstack/react": "^1.12.13",
|
||||
"artplayer": "^5.2.5",
|
||||
"bs58": "^6.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
"he": "^1.2.0",
|
||||
@@ -54,6 +55,7 @@
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/bs58": "^5.0.0",
|
||||
"@types/he": "^1.2.3",
|
||||
"@types/node": "24.0.3",
|
||||
"@types/react": "^18.3.18",
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
||||
artplayer:
|
||||
specifier: ^5.2.5
|
||||
version: 5.2.5
|
||||
bs58:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
clsx:
|
||||
specifier: ^2.0.0
|
||||
version: 2.1.1
|
||||
@@ -108,6 +111,9 @@ importers:
|
||||
'@testing-library/react':
|
||||
specifier: ^15.0.7
|
||||
version: 15.0.7(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/bs58':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
'@types/he':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
@@ -1456,6 +1462,10 @@ packages:
|
||||
'@types/babel__traverse@7.20.7':
|
||||
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
|
||||
|
||||
'@types/bs58@5.0.0':
|
||||
resolution: {integrity: sha512-cAw/jKBzo98m6Xz1X5ETqymWfIMbXbu6nK15W4LQYjeHJkVqSmM5PO8Bd9KVHQJ/F4rHcSso9LcjtgCW6TGu2w==}
|
||||
deprecated: This is a stub types definition. bs58 provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
@@ -2030,6 +2040,9 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
base-x@5.0.1:
|
||||
resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==}
|
||||
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
@@ -2063,6 +2076,9 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
bs58@6.0.0:
|
||||
resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
|
||||
|
||||
bser@2.1.1:
|
||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||
|
||||
@@ -6747,6 +6763,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.27.6
|
||||
|
||||
'@types/bs58@5.0.0':
|
||||
dependencies:
|
||||
bs58: 6.0.0
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
@@ -7387,6 +7407,8 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base-x@5.0.1: {}
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
@@ -7422,6 +7444,10 @@ snapshots:
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.2)
|
||||
|
||||
bs58@6.0.0:
|
||||
dependencies:
|
||||
base-x: 5.0.1
|
||||
|
||||
bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64: 0.4.0
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -60,6 +60,13 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
adminConfig.ConfigFile = configFile;
|
||||
if (!adminConfig.ConfigSubscribtion) {
|
||||
adminConfig.ConfigSubscribtion = {
|
||||
URL: '',
|
||||
AutoUpdate: false,
|
||||
LastCheck: '',
|
||||
};
|
||||
}
|
||||
|
||||
// 更新订阅配置
|
||||
if (subscriptionUrl !== undefined) {
|
||||
@@ -68,7 +75,6 @@ export async function POST(request: NextRequest) {
|
||||
if (autoUpdate !== undefined) {
|
||||
adminConfig.ConfigSubscribtion.AutoUpdate = autoUpdate;
|
||||
}
|
||||
// 更新最后检查时间 - 使用前端传递的时间或当前时间
|
||||
adminConfig.ConfigSubscribtion.LastCheck = lastCheckTime || '';
|
||||
|
||||
adminConfig = refineConfig(adminConfig);
|
||||
|
||||
@@ -20,9 +20,20 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const configContent = await response.text();
|
||||
|
||||
// 对 configContent 进行 base58 解码
|
||||
let decodedContent;
|
||||
try {
|
||||
const bs58 = (await import('bs58')).default;
|
||||
const decodedBytes = bs58.decode(configContent);
|
||||
decodedContent = new TextDecoder().decode(decodedBytes);
|
||||
} catch (decodeError) {
|
||||
console.warn('Base58 解码失败,返回原始内容:', decodeError);
|
||||
throw decodeError;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
configContent,
|
||||
configContent: decodedContent,
|
||||
message: '配置拉取成功'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { getConfig } from "@/lib/config";
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
console.log('custom_category', req.url);
|
||||
const config = await getConfig();
|
||||
return NextResponse.json(config.CustomCategories);
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { db, getStorage } from '@/lib/db';
|
||||
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
import { getConfig, refineConfig } from '@/lib/config';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@@ -13,7 +14,7 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('Cron job triggered:', new Date().toISOString());
|
||||
|
||||
refreshRecordAndFavorites();
|
||||
cronJob();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -35,14 +36,54 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRecordAndFavorites() {
|
||||
if (
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage') === 'localstorage'
|
||||
) {
|
||||
console.log('跳过刷新:当前使用 localstorage 存储模式');
|
||||
return;
|
||||
}
|
||||
async function cronJob() {
|
||||
await refreshConfig();
|
||||
await refreshRecordAndFavorites();
|
||||
}
|
||||
|
||||
async function refreshConfig() {
|
||||
let config = await getConfig();
|
||||
if (config && config.ConfigSubscribtion && config.ConfigSubscribtion.URL && config.ConfigSubscribtion.AutoUpdate) {
|
||||
try {
|
||||
const response = await fetch(config.ConfigSubscribtion.URL);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const configContent = await response.text();
|
||||
|
||||
// 对 configContent 进行 base58 解码
|
||||
let decodedContent;
|
||||
try {
|
||||
const bs58 = (await import('bs58')).default;
|
||||
const decodedBytes = bs58.decode(configContent);
|
||||
decodedContent = new TextDecoder().decode(decodedBytes);
|
||||
} catch (decodeError) {
|
||||
console.warn('Base58 解码失败:', decodeError);
|
||||
throw decodeError;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(decodedContent);
|
||||
} catch (e) {
|
||||
throw new Error('配置文件格式错误,请检查 JSON 语法');
|
||||
}
|
||||
config.ConfigFile = decodedContent;
|
||||
config = refineConfig(config);
|
||||
const storage = getStorage();
|
||||
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
||||
await (storage as any).setAdminConfig(config);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('刷新配置失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('跳过刷新:未配置订阅地址或自动更新');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRecordAndFavorites() {
|
||||
try {
|
||||
const users = await db.getAllUsers();
|
||||
if (process.env.USERNAME && !users.includes(process.env.USERNAME)) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Suspense } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
|
||||
import { getCustomCategories } from '@/lib/config.client';
|
||||
import {
|
||||
getDoubanCategories,
|
||||
getDoubanList,
|
||||
@@ -81,9 +80,10 @@ function DoubanPageClient() {
|
||||
|
||||
// 获取自定义分类数据
|
||||
useEffect(() => {
|
||||
getCustomCategories().then((categories) => {
|
||||
setCustomCategories(categories);
|
||||
});
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 同步最新参数值到 ref
|
||||
|
||||
@@ -16,9 +16,10 @@ const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
// 动态生成 metadata,支持配置更新后的标题变化
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||
const config = await getConfig();
|
||||
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
|
||||
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'upstash') {
|
||||
const config = await getConfig();
|
||||
if (storageType !== 'localstorage') {
|
||||
siteName = config.SiteConfig.SiteName;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,12 @@ export default async function RootLayout({
|
||||
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
|
||||
let disableYellowFilter =
|
||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
||||
if (storageType !== 'upstash' && storageType !== 'localstorage') {
|
||||
let customCategories = [] as {
|
||||
name: string;
|
||||
type: 'movie' | 'tv';
|
||||
query: string;
|
||||
}[];
|
||||
if (storageType !== 'localstorage') {
|
||||
const config = await getConfig();
|
||||
siteName = config.SiteConfig.SiteName;
|
||||
announcement = config.SiteConfig.Announcement;
|
||||
@@ -62,6 +68,13 @@ export default async function RootLayout({
|
||||
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
|
||||
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
|
||||
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
|
||||
customCategories = config.CustomCategories.filter(
|
||||
(category) => !category.disabled
|
||||
).map((category) => ({
|
||||
name: category.name || '',
|
||||
type: category.type,
|
||||
query: category.query,
|
||||
}));
|
||||
}
|
||||
|
||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||
@@ -73,6 +86,7 @@ export default async function RootLayout({
|
||||
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
|
||||
DOUBAN_IMAGE_PROXY: doubanImageProxy,
|
||||
DISABLE_YELLOW_FILTER: disableYellowFilter,
|
||||
CUSTOM_CATEGORIES: customCategories,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,8 +7,6 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getCustomCategories } from '@/lib/config.client';
|
||||
|
||||
interface MobileBottomNavProps {
|
||||
/**
|
||||
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
|
||||
@@ -48,18 +46,17 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
getCustomCategories().then((categories) => {
|
||||
if (categories.length > 0) {
|
||||
setNavItems((prevItems) => [
|
||||
...prevItems,
|
||||
{
|
||||
icon: Star,
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setNavItems((prevItems) => [
|
||||
...prevItems,
|
||||
{
|
||||
icon: Star,
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isActive = (href: string) => {
|
||||
@@ -101,8 +98,8 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
>
|
||||
<item.icon
|
||||
className={`h-6 w-6 ${active
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { getCustomCategories } from '@/lib/config.client';
|
||||
|
||||
import { useSite } from './SiteProvider';
|
||||
|
||||
interface SidebarContextType {
|
||||
@@ -150,18 +148,17 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
getCustomCategories().then((categories) => {
|
||||
if (categories.length > 0) {
|
||||
setMenuItems((prevItems) => [
|
||||
...prevItems,
|
||||
{
|
||||
icon: Star,
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setMenuItems((prevItems) => [
|
||||
...prevItems,
|
||||
{
|
||||
icon: Star,
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -432,13 +432,12 @@ export const UserMenu: React.FC = () => {
|
||||
当前用户
|
||||
</span>
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
(authInfo?.role || 'user') === 'owner'
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${(authInfo?.role || 'user') === 'owner'
|
||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
: (authInfo?.role || 'user') === 'admin'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||
}`}
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||
}`}
|
||||
>
|
||||
{getRoleText(authInfo?.role || 'user')}
|
||||
</span>
|
||||
@@ -517,13 +516,12 @@ export const UserMenu: React.FC = () => {
|
||||
updateStatus &&
|
||||
updateStatus !== UpdateStatus.FETCH_FAILED && (
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full -translate-y-2 ${
|
||||
updateStatus === UpdateStatus.HAS_UPDATE
|
||||
className={`w-2 h-2 rounded-full -translate-y-2 ${updateStatus === UpdateStatus.HAS_UPDATE
|
||||
? 'bg-yellow-500'
|
||||
: updateStatus === UpdateStatus.NO_UPDATE
|
||||
? 'bg-green-400'
|
||||
: ''
|
||||
}`}
|
||||
? 'bg-green-400'
|
||||
: ''
|
||||
}`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
@@ -555,7 +553,7 @@ export const UserMenu: React.FC = () => {
|
||||
className='px-2 py-1 text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 border border-red-200 hover:border-red-300 dark:border-red-800 dark:hover:border-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors'
|
||||
title='重置为默认设置'
|
||||
>
|
||||
重置
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -596,9 +594,8 @@ export const UserMenu: React.FC = () => {
|
||||
{/* 下拉箭头 */}
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
||||
isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -613,11 +610,10 @@ export const UserMenu: React.FC = () => {
|
||||
handleDoubanDataSourceChange(option.value);
|
||||
setIsDoubanDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
doubanDataSource === option.value
|
||||
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanDataSource === option.value
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<span className='truncate'>{option.label}</span>
|
||||
{doubanDataSource === option.value && (
|
||||
@@ -703,9 +699,8 @@ export const UserMenu: React.FC = () => {
|
||||
{/* 下拉箭头 */}
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
||||
isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -720,11 +715,10 @@ export const UserMenu: React.FC = () => {
|
||||
handleDoubanImageProxyTypeChange(option.value);
|
||||
setIsDoubanImageProxyDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
doubanImageProxyType === option.value
|
||||
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanImageProxyType === option.value
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<span className='truncate'>{option.label}</span>
|
||||
{doubanImageProxyType === option.value && (
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
'use client';
|
||||
|
||||
export async function getCustomCategories(): Promise<{
|
||||
name: string;
|
||||
type: 'movie' | 'tv';
|
||||
query: string;
|
||||
}[]> {
|
||||
const res = await fetch('/api/config/custom_category');
|
||||
const data = await res.json();
|
||||
return data.filter((item: any) => !item.disabled).map((category: any) => ({
|
||||
name: category.name || '',
|
||||
type: category.type,
|
||||
query: category.query,
|
||||
}));
|
||||
}
|
||||
@@ -149,7 +149,7 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
} catch (e) {
|
||||
cfgFile = {} as ConfigFileStruct;
|
||||
}
|
||||
let adminConfig: AdminConfig = {
|
||||
const adminConfig: AdminConfig = {
|
||||
ConfigFile: configFile,
|
||||
ConfigSubscribtion: subConfig,
|
||||
SiteConfig: {
|
||||
@@ -253,15 +253,14 @@ export async function resetConfig() {
|
||||
originConfig = await (storage as any).getAdminConfig();
|
||||
} else {
|
||||
originConfig = {} as AdminConfig;
|
||||
|
||||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||||
}
|
||||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||||
cachedConfig = adminConfig;
|
||||
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
||||
await (storage as any).setAdminConfig(adminConfig);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheTime(): Promise<number> {
|
||||
|
||||
Reference in New Issue
Block a user