Merge branch 'fork' of 124-template-94/d8d-admin-mobile-starter-public into main

This commit is contained in:
2025-05-13 04:05:46 +00:00
committed by Gogs
4 changed files with 238 additions and 17 deletions

7
HISTORY.md Normal file
View File

@@ -0,0 +1,7 @@
待实现
迁移管理页面在正式环境中需要验证env中配置的密码参数才能打开
2025.05.13 0.1.0
首页添加了迁移管理入口按钮, 无需登录即可访问
打开迁移管理页面时,将迁移历史读取出来

View File

@@ -0,0 +1,171 @@
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 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%' }}>
<Button
type="primary"
onClick={runMigrations}
loading={loading}
disabled={loading}
>
</Button>
{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>
);

View File

@@ -354,6 +354,15 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
>
</a>
{/* 迁移管理入口按钮 */}
<a
href="/migrations"
className="w-full flex justify-center py-3 px-4 border border-red-600 rounded-md shadow-sm text-lg font-medium text-red-600 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
</a>
</div>
</div>
</div>
@@ -366,21 +375,25 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
return (c: HonoContext) => {
const isProd = GLOBAL_CONFIG.ENV === 'production';
const isLocalDeploy = Deno.env.get('IS_LOCAL_DEPLOY') === 'true';
return c.html(
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{isProd && <meta name="version" content={Deno.env.get('VERSION') || '0.1.0'} />}
<title>{title}</title>
{isProd ? (
{isLocalDeploy ? (
<script type="module" src={scriptConfig.prodSrc || `/client_dist/${scriptConfig.prodPath}`}></script>
) : (
<script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson} refresh={scriptConfig.refresh}></script>
<script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson}
{...isProd ? {}:{ refresh: true }}
></script>
)}
{isProd ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
{isLocalDeploy ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
<script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
@@ -436,14 +449,31 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
}, GLOBAL_CONFIG.APP_NAME))
honoApp.get('/mobile/*', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/mobile/mobile_app.tsx",
denoJson: "/deno.json",
refresh: true,
src: "https://esm.d8d.fun/xb",
href: "/client/mobile/mobile_app.tsx",
denoJson: "/deno.json",
refresh: true,
prodPath: "mobile/mobile_app.js"
}, GLOBAL_CONFIG.APP_NAME))
const staticRoutes = serveStatic({
// 迁移管理路由
honoApp.get('/migrations', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/migrations/migrations_app.tsx",
denoJson: "/deno.json",
refresh: true,
prodPath: "migrations/migrations_app.js"
}, GLOBAL_CONFIG.APP_NAME))
honoApp.get('/migrations/*', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/migrations/migrations_app.tsx",
denoJson: "/deno.json",
refresh: true,
prodPath: "migrations/migrations_app.js"
}, GLOBAL_CONFIG.APP_NAME))
const staticRoutes = serveStatic({
root: moduleDir,
onFound: async (path: string, c: HonoContext) => {
const fileExt = path.split('.').pop()?.toLowerCase()

View File

@@ -7,20 +7,15 @@ import debug from "debug";
const log = {
api: debug("api:migrations"),
};
// 初始化数据库
const initDatabase = async (apiClient: APIClient) => {
log.api('正在执行数据库迁移...')
const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
// log.app('数据库迁移完成 %O',migrationsResult)
log.api('数据库迁移完成')
return migrationsResult
}
export function createMigrationsRoutes(withAuth: WithAuth) {
const migrationsRoutes = new Hono<{ Variables: Variables }>()
migrationsRoutes.get('/', async (c) => {
const apiClient = c.get('apiClient')
const migrationsResult = await initDatabase(apiClient)
log.api('正在执行数据库迁移...')
const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
// log.app('数据库迁移完成 %O',migrationsResult)
const failedResult = migrationsResult?.find((migration) => migration.status === 'failed')
if (failedResult) {
@@ -31,5 +26,23 @@ export function createMigrationsRoutes(withAuth: WithAuth) {
return c.json({ success: true })
})
migrationsRoutes.get('/history', async (c) => {
const apiClient = c.get('apiClient')
log.api('正在执行数据库迁移...')
const MIRGRATIONS_TABLE = 'knex_migrations'
const hasTable = await apiClient.database.schema.hasTable(MIRGRATIONS_TABLE);
let history = []
if(hasTable)
history = await apiClient.database.table(MIRGRATIONS_TABLE).orderBy('id', 'desc')
return c.json({
success: true,
history
})
})
return migrationsRoutes
}