码桶
发现社区成员的开源项目
CacheManagement.jsx13.5 KB
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { RefreshCw, ToggleLeft, ToggleRight, Trash2, Link, AlertTriangle } from 'lucide-react';
import TabSkeleton from './TabSkeleton';
const CacheManagement = ({ zone, getHeaders, authFetch, t, showToast, openConfirm }) => {
const af = authFetch || fetch;
const [devMode, setDevMode] = useState(null);
const [loading, setLoading] = useState(true);
const [fetchError, setFetchError] = useState(null);
const [toggling, setToggling] = useState(false);
const [purgingAll, setPurgingAll] = useState(false);
const [purgingUrls, setPurgingUrls] = useState(false);
const [urlText, setUrlText] = useState('');
const countdownRef = useRef(null);
const [countdown, setCountdown] = useState(0);
const fetchStatus = useCallback(async () => {
setLoading(true);
setFetchError(null);
try {
const res = await af(`/api/zones/${zone.id}/cache`, { headers: getHeaders() });
const data = await res.json();
if (data.success) {
if (data.development_mode) {
setDevMode(data.development_mode);
setCountdown(data.development_mode.time_remaining || 0);
}
} else {
const msg = data.errors?.[0]?.message || data.error || t('errorOccurred');
setFetchError(msg);
}
} catch (e) {
console.error('Failed to fetch cache status:', e);
setFetchError(t('errorOccurred'));
}
setLoading(false);
}, [zone.id]);
useEffect(() => {
fetchStatus();
}, [fetchStatus]);
// Countdown timer for development mode
useEffect(() => {
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
if (devMode?.value === 'on' && countdown > 0) {
countdownRef.current = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(countdownRef.current);
countdownRef.current = null;
// Auto-refresh status when countdown ends
fetchStatus();
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => {
if (countdownRef.current) clearInterval(countdownRef.current);
};
}, [devMode?.value, countdown, fetchStatus]);
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
};
const handleToggleDevMode = async () => {
const newValue = devMode?.value === 'on' ? 'off' : 'on';
setToggling(true);
try {
const res = await af(`/api/zones/${zone.id}/cache`, {
method: 'POST',
headers: getHeaders(true),
body: JSON.stringify({ action: 'toggle_dev_mode', value: newValue })
});
const data = await res.json();
if (data.success) {
setDevMode(data.development_mode);
setCountdown(data.development_mode?.time_remaining || 0);
showToast(t(newValue === 'on' ? 'devModeEnabled' : 'devModeDisabled'));
} else {
showToast(data.errors?.[0]?.message || t('errorOccurred'), 'error');
}
} catch (e) {
showToast(t('errorOccurred'), 'error');
}
setToggling(false);
};
const handlePurgeAll = () => {
openConfirm(t('confirmTitle'), t('cachePurgeAllConfirm'), async () => {
setPurgingAll(true);
try {
const res = await af(`/api/zones/${zone.id}/cache`, {
method: 'POST',
headers: getHeaders(true),
body: JSON.stringify({ action: 'purge_all' })
});
const data = await res.json();
if (data.success) {
showToast(t('cachePurgeAllSuccess'));
} else {
showToast(data.errors?.[0]?.message || t('errorOccurred'), 'error');
}
} catch (e) {
showToast(t('errorOccurred'), 'error');
}
setPurgingAll(false);
});
};
const handlePurgeUrls = async () => {
const urls = urlText.split('\n').map(u => u.trim()).filter(u => u.length > 0);
if (urls.length === 0) {
showToast(t('cacheUrlsEmpty'), 'error');
return;
}
if (urls.length > 30) {
showToast(t('cacheUrlsLimit'), 'error');
return;
}
setPurgingUrls(true);
try {
const res = await af(`/api/zones/${zone.id}/cache`, {
method: 'POST',
headers: getHeaders(true),
body: JSON.stringify({ action: 'purge_urls', urls })
});
const data = await res.json();
if (data.success) {
showToast(t('cachePurgeUrlsSuccess').replace('{count}', data.purged_count || urls.length));
setUrlText('');
} else {
showToast(data.errors?.[0]?.message || t('errorOccurred'), 'error');
}
} catch (e) {
showToast(t('errorOccurred'), 'error');
}
setPurgingUrls(false);
};
const urlCount = urlText.split('\n').map(u => u.trim()).filter(u => u.length > 0).length;
const isDevModeOn = devMode?.value === 'on';
if (loading && !devMode && !fetchError) {
return <TabSkeleton variant="cache" />;
}
return (
<div className="tab-enter" style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{fetchError && (
<div style={{
display: 'flex', flexDirection: 'column', gap: '6px',
padding: '0.75rem 1rem', borderRadius: '8px',
background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.3)',
fontSize: '0.8125rem', color: 'var(--error, #ef4444)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<AlertTriangle size={16} />
<span>{t('loadSettingsError').replace('{error}', fetchError)}</span>
</div>
{/auth/i.test(fetchError) && (
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{t('tokenPermissionHint')}
</span>
)}
<button
className="btn btn-outline"
onClick={fetchStatus}
style={{ alignSelf: 'flex-start', padding: '4px 12px', fontSize: '0.75rem', marginTop: '4px' }}
>
<RefreshCw size={12} /> {t('refresh') || 'Retry'}
</button>
</div>
)}
{/* Development Mode */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<h3 style={{ fontSize: '0.875rem', margin: 0 }}>{t('cacheDevMode')}</h3>
{!loading && (
<span className={`badge ${isDevModeOn ? 'badge-orange' : 'badge-green'}`} style={{ fontSize: '0.65rem' }}>
{isDevModeOn ? 'ON' : 'OFF'}
</span>
)}
</div>
<button
className="btn btn-outline"
onClick={fetchStatus}
disabled={loading}
style={{ padding: '4px 8px', height: 'auto', fontSize: '0.75rem' }}
>
<RefreshCw size={12} className={loading ? 'spin' : ''} />
</button>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.75rem', lineHeight: 1.5 }}>
{t('cacheDevModeDesc')}
</p>
{isDevModeOn && countdown > 0 && (
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '0.5rem 0.75rem', borderRadius: '6px',
background: 'rgba(245, 158, 11, 0.1)', border: '1px solid rgba(245, 158, 11, 0.3)',
marginBottom: '0.75rem', fontSize: '0.75rem', color: '#f59e0b'
}}>
<AlertTriangle size={14} />
<span>{t('cacheDevModeExpires').replace('{time}', formatTime(countdown))}</span>
</div>
)}
<button
className="btn"
onClick={handleToggleDevMode}
disabled={toggling || loading}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: isDevModeOn ? 'var(--error)' : 'var(--primary)',
color: 'white', border: 'none',
padding: '0.5rem 1rem', fontSize: '0.8125rem'
}}
>
{toggling ? (
<RefreshCw size={14} className="spin" />
) : isDevModeOn ? (
<ToggleRight size={14} />
) : (
<ToggleLeft size={14} />
)}
{isDevModeOn ? t('cacheDevModeOff') : t('cacheDevModeOn')}
</button>
</div>
{/* Purge All Cache */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<h3 style={{ fontSize: '0.875rem', margin: '0 0 0.5rem 0' }}>{t('cachePurgeAll')}</h3>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.75rem', lineHeight: 1.5 }}>
{t('cachePurgeAllDesc')}
</p>
<button
className="btn"
onClick={handlePurgeAll}
disabled={purgingAll}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: 'var(--error)', color: 'white', border: 'none',
padding: '0.5rem 1rem', fontSize: '0.8125rem'
}}
>
{purgingAll ? <RefreshCw size={14} className="spin" /> : <Trash2 size={14} />}
{t('cachePurgeAllBtn')}
</button>
</div>
{/* Purge by URL */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<h3 style={{ fontSize: '0.875rem', margin: '0 0 0.5rem 0' }}>{t('cachePurgeUrls')}</h3>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.75rem', lineHeight: 1.5 }}>
{t('cachePurgeUrlsDesc')}
</p>
<textarea
value={urlText}
onChange={(e) => setUrlText(e.target.value)}
placeholder={t('cachePurgeUrlsPlaceholder')}
rows={5}
style={{
width: '100%', resize: 'vertical', fontFamily: 'monospace',
fontSize: '0.8125rem', padding: '0.5rem 0.75rem',
borderRadius: '6px', border: '1px solid var(--border)',
background: 'var(--input-bg, white)', color: 'var(--text)',
marginBottom: '0.5rem', boxSizing: 'border-box'
}}
/>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
<span style={{
fontSize: '0.7rem',
color: urlCount > 30 ? 'var(--error)' : 'var(--text-muted)'
}}>
{urlCount} / 30 URLs
</span>
{urlCount > 30 && (
<span style={{ fontSize: '0.7rem', color: 'var(--error)' }}>
{t('cacheUrlsLimit')}
</span>
)}
</div>
<button
className="btn btn-primary"
onClick={handlePurgeUrls}
disabled={purgingUrls || urlCount === 0 || urlCount > 30}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '0.5rem 1rem', fontSize: '0.8125rem'
}}
>
{purgingUrls ? <RefreshCw size={14} className="spin" /> : <Link size={14} />}
{t('cachePurgeUrlsBtn')}
</button>
</div>
</div>
);
};
export default CacheManagement;