'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { activateConfigVersion, getActiveConfigVersion, getConfigVersionDiff, getConfigVersionPreview, getConfigVersions, publishConfigVersion, cleanupConfigVersions, } from '@/features/config-versions/api/config-versions'; import { ConfigVersionSnapshotModal } from '@/features/config-versions/components/config-version-snapshot-modal'; import { CleanupConfigVersionsModal } from '@/features/config-versions/components/cleanup-config-versions-modal'; import type { ConfigOptionDiffItem, ConfigDiffResult, ConfigPreviewResult, ConfigVersionDetail, ConfigVersionSummary, SupportFile, } from '@/features/config-versions/types'; import { CodeBlock, PrimaryButton, SecondaryButton, } from '@/features/shared/components/resource-primitives'; import { formatDateTime } from '@/lib/utils/date'; const versionsQueryKey = ['config-versions']; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function truncateChecksum(checksum: string) { if (!checksum) { return '—'; } return checksum.length > 16 ? `${checksum.slice(0, 16)}...` : checksum; } function hasConfigDiff(diff: ConfigDiffResult) { return ( diff.added_domains.length > 0 || diff.removed_domains.length > 0 || diff.modified_domains.length > 0 || diff.main_config_changed || diff.changed_option_keys.length > 0 || !diff.active_version ); } function DiffList({ title, items }: { title: string; items: string[] }) { return (

{title}

0 ? 'info' : 'warning'} />
{items.length > 0 ? (
{items.map((item) => ( {item} ))}
) : (

当前无相关变更。

)}
); } function SupportFilesList({ files }: { files: SupportFile[] }) { if (files.length === 0) { return (

当前发布不需要额外支持文件。

); } return (
{files.map((file) => (
{file.path} {file.content}
))}
); } function renderOptionValue(value: string) { return value === '' ? '空' : value; } function OptionDiffTable({ items }: { items: ConfigOptionDiffItem[] }) { if (items.length === 0) { return (

当前无 OpenResty 性能参数变化。

); } return (
{items.map((item) => ( ))}
参数 当前激活值 待发布值
{item.key} {renderOptionValue(item.previous_value)} {renderOptionValue(item.current_value)}
); } function PublishPreviewCard({ preview, diff, activeVersionMeta, activeVersionDetail, isActiveVersionDetailLoading, activeVersionDetailError, isPublishing, onConfirm, onCancel, }: { preview: ConfigPreviewResult; diff: ConfigDiffResult; activeVersionMeta: ConfigVersionSummary | null; activeVersionDetail: ConfigVersionDetail | null; isActiveVersionDetailLoading: boolean; activeVersionDetailError: string | null; isPublishing: boolean; onConfirm: () => void; onCancel: () => void; }) { const canPublish = preview.route_count > 0 && hasConfigDiff(diff); return ( } >

当前激活版本

{diff.active_version || '无'}

新增域名

{diff.added_domains.length}

删除域名

{diff.removed_domains.length}

修改域名

{diff.modified_domains.length}

主配置变化

{diff.main_config_changed ? '已变化' : '无变化'}

{!canPublish ? ( ) : null}

OpenResty 参数变化

0 ? 'info' : 'warning'} />
{diff.main_config_changed && activeVersionMeta ? (

Current Active Main Config

{isActiveVersionDetailLoading ? ( ) : activeVersionDetailError ? ( ) : activeVersionDetail ? ( {activeVersionDetail.main_config} ) : ( )}

Pending Main Config

{preview.main_config}
) : null} {/* Legacy duplicated main config preview block kept commented out while we replace it with an explicit active-vs-pending comparison above. {diff.main_config_changed && activeVersion ? (

褰撳墠婵€娲讳富閰嶇疆

{activeVersion.main_config}

寰呭彂甯冧富閰嶇疆

Checksum锛歿preview.checksum}

{preview.main_config}
) : null} */}

主配置

Checksum:{preview.checksum}

{preview.main_config}

路由配置

{preview.rendered_config}

支持文件

