style: refresh admin source forms

This commit is contained in:
leowang 2026-06-23 22:25:22 +08:00
parent 7dc3db8baa
commit 898a9874d0
1 changed files with 420 additions and 408 deletions

View File

@ -21,10 +21,10 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { Alert, Avatar, Button, Card, Checkbox, Chip, Input, Label, Skeleton, Switch, Table, TextArea, TextField } from '@heroui/react';
import { import {
AlertCircle, AlertCircle,
AlertTriangle, AlertTriangle,
Check,
CheckCircle, CheckCircle,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@ -37,61 +37,19 @@ import {
User, User,
Users, Users,
Video, Video,
X,
} from 'lucide-react'; } from 'lucide-react';
import { GripVertical, Palette } from 'lucide-react'; import { GripVertical, Palette } from 'lucide-react';
import { Alert, Avatar, Button, Card, Checkbox, Chip, Input, Label, Skeleton, Switch, Table, TextArea, TextField } from '@heroui/react';
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import DataMigration from '@/components/DataMigration'; import DataMigration from '@/components/DataMigration';
import ThemeManager from '@/components/ThemeManager';
import PageLayout from '@/components/PageLayout'; import PageLayout from '@/components/PageLayout';
import ThemeManager from '@/components/ThemeManager';
import { AppDialog, AppFilterSelect } from '@/components/ui/HeroPrimitives'; import { AppDialog, AppFilterSelect } from '@/components/ui/HeroPrimitives';
// 统一按钮样式系统
const buttonStyles = {
// 主要操作按钮(蓝色)- 用于配置、设置、确认等
primary: 'px-3 py-1.5 text-sm font-medium bg-accent hover:bg-accent-strong text-accent-foreground rounded-xl transition-colors',
// 成功操作按钮(绿色)- 用于添加、启用、保存等
success: 'px-3 py-1.5 text-sm font-medium bg-accent hover:bg-accent-strong text-accent-foreground rounded-xl transition-colors',
// 危险操作按钮(红色)- 用于删除、禁用、重置等
danger: 'px-3 py-1.5 text-sm font-medium bg-danger hover:bg-danger/90 text-white rounded-xl transition-colors',
// 次要操作按钮(灰色)- 用于取消、关闭等
secondary: 'px-3 py-1.5 text-sm font-medium bg-surface-secondary hover:bg-surface-tertiary text-foreground border border-border rounded-xl transition-colors',
// 警告操作按钮(黄色)- 用于批量禁用等
warning: 'px-3 py-1.5 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-lg transition-colors',
// 小尺寸主要按钮
primarySmall: 'px-2 py-1 text-xs font-medium bg-accent hover:bg-accent-strong text-accent-foreground rounded-lg transition-colors',
// 小尺寸成功按钮
successSmall: 'px-2 py-1 text-xs font-medium bg-accent hover:bg-accent-strong text-accent-foreground rounded-lg transition-colors',
// 小尺寸危险按钮
dangerSmall: 'px-2 py-1 text-xs font-medium bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white rounded-md transition-colors',
// 小尺寸次要按钮
secondarySmall: 'px-2 py-1 text-xs font-medium bg-surface-secondary hover:bg-surface-tertiary text-foreground border border-border rounded-lg transition-colors',
// 小尺寸警告按钮
warningSmall: 'px-2 py-1 text-xs font-medium bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white rounded-md transition-colors',
// 圆角小按钮(用于表格操作)
roundedPrimary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-accent/10 text-accent hover:bg-accent/15 transition-colors',
roundedSuccess: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-accent/10 text-accent hover:bg-accent/15 transition-colors',
roundedDanger: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors',
roundedSecondary: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-surface-secondary text-foreground hover:bg-surface-tertiary transition-colors',
roundedWarning: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 dark:text-yellow-200 transition-colors',
roundedPurple: 'inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-accent/10 text-accent hover:bg-accent/15 transition-colors',
// 禁用状态
disabled: 'px-3 py-1.5 text-sm font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-lg transition-colors',
disabledSmall: 'px-2 py-1 text-xs font-medium bg-gray-400 dark:bg-gray-600 cursor-not-allowed text-white rounded-md transition-colors',
// 开关按钮样式
toggleOn: 'bg-accent',
toggleOff: 'bg-surface-tertiary',
toggleThumb: 'bg-surface',
toggleThumbOn: 'translate-x-6',
toggleThumbOff: 'translate-x-1',
// 快速操作按钮样式
quickAction: 'px-3 py-1.5 text-xs font-medium text-muted bg-surface border border-border hover:bg-surface-secondary rounded-lg transition-colors',
};
const AdminTable = ({ const AdminTable = ({
ariaLabel, ariaLabel,
minWidth = 'min-w-[900px]', minWidth = 'min-w-[900px]',
@ -545,6 +503,83 @@ interface LiveDataSource {
from: 'config' | 'custom'; from: 'config' | 'custom';
} }
type ValidationResult = {
status: 'valid' | 'invalid' | 'no_results' | 'validating' | null;
message: string;
details?: {
responseTime?: number;
resultCount?: number;
error?: string;
searchKeyword?: string;
};
};
const SourceValidationSummary = ({ result }: { result: ValidationResult }) => {
if (!result.status) return null;
const statusMeta = {
valid: {
chipColor: 'success' as const,
icon: CheckCircle,
label: '检测通过',
},
validating: {
chipColor: 'accent' as const,
icon: Settings,
label: '检测中',
},
no_results: {
chipColor: 'warning' as const,
icon: AlertTriangle,
label: '无搜索结果',
},
invalid: {
chipColor: 'danger' as const,
icon: AlertCircle,
label: '检测失败',
},
}[result.status];
const StatusIcon = statusMeta.icon;
return (
<div className='rounded-lg border border-border bg-surface/70 p-3'>
<div className='space-y-3'>
<div className='flex flex-wrap items-center gap-2'>
<span className='text-sm font-medium text-foreground'></span>
<Chip size='sm' color={statusMeta.chipColor} variant='soft'>
<Chip.Label className='inline-flex items-center gap-1.5'>
<StatusIcon
className={`size-3.5 ${result.status === 'validating' ? 'animate-spin' : ''}`}
/>
{statusMeta.label}
</Chip.Label>
</Chip>
<span className='text-sm text-muted'>{result.message}</span>
</div>
{result.details && (
<div className='grid gap-1 text-xs text-muted sm:grid-cols-2'>
{result.details.searchKeyword && (
<div>: {result.details.searchKeyword}</div>
)}
{result.details.responseTime && (
<div>: {result.details.responseTime}ms</div>
)}
{result.details.resultCount !== undefined && (
<div>: {result.details.resultCount}</div>
)}
{result.details.error && (
<div className='text-danger sm:col-span-2'>
: {result.details.error}
</div>
)}
</div>
)}
</div>
</div>
);
};
// 自定义分类数据类型 // 自定义分类数据类型
interface CustomCategory { interface CustomCategory {
name?: string; name?: string;
@ -2772,7 +2807,7 @@ const VideoSourceConfig = ({
<div className='space-y-6'> <div className='space-y-6'>
{/* 添加视频源表单 */} {/* 添加视频源表单 */}
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4'> <div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4'>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'> <h4 className='text-sm font-medium text-foreground'>
</h4> </h4>
<div className='flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-2'> <div className='flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-2'>
@ -2780,7 +2815,7 @@ const VideoSourceConfig = ({
{selectedSources.size > 0 && ( {selectedSources.size > 0 && (
<> <>
<div className='flex flex-wrap items-center gap-3 order-2 sm:order-1'> <div className='flex flex-wrap items-center gap-3 order-2 sm:order-1'>
<span className='text-sm text-gray-600 dark:text-gray-400'> <span className='text-sm text-muted'>
<span className='sm:hidden'> {selectedSources.size}</span> <span className='sm:hidden'> {selectedSources.size}</span>
<span className='hidden sm:inline'> {selectedSources.size} </span> <span className='hidden sm:inline'> {selectedSources.size} </span>
</span> </span>
@ -2809,7 +2844,7 @@ const VideoSourceConfig = ({
{isLoading('batchSource_batch_delete') ? '删除中...' : '批量删除'} {isLoading('batchSource_batch_delete') ? '删除中...' : '批量删除'}
</Button> </Button>
</div> </div>
<div className='hidden sm:block w-px h-6 bg-gray-300 dark:bg-gray-600 order-2'></div> <div className='hidden sm:block w-px h-6 bg-border order-2'></div>
</> </>
)} )}
<div className='flex items-center gap-2 order-1 sm:order-2'> <div className='flex items-center gap-2 order-1 sm:order-2'>
@ -2845,240 +2880,189 @@ const VideoSourceConfig = ({
</div> </div>
{showAddForm && ( {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'> <Card variant='secondary'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> <Card.Header className='flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between'>
<input <div>
<Card.Title></Card.Title>
<Card.Description>
Key API
</Card.Description>
</div>
</Card.Header>
<Card.Content className='space-y-4'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<TextField fullWidth>
<Label></Label>
<Input
type='text' type='text'
placeholder='名称' placeholder='例如:量子资源'
value={newSource.name} value={newSource.name}
onChange={(e) => onChange={(e) =>
setNewSource((prev) => ({ ...prev, name: e.target.value })) setNewSource((prev) => ({ ...prev, name: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label>Key</Label>
<Input
type='text' type='text'
placeholder='Key' placeholder='例如lzizy'
value={newSource.key} value={newSource.key}
onChange={(e) => onChange={(e) =>
setNewSource((prev) => ({ ...prev, key: e.target.value })) setNewSource((prev) => ({ ...prev, key: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label>API </Label>
<Input
type='text' type='text'
placeholder='API 地址' placeholder='https://example.com/api.php/provide/vod'
value={newSource.api} value={newSource.api}
onChange={(e) => onChange={(e) =>
setNewSource((prev) => ({ ...prev, api: e.target.value })) setNewSource((prev) => ({ ...prev, api: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label>Detail </Label>
<Input
type='text' type='text'
placeholder='Detail 地址(选填)' placeholder='选填,用于详情页跳转'
value={newSource.detail} value={newSource.detail}
onChange={(e) => onChange={(e) =>
setNewSource((prev) => ({ ...prev, detail: e.target.value })) setNewSource((prev) => ({ ...prev, detail: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</TextField>
</div> </div>
{/* 新增视频源有效性检测结果显示 */} <SourceValidationSummary result={newSourceValidationResult} />
{newSourceValidationResult.status && ( </Card.Content>
<div className='p-3 rounded-lg border'> <Card.Footer className='flex flex-col-reverse gap-2 sm:flex-row sm:justify-end'>
<div className='space-y-2'> <Button
<div className='flex items-center space-x-2'> className='w-full sm:w-auto'
<span className='text-sm font-medium text-gray-700 dark:text-gray-300'>:</span> variant='secondary'
<span onPress={handleValidateNewSource}
className={`px-2 py-1 text-xs rounded-full ${newSourceValidationResult.status === 'valid' isDisabled={!newSource.api || isNewSourceValidating || isLoading('validateNewSource')}
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' isPending={isNewSourceValidating || isLoading('validateNewSource')}
: newSourceValidationResult.status === 'validating'
? 'bg-accent/10 text-accent'
: newSourceValidationResult.status === 'no_results'
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}
>
{newSourceValidationResult.status === 'valid' && '✓ '}
{newSourceValidationResult.status === 'validating' && '⏳ '}
{newSourceValidationResult.status === 'no_results' && '⚠️ '}
{newSourceValidationResult.status === 'invalid' && '✗ '}
{newSourceValidationResult.message}
</span>
</div>
{newSourceValidationResult.details && (
<div className='text-xs text-gray-600 dark:text-gray-400 space-y-1'>
{newSourceValidationResult.details.searchKeyword && (
<div>: {newSourceValidationResult.details.searchKeyword}</div>
)}
{newSourceValidationResult.details.responseTime && (
<div>: {newSourceValidationResult.details.responseTime}ms</div>
)}
{newSourceValidationResult.details.resultCount !== undefined && (
<div>: {newSourceValidationResult.details.resultCount}</div>
)}
{newSourceValidationResult.details.error && (
<div className='text-red-600 dark:text-red-400'>: {newSourceValidationResult.details.error}</div>
)}
</div>
)}
</div>
</div>
)}
<div className='flex justify-end space-x-2'>
<button
onClick={handleValidateNewSource}
disabled={!newSource.api || isNewSourceValidating || isLoading('validateNewSource')}
className={`px-4 py-2 ${!newSource.api || isNewSourceValidating || isLoading('validateNewSource') ? buttonStyles.disabled : buttonStyles.primary}`}
> >
{isNewSourceValidating || isLoading('validateNewSource') ? '检测中...' : '有效性检测'} {isNewSourceValidating || isLoading('validateNewSource') ? '检测中...' : '有效性检测'}
</button> </Button>
<button <Button
onClick={handleAddSource} className='w-full sm:w-auto'
disabled={!newSource.name || !newSource.key || !newSource.api || isLoading('addSource')} variant='primary'
className={`px-4 py-2 ${!newSource.name || !newSource.key || !newSource.api || isLoading('addSource') ? buttonStyles.disabled : buttonStyles.success}`} onPress={handleAddSource}
isDisabled={!newSource.name || !newSource.key || !newSource.api || isLoading('addSource')}
isPending={isLoading('addSource')}
> >
{isLoading('addSource') ? '添加中...' : '添加'} {isLoading('addSource') ? '添加中...' : '添加'}
</button> </Button>
</div> </Card.Footer>
</div> </Card>
)} )}
{/* 编辑视频源表单 */} {/* 编辑视频源表单 */}
{editingSource && ( {editingSource && (
<div className='p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 space-y-4'> <Card variant='secondary'>
<div className='flex items-center justify-between'> <Card.Header className='flex flex-row items-start justify-between gap-4'>
<h5 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
: {editingSource.name}
</h5>
<button
onClick={handleCancelEdit}
className='text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
>
</button>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div> <div>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Card.Title></Card.Title>
<Card.Description>
</label> {editingSource.name} · {editingSource.key}
<input </Card.Description>
</div>
<Button
isIconOnly
size='sm'
variant='tertiary'
aria-label='关闭编辑视频源'
onPress={handleCancelEdit}
>
<X className='size-4' />
</Button>
</Card.Header>
<Card.Content className='space-y-4'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<TextField fullWidth>
<Label></Label>
<Input
type='text' type='text'
value={editingSource.name} value={editingSource.name}
onChange={(e) => onChange={(e) =>
setEditingSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null) setEditingSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</div> </TextField>
<div> <TextField fullWidth>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Label>Key</Label>
Key () <Input
</label>
<input
type='text' type='text'
value={editingSource.key} value={editingSource.key}
disabled disabled
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed' variant='secondary'
/> />
</div> <p className='text-xs text-muted'>Key </p>
<div> </TextField>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <TextField fullWidth>
API <Label>API </Label>
</label> <Input
<input
type='text' type='text'
value={editingSource.api} value={editingSource.api}
onChange={(e) => onChange={(e) =>
setEditingSource((prev) => prev ? ({ ...prev, api: e.target.value }) : null) setEditingSource((prev) => prev ? ({ ...prev, api: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</div> </TextField>
<div> <TextField fullWidth>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Label>Detail </Label>
Detail <Input
</label>
<input
type='text' type='text'
value={editingSource.detail || ''} value={editingSource.detail || ''}
onChange={(e) => onChange={(e) =>
setEditingSource((prev) => prev ? ({ ...prev, detail: e.target.value }) : null) setEditingSource((prev) => prev ? ({ ...prev, detail: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</TextField>
</div> </div>
{/* 有效性检测结果显示 */} <SourceValidationSummary result={singleValidationResult} />
{singleValidationResult.status && ( </Card.Content>
<div className='col-span-full mt-4 p-3 rounded-lg border'> <Card.Footer className='flex flex-col-reverse gap-2 sm:flex-row sm:justify-end'>
<div className='space-y-2'> <Button
<div className='flex items-center space-x-2'> className='w-full sm:w-auto'
<span className='text-sm font-medium text-gray-700 dark:text-gray-300'>:</span> variant='secondary'
<span onPress={handleCancelEdit}
className={`px-2 py-1 text-xs rounded-full ${singleValidationResult.status === 'valid'
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: singleValidationResult.status === 'validating'
? 'bg-accent/10 text-accent'
: singleValidationResult.status === 'no_results'
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}
>
{singleValidationResult.status === 'valid' && '✓ '}
{singleValidationResult.status === 'validating' && '⏳ '}
{singleValidationResult.status === 'no_results' && '⚠️ '}
{singleValidationResult.status === 'invalid' && '✗ '}
{singleValidationResult.message}
</span>
</div>
{singleValidationResult.details && (
<div className='text-xs text-gray-600 dark:text-gray-400 space-y-1'>
{singleValidationResult.details.searchKeyword && (
<div>: {singleValidationResult.details.searchKeyword}</div>
)}
{singleValidationResult.details.responseTime && (
<div>: {singleValidationResult.details.responseTime}ms</div>
)}
{singleValidationResult.details.resultCount !== undefined && (
<div>: {singleValidationResult.details.resultCount}</div>
)}
{singleValidationResult.details.error && (
<div className='text-red-600 dark:text-red-400'>: {singleValidationResult.details.error}</div>
)}
</div>
)}
</div>
</div>
)}
</div>
<div className='flex justify-end space-x-2'>
<button
onClick={handleCancelEdit}
className={buttonStyles.secondary}
> >
</button> </Button>
<button <Button
onClick={handleValidateSingleSource} className='w-full sm:w-auto'
disabled={!editingSource.api || isSingleValidating || isLoading('validateSingleSource')} variant='secondary'
className={`${!editingSource.api || isSingleValidating || isLoading('validateSingleSource') ? buttonStyles.disabled : buttonStyles.primary}`} onPress={handleValidateSingleSource}
isDisabled={!editingSource.api || isSingleValidating || isLoading('validateSingleSource')}
isPending={isSingleValidating || isLoading('validateSingleSource')}
> >
{isSingleValidating || isLoading('validateSingleSource') ? '检测中...' : '有效性检测'} {isSingleValidating || isLoading('validateSingleSource') ? '检测中...' : '有效性检测'}
</button> </Button>
<button <Button
onClick={handleEditSource} className='w-full sm:w-auto'
disabled={!editingSource.name || !editingSource.api || isLoading('editSource')} variant='primary'
className={`${!editingSource.name || !editingSource.api || isLoading('editSource') ? buttonStyles.disabled : buttonStyles.success}`} onPress={handleEditSource}
isDisabled={!editingSource.name || !editingSource.api || isLoading('editSource')}
isPending={isLoading('editSource')}
> >
{isLoading('editSource') ? '保存中...' : '保存'} {isLoading('editSource') ? '保存中...' : '保存'}
</button> </Button>
</div> </Card.Footer>
</div> </Card>
)} )}
@ -4380,7 +4364,7 @@ const LiveSourceConfig = ({
<div className='space-y-6'> <div className='space-y-6'>
{/* 添加直播源表单 */} {/* 添加直播源表单 */}
<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-foreground'>
</h4> </h4>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
@ -4403,162 +4387,190 @@ const LiveSourceConfig = ({
</div> </div>
{showAddForm && ( {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'> <Card variant='secondary'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> <Card.Header>
<input <div>
<Card.Title></Card.Title>
<Card.Description>
M3U UA
</Card.Description>
</div>
</Card.Header>
<Card.Content className='space-y-4'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<TextField fullWidth>
<Label></Label>
<Input
type='text' type='text'
placeholder='名称' placeholder='例如:央视频道'
value={newLiveSource.name} value={newLiveSource.name}
onChange={(e) => onChange={(e) =>
setNewLiveSource((prev) => ({ ...prev, name: e.target.value })) setNewLiveSource((prev) => ({ ...prev, name: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label>Key</Label>
<Input
type='text' type='text'
placeholder='Key' placeholder='例如cctv'
value={newLiveSource.key} value={newLiveSource.key}
onChange={(e) => onChange={(e) =>
setNewLiveSource((prev) => ({ ...prev, key: e.target.value })) setNewLiveSource((prev) => ({ ...prev, key: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label>M3U </Label>
<Input
type='text' type='text'
placeholder='M3U 地址' placeholder='https://example.com/live.m3u'
value={newLiveSource.url} value={newLiveSource.url}
onChange={(e) => onChange={(e) =>
setNewLiveSource((prev) => ({ ...prev, url: e.target.value })) setNewLiveSource((prev) => ({ ...prev, url: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField fullWidth>
<Label></Label>
<Input
type='text' type='text'
placeholder='节目单地址(选填)' placeholder='选填XMLTV EPG 地址'
value={newLiveSource.epg} value={newLiveSource.epg}
onChange={(e) => onChange={(e) =>
setNewLiveSource((prev) => ({ ...prev, epg: e.target.value })) setNewLiveSource((prev) => ({ ...prev, epg: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
<input </TextField>
<TextField className='sm:col-span-2' fullWidth>
<Label> UA</Label>
<Input
type='text' type='text'
placeholder='自定义 UA选填' placeholder='选填,例如 Mozilla/5.0'
value={newLiveSource.ua} value={newLiveSource.ua}
onChange={(e) => onChange={(e) =>
setNewLiveSource((prev) => ({ ...prev, ua: e.target.value })) setNewLiveSource((prev) => ({ ...prev, ua: e.target.value }))
} }
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</TextField>
</div> </div>
<div className='flex justify-end'> </Card.Content>
<button <Card.Footer className='flex justify-end'>
onClick={handleAddLiveSource} <Button
disabled={!newLiveSource.name || !newLiveSource.key || !newLiveSource.url || isLoading('addLiveSource')} className='w-full sm:w-auto'
className={`w-full sm:w-auto px-4 py-2 ${!newLiveSource.name || !newLiveSource.key || !newLiveSource.url || isLoading('addLiveSource') ? buttonStyles.disabled : buttonStyles.success}`} variant='primary'
onPress={handleAddLiveSource}
isDisabled={!newLiveSource.name || !newLiveSource.key || !newLiveSource.url || isLoading('addLiveSource')}
isPending={isLoading('addLiveSource')}
> >
{isLoading('addLiveSource') ? '添加中...' : '添加'} {isLoading('addLiveSource') ? '添加中...' : '添加'}
</button> </Button>
</div> </Card.Footer>
</div> </Card>
)} )}
{/* 编辑直播源表单 */} {/* 编辑直播源表单 */}
{editingLiveSource && ( {editingLiveSource && (
<div className='p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 space-y-4'> <Card variant='secondary'>
<div className='flex items-center justify-between'> <Card.Header className='flex flex-row items-start justify-between gap-4'>
<h5 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
: {editingLiveSource.name}
</h5>
<button
onClick={handleCancelEdit}
className='text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
>
</button>
</div>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div> <div>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Card.Title></Card.Title>
<Card.Description>
</label> {editingLiveSource.name} · {editingLiveSource.key}
<input </Card.Description>
</div>
<Button
isIconOnly
size='sm'
variant='tertiary'
aria-label='关闭编辑直播源'
onPress={handleCancelEdit}
>
<X className='size-4' />
</Button>
</Card.Header>
<Card.Content className='space-y-4'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<TextField fullWidth>
<Label></Label>
<Input
type='text' type='text'
value={editingLiveSource.name} value={editingLiveSource.name}
onChange={(e) => onChange={(e) =>
setEditingLiveSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null) setEditingLiveSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</div> </TextField>
<div> <TextField fullWidth>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Label>Key</Label>
Key () <Input
</label>
<input
type='text' type='text'
value={editingLiveSource.key} value={editingLiveSource.key}
disabled disabled
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed' variant='secondary'
/> />
</div> <p className='text-xs text-muted'>Key </p>
<div> </TextField>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <TextField fullWidth>
M3U <Label>M3U </Label>
</label> <Input
<input
type='text' type='text'
value={editingLiveSource.url} value={editingLiveSource.url}
onChange={(e) => onChange={(e) =>
setEditingLiveSource((prev) => prev ? ({ ...prev, url: e.target.value }) : null) setEditingLiveSource((prev) => prev ? ({ ...prev, url: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</div> </TextField>
<div> <TextField fullWidth>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Label></Label>
<Input
</label>
<input
type='text' type='text'
value={editingLiveSource.epg} value={editingLiveSource.epg}
onChange={(e) => onChange={(e) =>
setEditingLiveSource((prev) => prev ? ({ ...prev, epg: e.target.value }) : null) setEditingLiveSource((prev) => prev ? ({ ...prev, epg: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</div> </TextField>
<div> <TextField className='sm:col-span-2' fullWidth>
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'> <Label> UA</Label>
UA <Input
</label>
<input
type='text' type='text'
value={editingLiveSource.ua} value={editingLiveSource.ua}
onChange={(e) => onChange={(e) =>
setEditingLiveSource((prev) => prev ? ({ ...prev, ua: e.target.value }) : null) setEditingLiveSource((prev) => prev ? ({ ...prev, ua: e.target.value }) : null)
} }
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' variant='secondary'
/> />
</TextField>
</div> </div>
</div> </Card.Content>
<div className='flex justify-end space-x-2'> <Card.Footer className='flex flex-col-reverse gap-2 sm:flex-row sm:justify-end'>
<button <Button
onClick={handleCancelEdit} className='w-full sm:w-auto'
className={buttonStyles.secondary} variant='secondary'
onPress={handleCancelEdit}
> >
</button> </Button>
<button <Button
onClick={handleEditLiveSource} className='w-full sm:w-auto'
disabled={!editingLiveSource.name || !editingLiveSource.url || isLoading('editLiveSource')} variant='primary'
className={`${!editingLiveSource.name || !editingLiveSource.url || isLoading('editLiveSource') ? buttonStyles.disabled : buttonStyles.success}`} onPress={handleEditLiveSource}
isDisabled={!editingLiveSource.name || !editingLiveSource.url || isLoading('editLiveSource')}
isPending={isLoading('editLiveSource')}
> >
{isLoading('editLiveSource') ? '保存中...' : '保存'} {isLoading('editLiveSource') ? '保存中...' : '保存'}
</button> </Button>
</div> </Card.Footer>
</div> </Card>
)} )}
{/* 直播源表格 */} {/* 直播源表格 */}