Files
d8d-admin-mobile-starter-pu…/client/migrations/migrations_app.tsx
2025-05-13 13:22:50 +00:00

214 lines
5.5 KiB
TypeScript

import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Button, Space, Alert, Spin, Typography, Table } from 'antd';
import axios from 'axios';
import dayjs from 'dayjs';
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query';
const { Title } = Typography;
// 创建QueryClient实例
const queryClient = new QueryClient();
interface MigrationResponse {
success: boolean;
error?: string;
failedResult?: any;
}
interface MigrationHistory {
id: string;
name: string;
status: string;
timestamp: string;
batch: string;
}
const MigrationsApp: React.FC = () => {
const [loading, setLoading] = useState(false);
const [migrationResult, setMigrationResult] = useState<MigrationResponse | null>(null);
const { data: historyData, isLoading: isHistoryLoading, error: historyError } = useQuery({
queryKey: ['migrations-history'],
queryFn: async () => {
const response = await axios.get('/api/migrations/history');
return response.data.history;
}
});
const runMigrations = async () => {
try {
setLoading(true);
setMigrationResult(null);
const response = await axios.get('/api/migrations');
setMigrationResult(response.data);
} catch (error: any) {
setMigrationResult({
success: false,
error: error.response?.data?.error || '数据库迁移失败',
failedResult: error.response?.data?.failedResult
});
} finally {
setLoading(false);
}
};
const rollbackMigrations = async (all: boolean) => {
try {
setLoading(true);
setMigrationResult(null);
const response = await axios.get(`/api/migrations/rollback?all=${all}`);
setMigrationResult(response.data);
} catch (error: any) {
setMigrationResult({
success: false,
error: error.response?.data?.error || '数据库回滚失败',
failedResult: error.response?.data?.failedResult
});
} finally {
setLoading(false);
}
};
const columns = [
{
title: '迁移名称',
dataIndex: 'name',
key: 'name',
sorter: (a: MigrationHistory, b: MigrationHistory) => a.name.localeCompare(b.name),
},
{
title: '批次',
dataIndex: 'batch',
key: 'batch',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<span style={{ color: status === 'completed' ? 'green' : 'red' }}>
{status === 'completed' ? '已完成' : '失败'}
</span>
)
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: string) => dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
];
return (
<div className="p-4">
<Title level={3}></Title>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space>
<Button
type="primary"
onClick={runMigrations}
loading={loading}
disabled={loading}
>
</Button>
<Button
danger
onClick={() => rollbackMigrations(false)}
loading={loading}
disabled={loading}
>
</Button>
<Button
danger
onClick={() => rollbackMigrations(true)}
loading={loading}
disabled={loading}
>
</Button>
</Space>
<Alert
message="警告"
description="回滚操作将删除数据,请谨慎使用"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
{loading && <Spin tip="迁移执行中..." />}
{migrationResult && (
migrationResult.success ? (
<Alert
message="迁移成功"
type="success"
showIcon
/>
) : (
<Alert
message="迁移失败"
description={
<>
<p>{migrationResult.error}</p>
{migrationResult.failedResult && (
<pre style={{ marginTop: 10 }}>
{JSON.stringify(migrationResult.failedResult, null, 2)}
</pre>
)}
</>
}
type="error"
showIcon
/>
)
)}
<Title level={4}></Title>
{isHistoryLoading ? (
<Spin tip="加载历史记录中..." />
) : historyError ? (
<Alert
message="加载历史记录失败"
description={historyError.message}
type="error"
showIcon
/>
) : (
<Table
columns={columns}
dataSource={historyData}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => `${total} 条记录`,
}}
bordered
className="migration-history-table"
style={{ marginTop: 16 }}
/>
)}
</Space>
</div>
);
};
// 渲染应用
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
<QueryClientProvider client={queryClient}>
<MigrationsApp />
</QueryClientProvider>
);