Bladeren bron

✨ feat(project): 重构项目为SSR架构并升级技术栈

- 迁移项目从Cloudflare Workers到Node.js SSR架构
- 新增index.html入口文件和server.js Node.js服务器
- 升级所有依赖包到最新版本(Vite 7、React 19、Hono 4.8等)
- 移除Cloudflare相关依赖,添加Express兼容的中间件支持
- 增强CRUD服务支持用户跟踪、关系查询和高级筛选功能
- 重构数据渲染逻辑,支持SSR流式渲染和客户端水合
- 优化构建配置,支持分客户端/服务端/SSR多入口构建
yourname 4 maanden geleden
bovenliggende
commit
de5d3fd40d

+ 18 - 0
index.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link href='/src/style.css' rel="stylesheet" />
+  <title>Vite + React + TS</title>
+  <!--app-head-->
+</head>
+
+<body>
+  <div id="root"><!--app-html--></div>
+  <script type="module" src="/src/client/index.tsx"></script>
+</body>
+
+</html>

+ 35 - 39
package.json

@@ -1,58 +1,54 @@
 {
-  "name": "monitor",
+  "name": "154-137-template-6",
   "private": true,
+  "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "export NODE_ENV='development' && export DEBUG=backend:* && vite",
-    "build": "export NODE_ENV='production' && vite build && vite build --ssr",
-    "start": "export NODE_ENV='production' && node dist-server/index.js"
+    "dev": "PORT=8080 node server",
+    "build": "npm run build:client && npm run build:server && npm run build:api",
+    "build:client": "vite build --outDir dist/client --manifest",
+    "build:server": "vite build --ssr src/server/index.tsx --outDir dist/server",
+    "build:api": "vite build --ssr src/server/api.ts --outDir dist/api",
+    "preview": "PORT=8080 cross-env NODE_ENV=production node server"
   },
   "dependencies": {
     "@ant-design/icons": "^6.0.0",
-    "@emotion/react": "^11.14.0",
     "@heroicons/react": "^2.2.0",
-    "@hono/node-server": "^1.14.3",
+    "@hono/node-server": "^1.17.1",
     "@hono/react-renderer": "^1.0.1",
-    "@hono/swagger-ui": "^0.5.1",
-    "@hono/vite-dev-server": "^0.19.1",
-    "@hono/zod-openapi": "^0.19.7",
-    "@hono/zod-validator": "^0.4.3",
-    "@tanstack/react-query": "^5.77.2",
-    "@types/bcrypt": "^5.0.2",
-    "@types/jsonwebtoken": "^9.0.9",
-    "antd": "^5.26.0",
-    "axios": "^1.9.0",
+    "@hono/swagger-ui": "^0.5.2",
+    "@hono/zod-openapi": "^1.0.2",
+    "@tanstack/react-query": "^5.83.0",
+    "antd": "^5.26.6",
+    "axios": "^1.11.0",
     "bcrypt": "^6.0.0",
+    "compression": "^1.8.0",
     "dayjs": "^1.11.13",
     "debug": "^4.4.1",
-    "dotenv": "^16.5.0",
-    "formdata-node": "^6.0.3",
-    "hono": "^4.7.11",
-    "ioredis": "^5.6.1",
+    "express": "^5.1.0",
+    "hono": "^4.8.5",
     "jsonwebtoken": "^9.0.2",
-    "minio": "^8.0.5",
-    "mysql2": "^3.14.1",
-    "node-fetch": "^3.3.2",
+    "mysql2": "^3.14.2",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
-    "react-hook-form": "^7.57.0",
-    "react-i18next": "^15.5.2",
-    "react-router": "^7.6.1",
-    "react-router-dom": "^7.6.1",
-    "react-toastify": "^11.0.5",
+    "react-hook-form": "^7.61.1",
+    "react-router": "^7.7.0",
+    "react-router-dom": "^7.7.0",
     "reflect-metadata": "^0.2.2",
-    "typeorm": "^0.3.24"
+    "sirv": "^3.0.1",
+    "typeorm": "^0.3.25"
   },
   "devDependencies": {
-    "@types/debug": "^4.1.12",
-    "@types/node": "^22.15.23",
-    "@types/node-cron": "^3.0.11",
-    "@types/react": "^19.1.1",
-    "@types/react-dom": "^19.1.2",
-    "@types/three": "^0.177.0",
-    "hono-vite-react-stack-node": "^0.2.1",
-    "node-cron": "^4.1.0",
-    "tailwindcss": "^4.1.3",
-    "vite": "^6.3.5"
+    "@tailwindcss/vite": "^4.1.11",
+    "@types/express": "^5.0.3",
+    "@types/node": "^24.0.10",
+    "@types/react": "^19.1.8",
+    "@types/react-dom": "^19.1.6",
+    "@vitejs/plugin-react-swc": "^3.10.2",
+    "cross-env": "^7.0.3",
+    "tailwindcss": "^4.1.11",
+    "tsx": "^4.20.3",
+    "typescript": "~5.8.3",
+    "vite": "^7.0.0"
   }
