feat: admin config subscription
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"@upstash/redis": "^1.25.0",
|
"@upstash/redis": "^1.25.0",
|
||||||
"@vidstack/react": "^1.12.13",
|
"@vidstack/react": "^1.12.13",
|
||||||
"artplayer": "^5.2.5",
|
"artplayer": "^5.2.5",
|
||||||
|
"bs58": "^6.0.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"framer-motion": "^12.18.1",
|
"framer-motion": "^12.18.1",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^15.0.7",
|
"@testing-library/react": "^15.0.7",
|
||||||
|
"@types/bs58": "^5.0.0",
|
||||||
"@types/he": "^1.2.3",
|
"@types/he": "^1.2.3",
|
||||||
"@types/node": "24.0.3",
|
"@types/node": "24.0.3",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
artplayer:
|
artplayer:
|
||||||
specifier: ^5.2.5
|
specifier: ^5.2.5
|
||||||
version: 5.2.5
|
version: 5.2.5
|
||||||
|
bs58:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -108,6 +111,9 @@ importers:
|
|||||||
'@testing-library/react':
|
'@testing-library/react':
|
||||||
specifier: ^15.0.7
|
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)
|
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':
|
'@types/he':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
@@ -1456,6 +1462,10 @@ packages:
|
|||||||
'@types/babel__traverse@7.20.7':
|
'@types/babel__traverse@7.20.7':
|
||||||
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
|
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':
|
'@types/eslint-scope@3.7.7':
|
||||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||||
|
|
||||||
@@ -2030,6 +2040,9 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
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:
|
big.js@5.2.2:
|
||||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||||
|
|
||||||
@@ -2063,6 +2076,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bs58@6.0.0:
|
||||||
|
resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
|
||||||
|
|
||||||
bser@2.1.1:
|
bser@2.1.1:
|
||||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||||
|
|
||||||
@@ -6747,6 +6763,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.6
|
'@babel/types': 7.27.6
|
||||||
|
|
||||||
|
'@types/bs58@5.0.0':
|
||||||
|
dependencies:
|
||||||
|
bs58: 6.0.0
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
@@ -7387,6 +7407,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
base-x@5.0.1: {}
|
||||||
|
|
||||||
big.js@5.2.2: {}
|
big.js@5.2.2: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
@@ -7422,6 +7444,10 @@ snapshots:
|
|||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.25.2)
|
update-browserslist-db: 1.1.3(browserslist@4.25.2)
|
||||||
|
|
||||||
|
bs58@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
base-x: 5.0.1
|
||||||
|
|
||||||
bser@2.1.1:
|
bser@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-int64: 0.4.0
|
node-int64: 0.4.0
|
||||||
|
|||||||
@@ -149,11 +149,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
// 当前登录用户名
|
// 当前登录用户名
|
||||||
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
||||||
|
|
||||||
// 检测存储类型是否为 upstash
|
|
||||||
const isUpstashStorage =
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.UserConfig) {
|
if (config?.UserConfig) {
|
||||||
setUserSettings({
|
setUserSettings({
|
||||||
@@ -314,26 +309,19 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!isUpstashStorage &&
|
|
||||||
toggleAllowRegister(!userSettings.enableRegistration)
|
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
|
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-green-600'
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
: 'bg-gray-200 dark:bg-gray-700'
|
||||||
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${userSettings.enableRegistration
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${userSettings.enableRegistration
|
||||||
@@ -976,11 +964,6 @@ const CategoryConfig = ({
|
|||||||
from: 'config',
|
from: 'config',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检测存储类型是否为 upstash
|
|
||||||
const isUpstashStorage =
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE === 'upstash';
|
|
||||||
|
|
||||||
// dnd-kit 传感器
|
// dnd-kit 传感器
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -1066,7 +1049,6 @@ const CategoryConfig = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = (event: any) => {
|
const handleDragEnd = (event: any) => {
|
||||||
if (isUpstashStorage) return;
|
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
const oldIndex = categories.findIndex(
|
const oldIndex = categories.findIndex(
|
||||||
@@ -1107,10 +1089,9 @@ const CategoryConfig = ({
|
|||||||
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
|
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
|
||||||
>
|
>
|
||||||
<td
|
<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' }}
|
style={{ touchAction: 'none' }}
|
||||||
{...(isUpstashStorage ? {} : { ...attributes, ...listeners })}
|
{...{ ...attributes, ...listeners }}
|
||||||
>
|
>
|
||||||
<GripVertical size={16} />
|
<GripVertical size={16} />
|
||||||
</td>
|
</td>
|
||||||
@@ -1146,20 +1127,16 @@ const CategoryConfig = ({
|
|||||||
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!isUpstashStorage &&
|
|
||||||
handleToggleEnable(category.query, category.type)
|
handleToggleEnable(category.query, category.type)
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!category.disabled
|
||||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${isUpstashStorage
|
? '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-gray-400 cursor-not-allowed text-white'
|
: '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'
|
||||||
: !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`}
|
} transition-colors`}
|
||||||
>
|
>
|
||||||
{!category.disabled ? '禁用' : '启用'}
|
{!category.disabled ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
{category.from !== 'config' && !isUpstashStorage && (
|
{category.from !== 'config' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(category.query, category.type)}
|
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'
|
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'>
|
<div className='flex items-center justify-between'>
|
||||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
<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>
|
</h4>
|
||||||
<button
|
<button
|
||||||
onClick={() => !isUpstashStorage && setShowAddForm(!showAddForm)}
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
disabled={isUpstashStorage}
|
className="px-3 py-1 text-sm rounded-lg transition-colors bg-green-600 hover:bg-green-700 text-white"
|
||||||
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'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{showAddForm ? '取消' : '添加分类'}
|
{showAddForm ? '取消' : '添加分类'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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='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'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||||
<input
|
<input
|
||||||
@@ -1275,7 +1243,7 @@ const CategoryConfig = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={isUpstashStorage ? [] : sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
autoScroll={false}
|
autoScroll={false}
|
||||||
@@ -1299,7 +1267,7 @@ const CategoryConfig = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 保存排序按钮 */}
|
{/* 保存排序按钮 */}
|
||||||
{orderChanged && !isUpstashStorage && (
|
{orderChanged && (
|
||||||
<div className='flex justify-end'>
|
<div className='flex justify-end'>
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveOrder}
|
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 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
配置订阅
|
配置订阅
|
||||||
</h3>
|
</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') : '从未更新'}
|
最后更新: {lastCheckTime ? new Date(lastCheckTime).toLocaleString('zh-CN') : '从未更新'}
|
||||||
</div>
|
</div>
|
||||||
</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'
|
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'>
|
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
输入配置文件的订阅地址,支持JSON格式
|
输入配置文件的订阅地址,要求 JSON 格式,且使用 Base58 编码
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 className='flex items-center justify-between'>
|
||||||
<div>
|
<div>
|
||||||
@@ -1468,27 +1457,6 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1521,7 +1489,7 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
: 'bg-green-600 hover:bg-green-700 text-white'
|
: 'bg-green-600 hover:bg-green-700 text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{saving ? '保存中…' : '保存配置文件'}
|
{saving ? '保存中…' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>({
|
const [siteSettings, setSiteSettings] = useState<SiteConfig>({
|
||||||
SiteName: '',
|
SiteName: '',
|
||||||
Announcement: '',
|
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(() => {
|
useEffect(() => {
|
||||||
if (config?.SiteConfig) {
|
if (config?.SiteConfig) {
|
||||||
setSiteSettings({
|
setSiteSettings({
|
||||||
@@ -1651,22 +1614,18 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
|
|
||||||
// 处理豆瓣数据源变化
|
// 处理豆瓣数据源变化
|
||||||
const handleDoubanDataSourceChange = (value: string) => {
|
const handleDoubanDataSourceChange = (value: string) => {
|
||||||
if (!isUpstashStorage) {
|
setSiteSettings((prev) => ({
|
||||||
setSiteSettings((prev) => ({
|
...prev,
|
||||||
...prev,
|
DoubanProxyType: value,
|
||||||
DoubanProxyType: value,
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理豆瓣图片代理变化
|
// 处理豆瓣图片代理变化
|
||||||
const handleDoubanImageProxyChange = (value: string) => {
|
const handleDoubanImageProxyChange = (value: string) => {
|
||||||
if (!isUpstashStorage) {
|
setSiteSettings((prev) => ({
|
||||||
setSiteSettings((prev) => ({
|
...prev,
|
||||||
...prev,
|
DoubanImageProxyType: value,
|
||||||
DoubanImageProxyType: value,
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存站点配置
|
// 保存站点配置
|
||||||
@@ -1685,6 +1644,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showSuccess('保存成功, 请刷新页面');
|
showSuccess('保存成功, 请刷新页面');
|
||||||
|
await refreshConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '保存失败');
|
showError(err instanceof Error ? err.message : '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1705,55 +1665,37 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{/* 站点名称 */}
|
{/* 站点名称 */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
value={siteSettings.SiteName}
|
value={siteSettings.SiteName}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!isUpstashStorage &&
|
|
||||||
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
|
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"
|
||||||
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' : ''
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 站点公告 */}
|
{/* 站点公告 */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={siteSettings.Announcement}
|
value={siteSettings.Announcement}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!isUpstashStorage &&
|
|
||||||
setSiteSettings((prev) => ({
|
setSiteSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
Announcement: e.target.value,
|
Announcement: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
|
||||||
rows={3}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -1761,24 +1703,16 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<div className='relative' data-dropdown='douban-datasource'>
|
<div className='relative' data-dropdown='douban-datasource'>
|
||||||
{/* 自定义下拉选择框 */}
|
{/* 自定义下拉选择框 */}
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
|
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"
|
||||||
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' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
doubanDataSourceOptions.find(
|
doubanDataSourceOptions.find(
|
||||||
@@ -1796,7 +1730,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
</div>
|
</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'>
|
<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) => (
|
{doubanDataSourceOptions.map((option) => (
|
||||||
<button
|
<button
|
||||||
@@ -1850,8 +1784,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{siteSettings.DoubanProxyType === 'custom' && (
|
{siteSettings.DoubanProxyType === 'custom' && (
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
@@ -1860,15 +1793,12 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
placeholder='例如: https://proxy.example.com/fetch?url='
|
placeholder='例如: https://proxy.example.com/fetch?url='
|
||||||
value={siteSettings.DoubanProxy}
|
value={siteSettings.DoubanProxy}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!isUpstashStorage &&
|
|
||||||
setSiteSettings((prev) => ({
|
setSiteSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
DoubanProxy: e.target.value,
|
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"
|
||||||
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' : ''
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
<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 className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<div className='relative' data-dropdown='douban-image-proxy'>
|
<div className='relative' data-dropdown='douban-image-proxy'>
|
||||||
{/* 自定义下拉选择框 */}
|
{/* 自定义下拉选择框 */}
|
||||||
@@ -1900,9 +1824,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
!isDoubanImageProxyDropdownOpen
|
!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"
|
||||||
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' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
doubanImageProxyTypeOptions.find(
|
doubanImageProxyTypeOptions.find(
|
||||||
@@ -1920,7 +1842,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
</div>
|
</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'>
|
<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) => (
|
{doubanImageProxyTypeOptions.map((option) => (
|
||||||
<button
|
<button
|
||||||
@@ -1974,8 +1896,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{siteSettings.DoubanImageProxyType === 'custom' && (
|
{siteSettings.DoubanImageProxyType === 'custom' && (
|
||||||
<div>
|
<div>
|
||||||
<label
|
<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>
|
</label>
|
||||||
@@ -1984,15 +1905,12 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
placeholder='例如: https://proxy.example.com/fetch?url='
|
placeholder='例如: https://proxy.example.com/fetch?url='
|
||||||
value={siteSettings.DoubanImageProxy}
|
value={siteSettings.DoubanImageProxy}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!isUpstashStorage &&
|
|
||||||
setSiteSettings((prev) => ({
|
setSiteSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
DoubanImageProxy: e.target.value,
|
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"
|
||||||
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' : ''
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
<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>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<label
|
<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>
|
</label>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!isUpstashStorage &&
|
|
||||||
setSiteSettings((prev) => ({
|
setSiteSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
DisableYellowFilter: !prev.DisableYellowFilter,
|
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
|
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-green-600'
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
: 'bg-gray-200 dark:bg-gray-700'
|
||||||
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${siteSettings.DisableYellowFilter
|
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}`);
|
throw new Error(`重置失败: ${response.status}`);
|
||||||
}
|
}
|
||||||
showSuccess('重置成功,请刷新页面!');
|
showSuccess('重置成功,请刷新页面!');
|
||||||
|
await fetchConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '重置失败');
|
showError(err instanceof Error ? err.message : '重置失败');
|
||||||
}
|
}
|
||||||
@@ -2249,7 +2160,7 @@ function AdminPageClient() {
|
|||||||
isExpanded={expandedTabs.siteConfig}
|
isExpanded={expandedTabs.siteConfig}
|
||||||
onToggle={() => toggleTab('siteConfig')}
|
onToggle={() => toggleTab('siteConfig')}
|
||||||
>
|
>
|
||||||
<SiteConfigComponent config={config} />
|
<SiteConfigComponent config={config} refreshConfig={fetchConfig} />
|
||||||
</CollapsibleTab>
|
</CollapsibleTab>
|
||||||
|
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
adminConfig.ConfigFile = configFile;
|
adminConfig.ConfigFile = configFile;
|
||||||
|
if (!adminConfig.ConfigSubscribtion) {
|
||||||
|
adminConfig.ConfigSubscribtion = {
|
||||||
|
URL: '',
|
||||||
|
AutoUpdate: false,
|
||||||
|
LastCheck: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 更新订阅配置
|
// 更新订阅配置
|
||||||
if (subscriptionUrl !== undefined) {
|
if (subscriptionUrl !== undefined) {
|
||||||
@@ -68,7 +75,6 @@ export async function POST(request: NextRequest) {
|
|||||||
if (autoUpdate !== undefined) {
|
if (autoUpdate !== undefined) {
|
||||||
adminConfig.ConfigSubscribtion.AutoUpdate = autoUpdate;
|
adminConfig.ConfigSubscribtion.AutoUpdate = autoUpdate;
|
||||||
}
|
}
|
||||||
// 更新最后检查时间 - 使用前端传递的时间或当前时间
|
|
||||||
adminConfig.ConfigSubscribtion.LastCheck = lastCheckTime || '';
|
adminConfig.ConfigSubscribtion.LastCheck = lastCheckTime || '';
|
||||||
|
|
||||||
adminConfig = refineConfig(adminConfig);
|
adminConfig = refineConfig(adminConfig);
|
||||||
|
|||||||
@@ -20,9 +20,20 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const configContent = await response.text();
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
configContent,
|
configContent: decodedContent,
|
||||||
message: '配置拉取成功'
|
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 { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db, getStorage } from '@/lib/db';
|
||||||
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
|
import { getConfig, refineConfig } from '@/lib/config';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
console.log('Cron job triggered:', new Date().toISOString());
|
console.log('Cron job triggered:', new Date().toISOString());
|
||||||
|
|
||||||
refreshRecordAndFavorites();
|
cronJob();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -35,14 +36,54 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRecordAndFavorites() {
|
async function cronJob() {
|
||||||
if (
|
await refreshConfig();
|
||||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage') === 'localstorage'
|
await refreshRecordAndFavorites();
|
||||||
) {
|
}
|
||||||
console.log('跳过刷新:当前使用 localstorage 存储模式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const users = await db.getAllUsers();
|
const users = await db.getAllUsers();
|
||||||
if (process.env.USERNAME && !users.includes(process.env.USERNAME)) {
|
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 { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
|
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
|
||||||
import { getCustomCategories } from '@/lib/config.client';
|
|
||||||
import {
|
import {
|
||||||
getDoubanCategories,
|
getDoubanCategories,
|
||||||
getDoubanList,
|
getDoubanList,
|
||||||
@@ -81,9 +80,10 @@ function DoubanPageClient() {
|
|||||||
|
|
||||||
// 获取自定义分类数据
|
// 获取自定义分类数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCustomCategories().then((categories) => {
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
setCustomCategories(categories);
|
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||||
});
|
setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 同步最新参数值到 ref
|
// 同步最新参数值到 ref
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ const inter = Inter({ subsets: ['latin'] });
|
|||||||
|
|
||||||
// 动态生成 metadata,支持配置更新后的标题变化
|
// 动态生成 metadata,支持配置更新后的标题变化
|
||||||
export async function generateMetadata(): Promise<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';
|
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
|
||||||
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'upstash') {
|
if (storageType !== 'localstorage') {
|
||||||
const config = await getConfig();
|
|
||||||
siteName = config.SiteConfig.SiteName;
|
siteName = config.SiteConfig.SiteName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,12 @@ export default async function RootLayout({
|
|||||||
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
|
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
|
||||||
let disableYellowFilter =
|
let disableYellowFilter =
|
||||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
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();
|
const config = await getConfig();
|
||||||
siteName = config.SiteConfig.SiteName;
|
siteName = config.SiteConfig.SiteName;
|
||||||
announcement = config.SiteConfig.Announcement;
|
announcement = config.SiteConfig.Announcement;
|
||||||
@@ -62,6 +68,13 @@ export default async function RootLayout({
|
|||||||
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
|
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
|
||||||
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
|
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
|
||||||
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
|
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
|
||||||
|
customCategories = config.CustomCategories.filter(
|
||||||
|
(category) => !category.disabled
|
||||||
|
).map((category) => ({
|
||||||
|
name: category.name || '',
|
||||||
|
type: category.type,
|
||||||
|
query: category.query,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||||
@@ -73,6 +86,7 @@ export default async function RootLayout({
|
|||||||
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
|
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
|
||||||
DOUBAN_IMAGE_PROXY: doubanImageProxy,
|
DOUBAN_IMAGE_PROXY: doubanImageProxy,
|
||||||
DISABLE_YELLOW_FILTER: disableYellowFilter,
|
DISABLE_YELLOW_FILTER: disableYellowFilter,
|
||||||
|
CUSTOM_CATEGORIES: customCategories,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import Link from 'next/link';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getCustomCategories } from '@/lib/config.client';
|
|
||||||
|
|
||||||
interface MobileBottomNavProps {
|
interface MobileBottomNavProps {
|
||||||
/**
|
/**
|
||||||
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
|
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
|
||||||
@@ -48,18 +46,17 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCustomCategories().then((categories) => {
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
if (categories.length > 0) {
|
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||||
setNavItems((prevItems) => [
|
setNavItems((prevItems) => [
|
||||||
...prevItems,
|
...prevItems,
|
||||||
{
|
{
|
||||||
icon: Star,
|
icon: Star,
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
href: '/douban?type=custom',
|
href: '/douban?type=custom',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isActive = (href: string) => {
|
const isActive = (href: string) => {
|
||||||
@@ -101,8 +98,8 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
className={`h-6 w-6 ${active
|
className={`h-6 w-6 ${active
|
||||||
? 'text-green-600 dark:text-green-400'
|
? 'text-green-600 dark:text-green-400'
|
||||||
: 'text-gray-500 dark:text-gray-400'
|
: 'text-gray-500 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { getCustomCategories } from '@/lib/config.client';
|
|
||||||
|
|
||||||
import { useSite } from './SiteProvider';
|
import { useSite } from './SiteProvider';
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
@@ -150,18 +148,17 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCustomCategories().then((categories) => {
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
if (categories.length > 0) {
|
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||||
setMenuItems((prevItems) => [
|
setMenuItems((prevItems) => [
|
||||||
...prevItems,
|
...prevItems,
|
||||||
{
|
{
|
||||||
icon: Star,
|
icon: Star,
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
href: '/douban?type=custom',
|
href: '/douban?type=custom',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -432,13 +432,12 @@ export const UserMenu: React.FC = () => {
|
|||||||
当前用户
|
当前用户
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${(authInfo?.role || 'user') === 'owner'
|
||||||
(authInfo?.role || 'user') === 'owner'
|
|
||||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||||
: (authInfo?.role || 'user') === 'admin'
|
: (authInfo?.role || 'user') === 'admin'
|
||||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-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'
|
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{getRoleText(authInfo?.role || 'user')}
|
{getRoleText(authInfo?.role || 'user')}
|
||||||
</span>
|
</span>
|
||||||
@@ -517,13 +516,12 @@ export const UserMenu: React.FC = () => {
|
|||||||
updateStatus &&
|
updateStatus &&
|
||||||
updateStatus !== UpdateStatus.FETCH_FAILED && (
|
updateStatus !== UpdateStatus.FETCH_FAILED && (
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full -translate-y-2 ${
|
className={`w-2 h-2 rounded-full -translate-y-2 ${updateStatus === UpdateStatus.HAS_UPDATE
|
||||||
updateStatus === UpdateStatus.HAS_UPDATE
|
|
||||||
? 'bg-yellow-500'
|
? 'bg-yellow-500'
|
||||||
: updateStatus === UpdateStatus.NO_UPDATE
|
: updateStatus === UpdateStatus.NO_UPDATE
|
||||||
? 'bg-green-400'
|
? 'bg-green-400'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
</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'
|
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='重置为默认设置'
|
title='重置为默认设置'
|
||||||
>
|
>
|
||||||
重置
|
恢复默认
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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'>
|
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||||
isDoubanDropdownOpen ? 'rotate-180' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -613,11 +610,10 @@ export const UserMenu: React.FC = () => {
|
|||||||
handleDoubanDataSourceChange(option.value);
|
handleDoubanDataSourceChange(option.value);
|
||||||
setIsDoubanDropdownOpen(false);
|
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 ${
|
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
|
||||||
doubanDataSource === option.value
|
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
: 'text-gray-900 dark:text-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className='truncate'>{option.label}</span>
|
<span className='truncate'>{option.label}</span>
|
||||||
{doubanDataSource === option.value && (
|
{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'>
|
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||||
isDoubanDropdownOpen ? 'rotate-180' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -720,11 +715,10 @@ export const UserMenu: React.FC = () => {
|
|||||||
handleDoubanImageProxyTypeChange(option.value);
|
handleDoubanImageProxyTypeChange(option.value);
|
||||||
setIsDoubanImageProxyDropdownOpen(false);
|
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 ${
|
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
|
||||||
doubanImageProxyType === option.value
|
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
: 'text-gray-900 dark:text-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className='truncate'>{option.label}</span>
|
<span className='truncate'>{option.label}</span>
|
||||||
{doubanImageProxyType === option.value && (
|
{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) {
|
} catch (e) {
|
||||||
cfgFile = {} as ConfigFileStruct;
|
cfgFile = {} as ConfigFileStruct;
|
||||||
}
|
}
|
||||||
let adminConfig: AdminConfig = {
|
const adminConfig: AdminConfig = {
|
||||||
ConfigFile: configFile,
|
ConfigFile: configFile,
|
||||||
ConfigSubscribtion: subConfig,
|
ConfigSubscribtion: subConfig,
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
@@ -253,15 +253,14 @@ export async function resetConfig() {
|
|||||||
originConfig = await (storage as any).getAdminConfig();
|
originConfig = await (storage as any).getAdminConfig();
|
||||||
} else {
|
} else {
|
||||||
originConfig = {} as AdminConfig;
|
originConfig = {} as AdminConfig;
|
||||||
|
}
|
||||||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||||||
cachedConfig = adminConfig;
|
cachedConfig = adminConfig;
|
||||||
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
||||||
await (storage as any).setAdminConfig(adminConfig);
|
await (storage as any).setAdminConfig(adminConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCacheTime(): Promise<number> {
|
export async function getCacheTime(): Promise<number> {
|
||||||
|
|||||||
Reference in New Issue
Block a user