{isPublishing ? '发布中...' : '确认发布'} 取消预览
); } export function ConfigVersionsPage() { const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [selectedVersionId, setSelectedVersionId] = useState( null, ); const [publishPreview, setPublishPreview] = useState<{ preview: ConfigPreviewResult; diff: ConfigDiffResult; } | null>(null); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [isCleanupModalOpen, setIsCleanupModalOpen] = useState(false); const versionsQuery = useQuery({ queryKey: versionsQueryKey, queryFn: getConfigVersions, }); const versions = useMemo( () => versionsQuery.data ?? [], [versionsQuery.data], ); const activeVersion = useMemo( () => versions.find((item) => item.is_active) ?? null, [versions], ); const selectedVersion = useMemo( () => versions.find((item) => item.id === selectedVersionId) ?? null, [selectedVersionId, versions], ); const shouldLoadActiveVersionDetail = Boolean( activeVersion?.id && publishPreview?.diff.main_config_changed, ); const activeVersionDetailQuery = useQuery({ queryKey: ['config-versions', 'active-detail', activeVersion?.id ?? 0], queryFn: getActiveConfigVersion, enabled: shouldLoadActiveVersionDetail, }); const publishMutation = useMutation({ mutationFn: (force?: boolean) => publishConfigVersion(force), onSuccess: async (version) => { setFeedback({ tone: 'success', message: `发布成功,版本 ${version.version}`, }); setPublishPreview(null); await queryClient.invalidateQueries({ queryKey: versionsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const activateMutation = useMutation({ mutationFn: activateConfigVersion, onSuccess: async (version) => { setFeedback({ tone: 'success', message: `已激活版本 ${version.version}`, }); setSelectedVersionId(version.id); await queryClient.invalidateQueries({ queryKey: versionsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const cleanupMutation = useMutation({ mutationFn: cleanupConfigVersions, onSuccess: async (data) => { setFeedback({ tone: 'success', message: `成功清理 ${data.deleted_count} 个历史快照`, }); setIsCleanupModalOpen(false); await queryClient.invalidateQueries({ queryKey: versionsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const handleOpenPublishPreview = async () => { setFeedback(null); setIsPreviewLoading(true); try { const [preview, diff] = await Promise.all([ getConfigVersionPreview(), getConfigVersionDiff(), ]); setPublishPreview({ preview, diff }); } catch (error) { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); } finally { setIsPreviewLoading(false); } }; const handleActivate = (version: ConfigVersionSummary) => { if (version.is_active) { return; } if (!window.confirm(`确认激活版本 ${version.version} 吗?`)) { return; } setFeedback(null); activateMutation.mutate(version.id); }; return ( <>
{ if ( window.confirm( '确认强制重新发布吗?这会忽略配置变化检查并立即生成一个新版本。', ) ) { publishMutation.mutate(true); } }} disabled={publishMutation.isPending} > 重新发布 {isPreviewLoading ? '加载预览中...' : '预览并发布'}
} /> {feedback ? ( ) : null} {publishPreview ? ( publishMutation.mutate(false)} onCancel={() => setPublishPreview(null)} /> ) : null} setIsCleanupModalOpen(true)} > 清理旧版本 void queryClient.invalidateQueries({ queryKey: versionsQueryKey, }) } > 刷新列表 } > {versionsQuery.isLoading ? ( ) : versionsQuery.isError ? ( ) : versions.length === 0 ? ( ) : (
{versions.map((version) => ( ))}
版本号 状态 创建人 Checksum 创建时间 操作
{version.version} {version.created_by || '系统'} {truncateChecksum(version.checksum)} {formatDateTime(version.created_at)}
setSelectedVersionId(version.id)} className="px-3 py-2 text-xs" > 查看快照 {!version.is_active ? ( handleActivate(version)} disabled={activateMutation.isPending} className="px-3 py-2 text-xs" > 重新激活 ) : null}
)}
setSelectedVersionId(null)} /> setIsCleanupModalOpen(false)} onConfirm={(keepCount) => cleanupMutation.mutate({ keep_count: keepCount }) } isPending={cleanupMutation.isPending} /> ); }