-}
+}

File diff suppressed because it is too large
+ 142 - 693
pnpm-lock.yaml


+ 1 - 0
public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 345 - 0
server.js

@@ -0,0 +1,345 @@
+import fs from 'node:fs/promises';
+import { URL } from 'node:url';
+import { Transform } from 'node:stream';
+import { Readable } from 'node:stream';
+import { pipeline } from 'node:stream/promises';
+import { Hono } from 'hono';
+import { logger } from 'hono/logger'; // 引入 Hono 日志中间件
+import { createServer as createNodeServer } from 'node:http';
+import process from 'node:process';
+import { createAdaptorServer } from '@hono/node-server'  
+
+
+// 创建 Hono 应用
+const app = new Hono();// API路由
+
+// 全局使用 Hono 日志中间件(记录所有请求)
+app.use('*', logger()); // 新增:添加请求日志中间件
+
+// 常量定义
+const isProduction = process.env.NODE_ENV === 'production';
+const port = process.env.PORT || 8080;
+const base = process.env.BASE || '/';
+const ABORT_DELAY = 10000;
+
+console.log('========================================');
+console.log('开始初始化服务器...');
+console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
+console.log(`端口: ${port}`);
+console.log(`基础路径: ${base}`);
+console.log('========================================');
+
+// 解析基础路径为 URL 对象,方便后续处理
+const baseUrl = new URL(base, `http://localhost:${port}`);
+console.log(`基础URL解析完成: ${baseUrl.href}`);
+
+// 先创建服务器实例  
+console.log('正在创建服务器实例...');
+const parentServer = createAdaptorServer({  
+  fetch: app.fetch,  
+  createServer: createNodeServer,  
+  port: port,  
+})  
+console.log('服务器实例创建成功');
+
+// 生产环境中间件
+let compressionMiddleware;
+let sirvMiddleware;
+if (isProduction) {
+  console.log('生产环境: 加载压缩和静态文件中间件...');
+  compressionMiddleware = (await import('compression')).default();
+  sirvMiddleware = (await import('sirv')).default('./dist/client', { 
+    extensions: [],
+    baseUrl: base 
+  });
+  console.log('生产环境中间件加载完成');
+}
+
+// Vite 开发服务器
+/** @type {import('vite').ViteDevServer | undefined} */
+let vite;
+if (!isProduction) {
+  console.log('开发环境: 初始化 Vite 开发服务器...');
+  const { createServer } = await import('vite');
+  vite = await createServer({
+    server: { middlewareMode: {
+        server: parentServer
+      },
+      hmr: {  
+        port: 8081,
+        clientPort: 443, // 或其他可用端口  
+        path: 'vite-hmr'
+      },
+      proxy: {
+        '/vite-hmr': {
+          target: 'ws://localhost:8081',
+          // 代理 WebSocket
+          ws: true,
+        },
+      },
+    },
+    appType: 'custom',
+    base,
+  });
+  console.log('Vite 开发服务器初始化完成');
+}
+
+// 加载 API 路由
+if (!isProduction) {
+  console.log('开发环境: 从 Vite 加载 API 路由...');
+  const apiModule = await vite.ssrLoadModule('./src/server/api.ts');
+  app.route('/', apiModule.default);
+  console.log('API 路由加载完成');
+}else{
+  console.log('生产环境: 加载编译后的 API 路由...');
+  const api = (await import('./dist/api/api.js')).default
+  app.route('/', api);
+  console.log('API 路由加载完成');
+}
+
+// 请求处理中间件 - 通用逻辑
+app.use(async (c, next) => {
+  try {
+    // 使用 c.env 获取原生请求响应对象
+    const req = c.env.incoming;
+    const res = c.env.outgoing;
+    const url = new URL(req.url, `http://${req.headers.host}`);
+    const path = url.pathname;
+
+    // 检查是否匹配基础路径
+    if (!path.startsWith(baseUrl.pathname)) {
+      return c.text('未找到', 404);
+    }
+
+    // 开发环境:使用 Vite 中间件
+    if (!isProduction && vite) {
+      // 使用 Vite 中间件处理请求
+      const handled = await new Promise((resolve) => {
+        vite.middlewares(req, res, () => resolve(false));
+      });
+      
+      // 如果 Vite 中间件已经处理了请求,直接返回
+      if (handled) {
+        return c.body;
+      }
+    } 
+    // 生产环境:使用 compression 和 sirv 中间件
+    else if (isProduction) {
+      // 先尝试 compression 中间件
+      const compressed = await new Promise((resolve) => {
+        compressionMiddleware(req, res, () => resolve(false));
+      });
+      
+      if (compressed) {
+        return c.body;
+      }
+      
+      // 再尝试 sirv 中间件处理静态文件
+      const served = await new Promise((resolve) => {
+        sirvMiddleware(req, res, () => resolve(false));
+      });
+      
+      if (served) {
+        return c.body;
+      }
+    }
+
+    await next()
+  } catch (e) {
+    if (!isProduction && vite) {
+      vite.ssrFixStacktrace(e);
+    }
+    console.error('请求处理中间件错误:', e.stack);
+    return c.text('服务器内部错误', 500);
+  }
+});
+
+// 请求处理中间件 - SSR 渲染逻辑
+app.use(async (c) => {
+  
+  try {
+    // 使用 c.env 获取原生请求响应对象
+    const req = c.env.incoming;
+    const res = c.env.outgoing;
+    const url = new URL(req.url, `http://${req.headers.host}`);
+    const path = url.pathname;
+
+    // 检查是否匹配基础路径
+    if (!path.startsWith(baseUrl.pathname)) {
+      return c.text('未找到', 404);
+    }
+
+    // 处理基础路径
+    const normalizedUrl = path.replace(baseUrl.pathname, '/') || '/';
+    console.log(`处理请求: ${normalizedUrl}`);
+
+    // 处理所有其他请求的 SSR 逻辑
+    /** @type {string} */
+    let template;
+    /** @type {import('./src/server/index.tsx').render} */
+    let render;
+    
+    if (!isProduction && vite) {
+      console.log('开发环境: 加载模板和渲染函数...');
+      // 开发环境:读取最新模板并转换
+      const module = (await vite.ssrLoadModule('/src/server/index.tsx'));
+      template = module.template;
+      template = await vite.transformIndexHtml(normalizedUrl, template);
+      render = module.render;
+      console.log('开发环境模板处理完成');
+    } else {
+      // 生产环境:使用缓存的模板
+      console.log('生产环境: 加载编译后的模板和渲染函数...');
+      const module = await import('./dist/server/index.js');
+      
+      // 读取 manifest.json 并处理模板
+      try {
+        // 读取 manifest
+        const manifestPath = new URL('./dist/client/.vite/manifest.json', import.meta.url);
+        const manifestContent = await fs.readFile(manifestPath, 'utf-8');
+        const manifest = JSON.parse(manifestContent);
+        console.log('生产环境: 成功读取 manifest.json');
+
+        // 获取 index.html 对应的资源信息
+        const indexManifest = manifest['index.html'];
+        if (!indexManifest) {
+          throw new Error('manifest 中未找到 index.html 入口配置');
+        }
+
+        template = module.template;
+
+        // 替换 CSS 链接
+        const cssLinks = indexManifest.css?.map(cssFile => {
+          // 结合基础路径生成完整 URL(处理 base 前缀)
+          const cssUrl = new URL(cssFile, baseUrl).pathname;
+          return `<link href="${cssUrl}" rel="stylesheet" />`;
+        }).join('\n') || ''; // 无 CSS 则清空
+
+        // 替换入口脚本
+        const jsEntryPath = new URL(indexManifest.file, baseUrl).pathname;
+        const entryScript = `<script type="module" src="${jsEntryPath}"></script>`;
+
+        // 执行替换
+        template = template
+          .replace(/<link href="\/src\/style.css" rel="stylesheet"\/>/, cssLinks)
+          .replace(/<script type="module" src="\/src\/client\/index.tsx"><\/script>/, entryScript);
+
+        console.log('生产环境模板处理完成');
+
+      } catch (err) {
+        console.error('生产环境模板处理失败:', err);
+        throw err; // 终止启动,避免使用错误模板
+      }
+
+      render = module.render;
+    }
+
+    let didError = false;
+    let abortController;
+
+    // 创建一个可读流用于 SSR 渲染内容
+    const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`);
+    const ssrStream = new Readable({ read: () => {} });
+    
+    // 写入 HTML 头部
+    ssrStream.push(htmlStart);
+
+    // 设置响应头和状态码
+    c.header('Content-Type', 'text/html');
+    
+    // 处理渲染
+    const { pipe, abort } = render(normalizedUrl, {
+      onShellError() {
+        didError = true;
+        c.status(500);
+        ssrStream.push('<h1>服务器渲染出错</h1>');
+        ssrStream.push(null); // 结束流
+      },
+      onShellReady() {
+        console.log(`开始渲染页面: ${normalizedUrl}`);
+        // 将渲染结果通过管道传入 ssrStream
+        const transformStream = new Transform({
+          transform(chunk, encoding, callback) {
+            ssrStream.push(chunk, encoding);
+            callback();
+          }
+        });
+
+        pipe(transformStream);
+        
+        // 当 transformStream 完成时,添加 HTML 尾部
+        transformStream.on('finish', () => {
+          ssrStream.push(htmlEnd);
+          ssrStream.push(null); // 结束流
+          console.log(`页面渲染完成: ${normalizedUrl}`);
+        });
+      },
+      onError(error) {
+        didError = true;
+        console.error('渲染过程出错:', error);
+      },
+    });
+
+    // 设置超时中止
+    abortController = new AbortController();
+    const abortTimeout = setTimeout(() => {
+      console.log(`渲染超时,终止请求: ${normalizedUrl}`);
+      abort();
+      abortController.abort();
+    }, ABORT_DELAY);
+
+    // 将流通过 Hono 响应返回
+    return c.body(ssrStream, {
+      onEnd: () => {
+        clearTimeout(abortTimeout);
+      }
+    });
+  } catch (e) {
+    if (!isProduction && vite) {
+      vite.ssrFixStacktrace(e);
+    }
+    console.error('SSR 处理错误:', e.stack);
+    return c.text('服务器内部错误', 500);
+  }
+});
+
+
+// 启动服务器
+console.log('准备启动服务器...');
+parentServer.listen(port, () => {  
+  console.log('========================================');
+  console.log(`服务器已成功启动!`);
+  console.log(`访问地址: http://localhost:${port}`);
+  console.log(`环境: ${isProduction ? '生产环境' : '开发环境'}`);
+  console.log('========================================');
+})
+
+// 统一的服务器关闭处理函数
+const shutdownServer = async () => {
+  console.log('正在关闭服务器...');
+  
+  // 1. 先关闭 Vite 开发服务器(包括 HMR 服务)
+  if (!isProduction && vite) {
+    console.log('正在关闭 Vite 开发服务器(包括 HMR 服务)...');
+    try {
+      await vite.close(); // 关闭 Vite 实例,会自动终止 HMR 服务(8081 端口)
+      console.log('Vite 开发服务器已关闭');
+    } catch (err) {
+      console.error('关闭 Vite 服务器时出错:', err);
+    }
+  }
+  
+  // 2. 关闭主服务器
+  parentServer.close((err) => {
+    if (err) {
+      console.error('关闭主服务器时出错:', err);
+      process.exit(1);
+    }
+    console.log('主服务器已关闭');
+    process.exit(0);
+  });
+};
+
+// 处理进程终止信号(包括 Ctrl+C)
+process.on('SIGINT', shutdownServer);  // 处理 Ctrl+C
+process.on('SIGTERM', shutdownServer); // 处理 kill 命令

File diff suppressed because it is too large
+ 0 - 0
src/assets/react.svg


+ 4 - 4
src/client/index.tsx

@@ -1,6 +1,6 @@
 // 如果当前是在 /big 下
 if (window.location.pathname.startsWith('/admin')) {
-    import('./admin/index')
-  }else{
-    import('./home/index')
-  }
+  import('./admin/index')
+} else {
+  import('./home/index')
+}

