app.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
  2. import { Hono } from 'hono'
  3. import React from 'hono/jsx'
  4. import type { Context as HonoContext } from 'hono'
  5. import { serveStatic } from 'hono/deno'
  6. import { APIClient } from '@d8d-appcontainer/api'
  7. import { Auth } from '@d8d-appcontainer/auth';
  8. import debug from "debug"
  9. import dayjs from 'dayjs';
  10. import utc from 'dayjs/plugin/utc';
  11. import type { GlobalConfig } from '../client/share/types.ts';
  12. import { OssType, MapMode } from '../client/share/types.ts';
  13. import { createRouter } from './router.ts'
  14. dayjs.extend(utc)
  15. // 初始化debug实例
  16. const log = {
  17. app: debug('app:server'),
  18. auth: debug('auth:server'),
  19. api: debug('api:server'),
  20. debug: debug('debug:server')
  21. }
  22. const GLOBAL_CONFIG: GlobalConfig = {
  23. OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
  24. OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
  25. API_BASE_URL: '/api',
  26. APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
  27. ENV: Deno.env.get('ENV') || 'development',
  28. DEFAULT_THEME: 'light', // 默认主题
  29. MAP_CONFIG: {
  30. KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
  31. VERSION: '2.0',
  32. PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
  33. MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
  34. },
  35. CHART_THEME: 'default', // 图表主题
  36. ENABLE_THEME_CONFIG: false, // 主题配置开关
  37. THEME: null
  38. };
  39. log.app.enabled = true
  40. log.auth.enabled = true
  41. log.api.enabled = true
  42. log.debug.enabled = true
  43. interface EsmScriptConfig {
  44. src: string
  45. href: string
  46. denoJson: string
  47. refresh: boolean
  48. prodPath?: string
  49. prodSrc?: string
  50. }
  51. // 定义模块参数接口
  52. interface ModuleParams {
  53. apiClient: APIClient,
  54. auth: Auth,
  55. app: Hono
  56. moduleDir: string
  57. }
  58. export default function({ apiClient, app, moduleDir , auth}: ModuleParams) {
  59. const honoApp = app
  60. // 创建路由
  61. const router = createRouter(apiClient, moduleDir, auth)
  62. honoApp.route('/', router)
  63. // 首页路由 - SSR
  64. honoApp.get('/', async (c: HonoContext) => {
  65. const systemName = GLOBAL_CONFIG.APP_NAME
  66. return c.html(
  67. <html>
  68. <head>
  69. <title>{systemName}</title>
  70. <meta charset="UTF-8" />
  71. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  72. <script src="https://cdn.tailwindcss.com"></script>
  73. </head>
  74. <body>
  75. <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  76. <div className="max-w-md w-full space-y-8">
  77. {/* 系统介绍区域 */}
  78. <div className="text-center">
  79. <h1 className="text-4xl font-bold text-gray-900 mb-4">
  80. {systemName}
  81. </h1>
  82. <p className="text-lg text-gray-600 mb-8">
  83. 全功能应用Starter
  84. </p>
  85. <p className="text-base text-gray-500 mb-8">
  86. 这是一个基于Hono和React的应用Starter,提供了用户认证、文件管理、图表分析、地图集成和主题切换等常用功能。
  87. </p>
  88. </div>
  89. {/* 管理入口按钮 */}
  90. <div className="space-y-4">
  91. <a
  92. href="/admin"
  93. className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-lg font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
  94. >
  95. 进入管理后台
  96. </a>
  97. {/* 移动端入口按钮 */}
  98. <a
  99. href="/mobile"
  100. className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
  101. >
  102. 进入移动端
  103. </a>
  104. {/* 迁移管理入口按钮 */}
  105. <a
  106. href="/migrations"
  107. 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"
  108. >
  109. 数据库迁移
  110. </a>
  111. {/* 股票训练入口按钮 */}
  112. <a
  113. href="/stock"
  114. className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
  115. >
  116. 进入股票训练
  117. </a>
  118. </div>
  119. </div>
  120. </div>
  121. </body>
  122. </html>
  123. )
  124. })
  125. // 创建一个函数,用于生成包含全局配置的HTML页面
  126. const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
  127. return (c: HonoContext) => {
  128. const isProd = GLOBAL_CONFIG.ENV === 'production';
  129. const isLocalDeploy = Deno.env.get('IS_LOCAL_DEPLOY') === 'true';
  130. return c.html(
  131. <html lang="zh-CN">
  132. <head>
  133. <meta charset="UTF-8" />
  134. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  135. {isProd && <meta name="version" content={Deno.env.get('VERSION') || '0.1.0'} />}
  136. <title>{title}</title>
  137. {isLocalDeploy ? (
  138. <script type="module" src={scriptConfig.prodSrc || `/client_dist/${scriptConfig.prodPath}`}></script>
  139. ) : (
  140. <script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson}
  141. {...isProd ? {}:{ refresh: true }}
  142. ></script>
  143. )}
  144. {isLocalDeploy ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
  145. <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
  146. {isProd && (
  147. <>
  148. <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
  149. <script dangerouslySetInnerHTML={{ __html: `
  150. const urlParams = new URLSearchParams(window.location.search);
  151. if (urlParams.has('vconsole')) {
  152. var vConsole = new VConsole({
  153. theme: urlParams.get('vconsole_theme') || 'light',
  154. onReady: function() {
  155. console.log('vConsole is ready');
  156. }
  157. });
  158. }
  159. `}} />
  160. </>
  161. )}
  162. {!isProd && (
  163. <>
  164. <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
  165. <script dangerouslySetInnerHTML={{ __html: `
  166. const urlParams = new URLSearchParams(window.location.search);
  167. var vConsole = new VConsole({
  168. theme: urlParams.get('vconsole_theme') || 'light',
  169. onReady: function() {
  170. console.log('vConsole is ready');
  171. }
  172. });
  173. `}} />
  174. </>
  175. )}
  176. <script src="https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js"></script>
  177. </head>
  178. <body className="bg-gray-50">
  179. <div id="root"></div>
  180. </body>
  181. </html>
  182. )
  183. }
  184. }
  185. // 后台管理路由
  186. honoApp.get('/admin', createHtmlWithConfig({
  187. src: "https://esm.d8d.fun/xb",
  188. href: "/client/admin/web_app.tsx",
  189. denoJson: "/deno.json",
  190. refresh: true,
  191. prodPath: "admin/web_app.js"
  192. }, GLOBAL_CONFIG.APP_NAME))
  193. honoApp.get('/admin/*', createHtmlWithConfig({
  194. src: "https://esm.d8d.fun/xb",
  195. href: "/client/admin/web_app.tsx",
  196. denoJson: "/deno.json",
  197. refresh: true,
  198. prodPath: "admin/web_app.js"
  199. }, GLOBAL_CONFIG.APP_NAME))
  200. // 移动端路由
  201. honoApp.get('/mobile', createHtmlWithConfig({
  202. src: "https://esm.d8d.fun/xb",
  203. href: "/client/mobile/mobile_app.tsx",
  204. denoJson: "/deno.json",
  205. refresh: true,
  206. prodPath: "mobile/mobile_app.js"
  207. }, GLOBAL_CONFIG.APP_NAME))
  208. honoApp.get('/mobile/*', createHtmlWithConfig({
  209. src: "https://esm.d8d.fun/xb",
  210. href: "/client/mobile/mobile_app.tsx",
  211. denoJson: "/deno.json",
  212. refresh: true,
  213. prodPath: "mobile/mobile_app.js"
  214. }, GLOBAL_CONFIG.APP_NAME))
  215. // 迁移管理路由
  216. honoApp.get('/migrations', createHtmlWithConfig({
  217. src: "https://esm.d8d.fun/xb",
  218. href: "/client/migrations/migrations_app.tsx",
  219. denoJson: "/deno.json",
  220. refresh: true,
  221. prodPath: "migrations/migrations_app.js"
  222. }, GLOBAL_CONFIG.APP_NAME))
  223. honoApp.get('/migrations/*', createHtmlWithConfig({
  224. src: "https://esm.d8d.fun/xb",
  225. href: "/client/migrations/migrations_app.tsx",
  226. denoJson: "/deno.json",
  227. refresh: true,
  228. prodPath: "migrations/migrations_app.js"
  229. }, GLOBAL_CONFIG.APP_NAME))
  230. honoApp.get('/stock/*', createHtmlWithConfig({
  231. src: "https://esm.d8d.fun/xb",
  232. href: "/client/stock/stock_app.tsx",
  233. denoJson: "/deno.json",
  234. refresh: true,
  235. prodPath: "stock/stock_app.js"
  236. }, GLOBAL_CONFIG.APP_NAME))
  237. const staticRoutes = serveStatic({
  238. root: moduleDir,
  239. onFound: async (path: string, c: HonoContext) => {
  240. const fileExt = path.split('.').pop()?.toLowerCase()
  241. if (fileExt === 'tsx' || fileExt === 'ts') {
  242. c.header('Content-Type', 'text/typescript; charset=utf-8')
  243. } else if (fileExt === 'js' || fileExt === 'mjs') {
  244. c.header('Content-Type', 'application/javascript; charset=utf-8')
  245. } else if (fileExt === 'json') {
  246. c.header('Content-Type', 'application/json; charset=utf-8')
  247. } else if (fileExt === 'html') {
  248. c.header('Content-Type', 'text/html; charset=utf-8')
  249. } else if (fileExt === 'css') {
  250. c.header('Content-Type', 'text/css; charset=utf-8')
  251. } else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
  252. c.header('Content-Type', `image/${fileExt}`)
  253. }
  254. const fileInfo = await Deno.stat(path)
  255. c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
  256. },
  257. })
  258. // 静态资源路由
  259. honoApp.get('/deno.json', staticRoutes)
  260. honoApp.get('/client/*', staticRoutes)
  261. honoApp.get('/amap/*', staticRoutes)
  262. honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
  263. honoApp.get('/client_dist/*', staticRoutes)
  264. return honoApp
  265. }