mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import {
|
|
ManageDetailPanel,
|
|
ManagePage,
|
|
} from '@/components/common/general/manage-pannel';
|
|
import { ShieldCheck } from 'lucide-react';
|
|
|
|
import { cn, formatDateTime } from '@/lib/utils';
|
|
import type { SystemConfig } from '@/lib/services/admin';
|
|
import { AdminProvider, useAdmin } from '@/contexts/admin-context';
|
|
|
|
/**
|
|
* 系统配置
|
|
* 显示系统配置的详细信息和编辑面板
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <SystemConfigDetailPanel
|
|
* config={config}
|
|
* editData={editData}
|
|
* onEditDataChange={onEditDataChange}
|
|
* onSave={onSave}
|
|
* saving={saving}
|
|
* />
|
|
* ```
|
|
* @param {SystemConfig} config - 系统配置
|
|
* @param {Partial<SystemConfig>} editData - 编辑数据
|
|
* @param {function} onEditDataChange - 编辑数据改变回调
|
|
* @param {function} onSave - 保存回调
|
|
* @param {boolean} saving - 是否正在保存
|
|
* @returns {React.ReactNode} 系统配置详情面板组件
|
|
*/
|
|
function SystemConfigDetailPanel({
|
|
config,
|
|
editData,
|
|
onEditDataChange,
|
|
onSave,
|
|
saving,
|
|
}: {
|
|
config: SystemConfig | null;
|
|
editData: Partial<SystemConfig>;
|
|
onEditDataChange: (
|
|
field: keyof SystemConfig,
|
|
value: SystemConfig[keyof SystemConfig],
|
|
) => void;
|
|
onSave: () => void;
|
|
saving: boolean;
|
|
}) {
|
|
const t = useTranslations('admin.system');
|
|
return (
|
|
<ManageDetailPanel isEmpty={!config} onSave={onSave} saving={saving}>
|
|
<div className='grid grid-cols-1 gap-0'>
|
|
<div className='border border-dashed rounded-lg'>
|
|
<div className='px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('configKey')}
|
|
</label>
|
|
<p className='text-xs text-muted-foreground font-mono'>
|
|
{config?.key}
|
|
</p>
|
|
</div>
|
|
|
|
<div className='pl-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('configValue')}
|
|
</label>
|
|
<div className='flex gap-1 w-[90%] justify-end items-center pr-3'>
|
|
{config?.key.endsWith('_enabled') ? (
|
|
<Switch
|
|
checked={
|
|
editData.value !== undefined
|
|
? editData.value === 'true'
|
|
: config?.value === 'true'
|
|
}
|
|
onCheckedChange={(checked) => {
|
|
onEditDataChange('value', checked ? 'true' : 'false');
|
|
}}
|
|
/>
|
|
) : (
|
|
<Input
|
|
type={
|
|
config?.key &&
|
|
(config.key.endsWith('_limit') ||
|
|
config.key.endsWith('_minutes') ||
|
|
config.key.endsWith('_days') ||
|
|
config.key.endsWith('_seconds') ||
|
|
config.key.endsWith('_hours') ||
|
|
config.key.endsWith('_ttl') ||
|
|
config.key.endsWith('_max') ||
|
|
config.key.endsWith('_min') ||
|
|
config.key.includes('max_') ||
|
|
config.key.includes('min_') ||
|
|
config.key.includes('limit') ||
|
|
config.key.includes('count'))
|
|
? 'number'
|
|
: 'text'
|
|
}
|
|
step='1'
|
|
min='0'
|
|
value={
|
|
editData.value !== undefined
|
|
? editData.value
|
|
: config?.value || ''
|
|
}
|
|
placeholder={
|
|
editData.value === undefined && !config?.value
|
|
? t('required')
|
|
: ''
|
|
}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
if (value === '') {
|
|
onEditDataChange('value', '');
|
|
return;
|
|
}
|
|
onEditDataChange('value', value);
|
|
}}
|
|
className='!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]'
|
|
style={{
|
|
MozAppearance: 'textfield',
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className='pl-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('configDescription')}
|
|
</label>
|
|
<div className='flex gap-1 w-[90%]'>
|
|
<Input
|
|
type='text'
|
|
value={
|
|
editData.description !== undefined
|
|
? editData.description
|
|
: config?.description || ''
|
|
}
|
|
placeholder={t('optionalDescription')}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
onEditDataChange('description', value);
|
|
}}
|
|
className='!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px]'
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className='px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('publicVisibility')}
|
|
</label>
|
|
<Switch
|
|
checked={(editData.visibility ?? config?.visibility ?? 0) === 1}
|
|
onCheckedChange={(checked) => {
|
|
onEditDataChange('visibility', checked ? 1 : 0);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className='px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('configType')}
|
|
</label>
|
|
<span
|
|
className={`text-[11px] px-1.5 py-0.5 rounded font-medium ${
|
|
config?.type === 'system'
|
|
? 'bg-blue-500/10 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400'
|
|
: 'bg-indigo-500/10 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400'
|
|
}`}
|
|
>
|
|
{config?.type === 'system'
|
|
? t('systemConfig')
|
|
: t('businessConfig')}
|
|
</span>
|
|
</div>
|
|
|
|
<div className='px-3 py-2 flex items-center justify-between'>
|
|
<label className='text-xs font-medium text-muted-foreground'>
|
|
{t('createdAt')}
|
|
</label>
|
|
<p className='text-xs text-muted-foreground'>
|
|
{config ? formatDateTime(config.created_at) : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ManageDetailPanel>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 系统配置管理组件
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <SystemConfigs />
|
|
* ```
|
|
* @returns {React.ReactNode} 系统配置管理组件
|
|
*/
|
|
export function SystemConfigs() {
|
|
const t = useTranslations('admin.system');
|
|
const {
|
|
systemConfigs: configs,
|
|
systemConfigsLoading: loading,
|
|
systemConfigsError: error,
|
|
refetchSystemConfigs,
|
|
updateSystemConfig,
|
|
} = useAdmin();
|
|
|
|
const [activeTab, setActiveTab] = React.useState<'system' | 'business'>(
|
|
'business',
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
refetchSystemConfigs(activeTab);
|
|
}, [activeTab, refetchSystemConfigs]);
|
|
|
|
const getInitialEditData = (config: SystemConfig) => ({
|
|
value: config.value,
|
|
visibility: config.visibility,
|
|
description: config.description,
|
|
});
|
|
|
|
const handleSave = async (
|
|
config: SystemConfig,
|
|
editData: Partial<SystemConfig>,
|
|
) => {
|
|
if (!config) return;
|
|
|
|
await updateSystemConfig(config.key, {
|
|
value: editData.value ?? config.value,
|
|
visibility: editData.visibility ?? config.visibility,
|
|
description: editData.description ?? config.description,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<ManagePage<SystemConfig>
|
|
title={t('pageTitle')}
|
|
icon={ShieldCheck}
|
|
data={configs}
|
|
loading={loading}
|
|
error={error}
|
|
onReload={() => refetchSystemConfigs(activeTab)}
|
|
getInitialEditData={getInitialEditData}
|
|
onSave={handleSave}
|
|
getId={(config) => config.key}
|
|
emptyDescription={t('emptyDescription')}
|
|
loadingDescription={t('loadingDescription')}
|
|
headerExtra={
|
|
<div
|
|
role='group'
|
|
aria-label={t('configType')}
|
|
className='grid w-[180px] grid-cols-2 h-8 rounded-md border border-input bg-muted/40 p-0.5'
|
|
>
|
|
{(['business', 'system'] as const).map((tab) => (
|
|
<button
|
|
key={tab}
|
|
type='button'
|
|
onClick={() => setActiveTab(tab)}
|
|
aria-pressed={activeTab === tab}
|
|
className={cn(
|
|
'h-full rounded-sm text-[11px] font-medium transition-colors',
|
|
activeTab === tab
|
|
? 'bg-background shadow-sm text-foreground'
|
|
: 'text-muted-foreground hover:text-foreground',
|
|
)}
|
|
>
|
|
{tab === 'business' ? t('businessConfig') : t('systemConfig')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
}
|
|
columns={[
|
|
{
|
|
header: t('configKey'),
|
|
cell: (item) => (
|
|
<span className='font-mono font-medium'>{item.key}</span>
|
|
),
|
|
width: '200px',
|
|
},
|
|
{
|
|
header: t('configValue'),
|
|
cell: (item) => (
|
|
<span
|
|
className='truncate max-w-[120px] inline-block'
|
|
title={item.value}
|
|
>
|
|
{item.value}
|
|
</span>
|
|
),
|
|
width: '120px',
|
|
},
|
|
{
|
|
header: t('publicVisibility'),
|
|
cell: (item) => (
|
|
<span>{item.visibility === 1 ? t('visible') : t('invisible')}</span>
|
|
),
|
|
width: '80px',
|
|
},
|
|
{
|
|
header: t('description'),
|
|
cell: (item) => (
|
|
<span
|
|
className='truncate max-w-[200px] inline-block text-muted-foreground'
|
|
title={item.description}
|
|
>
|
|
{item.description}
|
|
</span>
|
|
),
|
|
width: '200px',
|
|
},
|
|
]}
|
|
renderDetail={({
|
|
selected,
|
|
hovered,
|
|
editData,
|
|
onEditDataChange,
|
|
onSave,
|
|
saving,
|
|
}) => (
|
|
<SystemConfigDetailPanel
|
|
config={selected || hovered}
|
|
editData={editData}
|
|
onEditDataChange={onEditDataChange}
|
|
onSave={onSave}
|
|
saving={saving}
|
|
/>
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default function SystemConfigPage() {
|
|
return (
|
|
<AdminProvider>
|
|
<SystemConfigs />
|
|
</AdminProvider>
|
|
);
|
|
}
|