+ 21 - 0
src/client/utils/ClientOnly.tsx

@@ -0,0 +1,21 @@
+// components/ClientOnly.tsx
+import { useEffect, useState, ReactNode } from 'react';
+
+interface ClientOnlyProps {
+  // 子组件(仅在客户端渲染)
+  children: ReactNode;
+  // 服务器端渲染时的占位内容(可选,默认 null)
+  fallback?: ReactNode | null;
+}
+
+export default function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
+  const [isClient, setIsClient] = useState<boolean>(false);
+
+  useEffect(() => {
+    // 仅在客户端执行,标记为客户端环境
+    setIsClient(true);
+  }, []);
+
+  // 服务器端渲染时显示 fallback,客户端渲染时显示 children
+  return isClient ? children : fallback;
+}

+ 26 - 1
src/server/api.ts

@@ -1,11 +1,14 @@
 import { OpenAPIHono } from '@hono/zod-openapi'
+import { swaggerUI } from '@hono/swagger-ui'
 import { errorHandler } from './utils/errorHandler'
 import usersRouter from './api/users/index'
 import authRoute from './api/auth/index'
 import rolesRoute from './api/roles/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
+import { Hono } from 'hono'
 
+const app = new Hono();
 const api = new OpenAPIHono<AuthContext>()
 
 api.onError(errorHandler)
