码桶

发现社区成员的开源项目

main
cloudflare-dns-manager/src/components/ScheduledChangesModal.jsx
ScheduledChangesModal.jsx8.1 KB
import React, { useState, useEffect, useRef } from 'react';
import { Clock, Trash2, RefreshCw, X, AlertCircle } from 'lucide-react';
import { ApiClient, ApiError } from '../utils/api.js';
import { useAuth } from '../contexts/AuthContext.jsx';
import { useToast } from '../contexts/ToastContext.jsx';
import { useTheme } from '../contexts/ThemeContext.jsx';

const ScheduledChangesModal = ({ show, onClose }) => {
    const { auth } = useAuth();
    const { showToast } = useToast();
    const { t } = useTheme();
    const [changes, setChanges] = useState([]);
    const [loading, setLoading] = useState(false);
    const authRef = useRef(auth);
    authRef.current = auth;

    const apiRef = useRef(null);
    if (!apiRef.current) {
        apiRef.current = new ApiClient(() => authRef.current);
    }

    const fetchChanges = async () => {
        setLoading(true);
        try {
            const data = await apiRef.current.get('/api/scheduled-changes');
            setChanges(data.changes || []);
        } catch (err) {
            if (err instanceof ApiError) {
                console.error(`Failed to fetch scheduled changes (${err.status}):`, err.message);
            } else {
                console.error('Failed to fetch scheduled changes:', err);
            }
        }
        setLoading(false);
    };

    useEffect(() => {
        if (show) {
            fetchChanges();
        }
    }, [show]);

    const handleCancel = async (id) => {
        try {
            await apiRef.current.del(`/api/scheduled-changes?id=${id}`);
            showToast(t('scheduleCancelled'));
            setChanges(prev => prev.filter(c => c.id !== id));
        } catch (err) {
            if (err instanceof ApiError) {
                showToast(err.message || t('errorOccurred'), 'error');
            } else {
                showToast(t('errorOccurred'), 'error');
            }
        }
    };

    const formatDate = (iso) => {
        try {
            const d = new Date(iso);
            return d.toLocaleString();
        } catch {
            return iso;
        }
    };

    const actionLabel = (action) => {
        switch (action) {
            case 'create': return t('scheduleActionCreate');
            case 'update': return t('scheduleActionUpdate');
            case 'delete': return t('scheduleActionDelete');
            default: return action;
        }
    };

    if (!show) return null;

    return (
        <div
            className="modal-overlay"
            onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
        >
            <div className="glass-card fade-in modal-content" role="dialog" aria-label={t('scheduledChanges')} style={{ padding: '2rem', maxWidth: '560px', width: '90%', position: 'relative' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
                    <h2 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
                        <Clock size={20} />
                        {t('scheduledChanges')}
                    </h2>
                    <button
                        onClick={onClose}
                        style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '4px', display: 'flex' }}
                        aria-label="Close"
                    >
                        <X size={18} color="var(--text-muted)" />
                    </button>
                </div>

                {loading ? (
                    <div style={{ textAlign: 'center', padding: '2rem' }}>
                        <RefreshCw className="spin" size={24} style={{ color: 'var(--primary)' }} />
                    </div>
                ) : changes.length === 0 ? (
                    <div style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-muted)' }}>
                        <AlertCircle size={32} style={{ marginBottom: '0.5rem', opacity: 0.5 }} />
                        <p style={{ fontSize: '0.875rem' }}>{t('noScheduledChanges')}</p>
                    </div>
                ) : (
                    <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
                        {changes.map(change => (
                            <div key={change.id} style={{
                                padding: '0.75rem',
                                borderRadius: '8px',
                                background: 'var(--hover-bg)',
                                marginBottom: '0.5rem',
                                border: '1px solid var(--border)'
                            }}>
                                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '8px' }}>
                                    <div style={{ flex: 1, minWidth: 0 }}>
                                        <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px', flexWrap: 'wrap' }}>
                                            <span className={`badge ${change.action === 'create' ? 'badge-green' : change.action === 'delete' ? 'badge-orange' : 'badge-blue'}`} style={{ fontSize: '0.65rem', padding: '1px 6px' }}>
                                                {actionLabel(change.action)}
                                            </span>
                                            {change.record && change.record.type && (
                                                <span className="badge badge-blue" style={{ fontSize: '0.65rem', padding: '1px 6px' }}>
                                                    {change.record.type}
                                                </span>
                                            )}
                                            <span style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text)' }}>
                                                {change.zoneName || change.zoneId}
                                            </span>
                                        </div>
                                        {change.record && (
                                            <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '4px' }}>
                                                {change.record.name}{change.record.content ? ` \u2192 ${change.record.content}` : ''}
                                            </div>
                                        )}
                                        <div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.7rem', color: 'var(--text-muted)' }}>
                                            <Clock size={11} />
                                            {t('scheduledFor')}: {formatDate(change.scheduledAt)}
                                        </div>
                                    </div>
                                    <button
                                        onClick={() => handleCancel(change.id)}
                                        style={{
                                            border: 'none', background: 'transparent', cursor: 'pointer',
                                            padding: '4px', display: 'flex', flexShrink: 0,
                                            color: 'var(--text-muted)', transition: 'color 0.2s'
                                        }}
                                        onMouseEnter={e => e.currentTarget.style.color = 'var(--error)'}
                                        onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}
                                        title={t('cancelSchedule')}
                                        aria-label={t('cancelSchedule')}
                                    >
                                        <Trash2 size={15} />
                                    </button>
                                </div>
                            </div>
                        ))}
                    </div>
                )}

                <div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'flex-end' }}>
                    <button className="btn btn-outline" onClick={onClose}>{t('cancel')}</button>
                </div>
            </div>
        </div>
    );
};

export default ScheduledChangesModal;