@@ -46,6 +49,27 @@ if(!import.meta.env.PROD){
     }]
     // servers: [{ url: '/api/v1' }]
   })
+
+  app.get('/ui', swaggerUI({
+    url: '/doc',
+    persistAuthorization: true,
+    manuallySwaggerUIHtml: (asset) => `
+      <div>
+        <div id="swagger-ui"></div>
+        <link rel="stylesheet" href="https://ai-oss.d8d.fun/swagger-ui-dist/swagger-ui.css" />
+        <script src="https://ai-oss.d8d.fun/swagger-ui-dist/swagger-ui-bundle.js" crossorigin="anonymous"></script>
+        <script>
+          window.onload = () => {
+            window.ui = SwaggerUIBundle({
+              dom_id: '#swagger-ui',
+              url: '/doc',
+              persistAuthorization: true
+            })
+          }
+        </script>
+      </div>
+    `
+  }))
 }
 
 
@@ -58,4 +82,5 @@ export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
 
-export default api
+app.route('/', api)
+export default app

+ 1 - 1
src/server/data-source.ts

@@ -14,7 +14,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role
+    User, Role, 
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 20 - 85
src/server/index.tsx

@@ -1,90 +1,25 @@
-import 'dotenv/config'
-import { Hono } from 'hono'
-import { cors } from 'hono/cors'
-import { logger } from 'hono/logger'
-import { swaggerUI } from '@hono/swagger-ui'
-import * as fs from 'fs/promises'
-import { renderer } from './renderer'
-import createApi from './api'
-
-
-const app = new Hono();
-// Middleware chain
-app.use('*', logger())
-app.use('*', cors(
-  // {
-  //   origin: ['http://localhost:3000'],
-  //   allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
-  //   credentials: true
-  // }
-))
-
-
-app.route('/', createApi)
-
-if(!import.meta.env.PROD){
-  app.get('/ui', swaggerUI({
-    url: '/doc',
-    persistAuthorization: true,
-    manuallySwaggerUIHtml: (asset) => `
-      <div>
-        <div id="swagger-ui"></div>
-        <link rel="stylesheet" href="https://ai-oss.d8d.fun/swagger-ui-dist/swagger-ui.css" />
-        <script src="https://ai-oss.d8d.fun/swagger-ui-dist/swagger-ui-bundle.js" crossorigin="anonymous"></script>
-        <script>
-          window.onload = () => {
-            window.ui = SwaggerUIBundle({
-              dom_id: '#swagger-ui',
-              url: '/doc',
-              persistAuthorization: true
-            })
-          }
-        </script>
-      </div>
-    `
-  }))
+import { StrictMode } from 'react'
+import {
+  type RenderToPipeableStreamOptions,
+  renderToPipeableStream,
+} from 'react-dom/server'
+// import App from '../App'
+
+export function render(_url: string, options?: RenderToPipeableStreamOptions) {
+  return renderToPipeableStream(
+    <StrictMode>
+      {/* <App /> */}
+    </StrictMode>,
+    options,
+  )
 }
 
-if(import.meta.env.PROD){
-  app.get('/assets/:filename', async (c) => {
-    const filename = c.req.param('filename')
-    const filePath = import.meta.env.PROD? `./dist/assets/${filename}` : `./public/assets/${filename}`
-    const content = await fs.readFile(filePath);
-    const modifyDate = (await fs.stat(filePath))?.mtime?.toUTCString()?? new Date().toUTCString();
-
 
-    const fileExt = filePath.split('.').pop()?.toLowerCase()
-    // 根据文件扩展名设置适当的 Content-Type
-    if (fileExt === 'tsx' || fileExt === 'ts') {
-      c.header('Content-Type', 'text/typescript; charset=utf-8')
-    } else if (fileExt === 'js' || fileExt === 'mjs') {
-      c.header('Content-Type', 'application/javascript; charset=utf-8')
-    } else if (fileExt === 'json') {
-      c.header('Content-Type', 'application/json; charset=utf-8')
-    } else if (fileExt === 'html') {
-      c.header('Content-Type', 'text/html; charset=utf-8')
-    } else if (fileExt === 'css') {
-      c.header('Content-Type', 'text/css; charset=utf-8')
-    } else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
-      c.header('Content-Type', `image/${fileExt}`)
-    }
+import { renderToStaticMarkup } from 'react-dom/server'
+import { Rooter } from './renderer';
 
-    return c.body(content, {
-      headers: {
-        // 'Content-Type': 'text/html; charset=utf-8',
-        'Last-Modified': modifyDate
-      }
-    })
-  })
-}
-
-app.use(renderer)
-app.get('/*', (c) => {
-  return c.render(
-    <>
-      <div id="root"></div>
-    </>
-  )
-}) 
+// 使用renderToStaticMarkup - 不会包含React内部属性,生成纯静态HTML
+export const template = renderToStaticMarkup(
+  <Rooter />
+);
 
-export default app

+ 40 - 6
src/server/renderer.tsx

@@ -1,6 +1,6 @@
 import { GlobalConfig } from '@/share/types'
 import { reactRenderer } from '@hono/react-renderer'
-import { Script, Link } from 'hono-vite-react-stack-node/components'
+// import { Script, Link } from 'hono-vite-react-stack-node/components'
 import process from 'node:process'
 
 // 全局配置常量
@@ -9,14 +9,45 @@ const GLOBAL_CONFIG: GlobalConfig = {
   APP_NAME: process.env.APP_NAME || '多八多Aider',
 }
 
-export const renderer = reactRenderer(({ children }) => {
+// export const renderer = reactRenderer(({ children }) => {
+//   return (
+//     <html>
+//       <head>
+//         <meta charSet="UTF-8" />
+//         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+//         <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
+//         <script dangerouslySetInnerHTML={{ __html: `
+//           const init = () => {
+//             const urlParams = new URLSearchParams(window.location.search);
+//             if (${import.meta.env?.PROD ? "true":"false"} && !urlParams.has('vconsole')) return;
+//             var vConsole = new VConsole({
+//               theme: urlParams.get('vconsole_theme') || 'light',
+//               onReady: function() {
+//                 console.log('vConsole is ready');
+//               }
+//             });
+//           }
+//           init();
+//         `}} />
+//         {/* 注入全局配置 */}
+//         <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
+            
+//       </head>
+//       <body>
+//         {children}
+//         <script type="module" src="/src/client/index.tsx"></script>
+//       </body>
+//     </html> 
+//   )
+// })
+
+export const Rooter = () => {
   return (
     <html>
       <head>
         <meta charSet="UTF-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-        <Script />
-        <Link href="/src/style.css" rel="stylesheet" />
+        <link href='/src/style.css' rel="stylesheet" />
         <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
         <script dangerouslySetInnerHTML={{ __html: `
           const init = () => {
@@ -35,7 +66,10 @@ export const renderer = reactRenderer(({ children }) => {
         <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
             
       </head>
-      <body>{children}</body>
+      <body>
+        <div id='root' dangerouslySetInnerHTML={{ __html: '<!--app-html-->'}}></div>
+        <script type="module" src="/src/client/index.tsx"></script>
+      </body>
     </html> 
   )
-})
+}

+ 37 - 26
src/server/utils/generic-crud.routes.ts

@@ -13,13 +13,13 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, middleware = [] } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking } = options;
   
   // 创建CRUD服务实例
   // 抽象类不能直接实例化,需要创建具体实现类
   class ConcreteCrudService extends GenericCrudService<T> {
     constructor() {
-      super(AppDataSource, entity);
+      super(AppDataSource, entity, { userTracking });
     }
   }
   const crudService = new ConcreteCrudService();
@@ -53,6 +53,11 @@ export function createCrudRoutes<
         sortOrder: z.enum(['ASC', 'DESC']).optional().default('DESC').openapi({
           example: 'DESC',
           description: '排序方向'
+        }),
+        // 增强的筛选参数
+        filters: z.string().optional().openapi({
+          example: '{"status": 1, "createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}',
+          description: '筛选条件(JSON字符串),支持精确匹配、范围查询、IN查询等'
         })
       })
     },
@@ -215,16 +220,25 @@ export function createCrudRoutes<
   const routes = app
     .openapi(listRoute, async (c) => {
       try {
-        const { page, pageSize, keyword, sortBy, sortOrder } = c.req.valid('query');
+        const query = c.req.valid('query') as any;
+        const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
         
         // 构建排序对象
-        // 使用Record和类型断言解决泛型索引写入问题
-        const order: Partial<Record<keyof T, 'ASC' | 'DESC'>> = {};
+        const order: any = {};
         if (sortBy) {
-          (order as Record<string, 'ASC' | 'DESC'>)[sortBy] = sortOrder || 'DESC';
+          order[sortBy] = sortOrder || 'DESC';
         } else {
-          // 默认按id降序排序
-          (order as Record<string, 'ASC' | 'DESC'>)['id'] = 'DESC';
+          order['id'] = 'DESC';
+        }
+        
+        // 解析筛选条件
+        let parsedFilters: any = undefined;
+        if (filters) {
+          try {
+            parsedFilters = JSON.parse(filters);
+          } catch (e) {
+            return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+          }
         }
         
         const [data, total] = await crudService.getList(
@@ -232,13 +246,14 @@ export function createCrudRoutes<
           pageSize,
           keyword,
           searchFields,
-          undefined, // where条件
-          [], // relations
-          order
+          undefined,
+          relations || [],
+          order,
+          parsedFilters
         );
         
         return c.json({
-          data: data as any[],
+          data,
           pagination: { total, current: page, pageSize }
         }, 200);
       } catch (error) {
@@ -251,10 +266,11 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(createRouteDef, async (c) => {
+    .openapi(createRouteDef, async (c: any) => {
       try {
         const data = c.req.valid('json');
-        const result = await crudService.create(data);
+        const user = c.get('user');
+        const result = await crudService.create(data, user?.id);
         return c.json(result, 201);
       } catch (error) {
         if (error instanceof z.ZodError) {
@@ -266,10 +282,10 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(getRouteDef, async (c) => {
+    .openapi(getRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
-        const result = await crudService.getById(id);
+        const result = await crudService.getById(id, relations || []);
         
         if (!result) {
           return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -286,11 +302,12 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(updateRouteDef, async (c) => {
+    .openapi(updateRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
         const data = c.req.valid('json');
-        const result = await crudService.update(id, data);
+        const user = c.get('user');
+        const result = await crudService.update(id, data, user?.id);
         
         if (!result) {
           return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -298,9 +315,6 @@ export function createCrudRoutes<
         
         return c.json(result, 200);
       } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
         if (error instanceof z.ZodError) {
           return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
         }
@@ -310,7 +324,7 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(deleteRouteDef, async (c) => {
+    .openapi(deleteRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
         const success = await crudService.delete(id);
@@ -319,11 +333,8 @@ export function createCrudRoutes<
           return c.json({ code: 404, message: '资源不存在' }, 404);
         }
         
-        return c.body(null, 204) as unknown as Response;
+        return c.body(null, 204);
       } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
         if (error instanceof z.ZodError) {
           return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
         }

+ 112 - 24
src/server/utils/generic-crud.service.ts

@@ -3,20 +3,22 @@ import { z } from '@hono/zod-openapi';
 
 export abstract class GenericCrudService<T extends ObjectLiteral> {
   protected repository: Repository<T>;
-  
+  private userTrackingOptions?: UserTrackingOptions;
+
   constructor(
     protected dataSource: DataSource,
-    protected entity: new () => T
+    protected entity: new () => T,
+    options?: {
+      userTracking?: UserTrackingOptions;
+    }
   ) {
     this.repository = this.dataSource.getRepository(entity);
+    this.userTrackingOptions = options?.userTracking;
   }
 
   /**
    * 获取分页列表
    */
-  /**
-   * 获取分页列表,支持高级查询
-   */
   async getList(
     page: number = 1,
     pageSize: number = 10,
@@ -24,25 +26,35 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     searchFields?: string[],
     where?: Partial<T>,
     relations: string[] = [],
-    order: { [P in keyof T]?: 'ASC' | 'DESC' } = {}
+    order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
+    filters?: {
+      [key: string]: any;
+    }
   ): Promise<[T[], number]> {
     const skip = (page - 1) * pageSize;
     const query = this.repository.createQueryBuilder('entity');
-    
-    // 关联查询
+
+    // 添加关联关系(支持嵌套关联,如 ['contract.client'])
     if (relations.length > 0) {
-      relations.forEach(relation => {
-        query.leftJoinAndSelect(`entity.${relation}`, relation);
+      relations.forEach((relation, relationIndex) => {
+        const parts = relation.split('.');
+        let currentAlias = 'entity';
+        
+        parts.forEach((part, index) => {
+          const newAlias = index === 0 ? part : `${currentAlias}_${relationIndex}`;
+          query.leftJoinAndSelect(`${currentAlias}.${part}`, newAlias);
+          currentAlias = newAlias;
+        });
       });
     }
-    
+
     // 关键词搜索
     if (keyword && searchFields && searchFields.length > 0) {
       query.andWhere(searchFields.map(field => `entity.${field} LIKE :keyword`).join(' OR '), {
         keyword: `%${keyword}%`
       });
     }
-    
+
     // 条件查询
     if (where) {
       Object.entries(where).forEach(([key, value]) => {
@@ -51,42 +63,104 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
         }
       });
     }
-    
+
+    // 扩展筛选条件
+    if (filters) {
+      Object.entries(filters).forEach(([key, value]) => {
+        if (value !== undefined && value !== null && value !== '') {
+          const fieldName = key.startsWith('_') ? key.substring(1) : key;
+          
+          // 支持不同类型的筛选
+          if (Array.isArray(value)) {
+            // 数组类型:IN查询
+            if (value.length > 0) {
+              query.andWhere(`entity.${fieldName} IN (:...${key})`, { [key]: value });
+            }
+          } else if (typeof value === 'string' && value.includes('%')) {
+            // 模糊匹配
+            query.andWhere(`entity.${fieldName} LIKE :${key}`, { [key]: value });
+          } else if (typeof value === 'object' && value !== null) {
+            // 范围查询
+            if ('gte' in value) {
+              query.andWhere(`entity.${fieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
+            }
+            if ('gt' in value) {
+              query.andWhere(`entity.${fieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
+            }
+            if ('lte' in value) {
+              query.andWhere(`entity.${fieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
+            }
+            if ('lt' in value) {
+              query.andWhere(`entity.${fieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
+            }
+            if ('between' in value && Array.isArray(value.between) && value.between.length === 2) {
+              query.andWhere(`entity.${fieldName} BETWEEN :${key}_start AND :${key}_end`, {
+                [`${key}_start`]: value.between[0],
+                [`${key}_end`]: value.between[1]
+              });
+            }
+          } else {
+            // 精确匹配
+            query.andWhere(`entity.${fieldName} = :${key}`, { [key]: value });
+          }
+        }
+      });
+    }
+
     // 排序
     Object.entries(order).forEach(([key, direction]) => {
       query.orderBy(`entity.${key}`, direction);
     });
-    
+
     return query.skip(skip).take(pageSize).getManyAndCount();
   }
 
   /**
-   * 高级查询方法
+   * 根据ID获取单个实体
    */
-  createQueryBuilder(alias: string = 'entity') {
-    return this.repository.createQueryBuilder(alias);
+  async getById(id: number, relations: string[] = []): Promise<T | null> {
+    return this.repository.findOne({
+      where: { id } as any,
+      relations
+    });
   }
 
   /**
-   * 根据ID获取单个实体
+   * 设置用户跟踪字段
    */
-  async getById(id: number): Promise<T | null> {
-    return this.repository.findOneBy({ id } as any);
+  private setUserFields(data: any, userId?: string | number, isCreate: boolean = true): void {
+    if (!this.userTrackingOptions || !userId) {
+      return;
+    }
+
+    const { createdByField = 'createdBy', updatedByField = 'updatedBy' } = this.userTrackingOptions;
+
+    if (isCreate && createdByField) {
+      data[createdByField] = userId;
+    }
+
+    if (updatedByField) {
+      data[updatedByField] = userId;
+    }
   }
 
   /**
    * 创建实体
    */
-  async create(data: DeepPartial<T>): Promise<T> {
-    const entity = this.repository.create(data as DeepPartial<T>);
+  async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
+    const entityData = { ...data };
+    this.setUserFields(entityData, userId, true);
+    const entity = this.repository.create(entityData as DeepPartial<T>);
     return this.repository.save(entity);
   }
 
   /**
    * 更新实体
    */
-  async update(id: number, data: Partial<T>): Promise<T | null> {
-    await this.repository.update(id, data);
+  async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
+    const updateData = { ...data };
+    this.setUserFields(updateData, userId, false);
+    await this.repository.update(id, updateData);
     return this.getById(id);
   }
 
@@ -97,6 +171,18 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     const result = await this.repository.delete(id);
     return result.affected === 1;
   }
+
+  /**
+   * 高级查询方法
+   */
+  createQueryBuilder(alias: string = 'entity') {
+    return this.repository.createQueryBuilder(alias);
+  }
+}
+
+export interface UserTrackingOptions {
+  createdByField?: string;
+  updatedByField?: string;
 }
 
 export type CrudOptions<
@@ -112,5 +198,7 @@ export type CrudOptions<
   getSchema: GetSchema;
   listSchema: ListSchema;
   searchFields?: string[];
+  relations?: string[];
   middleware?: any[];
+  userTracking?: UserTrackingOptions;
 };

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 22 - 17
tsconfig.json

@@ -1,21 +1,26 @@
 {
   "compilerOptions": {
-    "target": "ESNext",
-    "module": "ESNext",
-    "moduleResolution": "Bundler",
-    "incremental": true, 
-    "strict": true,
+    "target": "es2022",
+    "useDefineForClassFields": true,
+    "module": "esnext",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
     "skipLibCheck": true,
-    "lib": [
-      "DOM",
-      "DOM.Iterable",
-      "ESNext"
-    ],
-    "types": [
-      "vite/client"
-    ],
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
     "jsx": "react-jsx",
-    "jsxImportSource": "react",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true,
+
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
     "baseUrl": ".",
@@ -23,6 +28,6 @@
       "@/*": ["src/*"]
     },
   },
-  "include": ["src/**/*.ts", "src/**/*.tsx"],
-  "exclude": ["node_modules"]
-}
+  "include": ["src"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 24 - 0
tsconfig.node.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "es2023",
+    "lib": ["ES2023"],
+    "module": "esnext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 10 - 33
vite.config.ts

@@ -1,45 +1,22 @@
-import reactStack from 'hono-vite-react-stack-node'
 import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+import tailwindcss from '@tailwindcss/vite'
 
+// https://vite.dev/config/
 export default defineConfig({
   plugins: [
-    reactStack({
-      minify: false,
-      port: 8080
+    react({
+      tsDecorators: true,
     }),
+    tailwindcss(),
   ],
+  server: {
+    allowedHosts:true
+  },
   // 配置 @ 别名
   resolve: {
     alias: {
       '@': '/src',
     },
   },
-  build:{
-    // assetsDir: 'ai-assets',
-  },
-  ssr:{
-    external:[
-      'dotenv','typeorm','bcrypt', '@d8d-appcontainer/api',
-      'mysql2', 'ioredis','reflect-metadata',
-      '@hono/node-server', 'jsonwebtoken', 'minio',
-      'node-fetch', 'node-cron',
-      '@alicloud/dysmsapi20170525', '@alicloud/openapi-client',
-      '@alicloud/tea-util',
-      'react',
-      'react-dom',
-      'hono',
-      '@heroicons/react',
-      '@hono/node-server',
-      '@hono/react-renderer',
-      '@hono/swagger-ui',
-      '@hono/vite-dev-server',
-      '@hono/zod-openapi',
-      '@hono/zod-validator',
-    ]
-  },
-  server:{
-    host:'0.0.0.0',
-    port: 8080,
-    allowedHosts: true,
-  },
-})
+})

Some files were not shown because too many files changed in this diff