Files
d8d-admin-mobile-starter-pu…/server/app.tsx

477 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
import { Hono } from 'hono'
import { Auth } from '@d8d-appcontainer/auth'
import type { User as AuthUser } from '@d8d-appcontainer/auth'
import React from 'hono/jsx'
import type { FC } from 'hono/jsx'
import { cors } from 'hono/cors'
import type { Context as HonoContext } from 'hono'
import { serveStatic } from 'hono/deno'
import { APIClient } from '@d8d-appcontainer/api'
import debug from "debug"
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import type { SystemSettingRecord, GlobalConfig } from '../client/share/types.ts';
import { SystemSettingKey, OssType, MapMode } from '../client/share/types.ts';
import {
createKnowInfoRoutes,
createFileCategoryRoutes,
createFileUploadRoutes,
createThemeRoutes,
createSystemSettingsRoutes,
} from "./routes_sys.ts";
import {
createMapRoutes,
} from "./routes_maps.ts";
import {
createChartRoutes,
} from "./routes_charts.ts";
// 导入基础路由
import { createAuthRoutes } from "./routes_auth.ts";
import { createUserRoutes } from "./routes_users.ts";
import { createMessagesRoutes } from "./routes_messages.ts";
import { createMigrationsRoutes } from "./routes_migrations.ts";
import { createHomeRoutes } from "./routes_home.ts";
dayjs.extend(utc)
// 初始化debug实例
const log = {
app: debug('app:server'),
auth: debug('auth:server'),
api: debug('api:server'),
debug: debug('debug:server')
}
const GLOBAL_CONFIG: GlobalConfig = {
OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
API_BASE_URL: '/api',
APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
ENV: Deno.env.get('ENV') || 'development',
DEFAULT_THEME: 'light', // 默认主题
MAP_CONFIG: {
KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
VERSION: '2.0',
PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
},
CHART_THEME: 'default', // 图表主题
ENABLE_THEME_CONFIG: false, // 主题配置开关
THEME: null
};
log.app.enabled = true
log.auth.enabled = true
log.api.enabled = true
log.debug.enabled = true
// 定义自定义上下文类型
export interface Variables {
auth: Auth
user?: AuthUser
apiClient: APIClient
moduleDir: string
systemSettings?: SystemSettingRecord
}
// 定义登录历史类型
interface LoginHistory {
id: number
user_id: number
login_time: string
ip_address?: string
user_agent?: string
}
// 定义仪表盘数据类型
interface DashboardData {
lastLogin: string
loginCount: number
fileCount: number
userCount: number
systemInfo: {
version: string
lastUpdate: string
}
}
interface EsmScriptConfig {
src: string
href: string
denoJson: string
refresh: boolean
prodPath?: string
prodSrc?: string
}
// Auth实例
let authInstance: Auth | null = null
// 初始化Auth实例
const initAuth = async (apiClient: APIClient) => {
try {
if (authInstance) {
return authInstance
}
log.auth('正在初始化Auth实例')
authInstance = new Auth(apiClient as any, {
jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
initialUsers: [],
storagePrefix: '',
userTable: 'users',
fieldNames: {
id: 'id',
username: 'username',
password: 'password',
phone: 'phone',
email: 'email',
is_disabled: 'is_disabled',
is_deleted: 'is_deleted'
},
tokenExpiry: 24 * 60 * 60,
refreshTokenExpiry: 7 * 24 * 60 * 60
})
log.auth('Auth实例初始化完成')
return authInstance
} catch (error) {
log.auth('Auth初始化失败:', error)
throw error
}
}
// 初始化系统设置
const initSystemSettings = async (apiClient: APIClient) => {
try {
const systemSettings = await apiClient.database.table('system_settings')
.select()
// 将系统设置转换为键值对形式
const settings = systemSettings.reduce((acc: Record<string, any>, setting: any) => {
acc[setting.key] = setting.value
return acc
}, {}) as SystemSettingRecord
// 更新全局配置
if (settings[SystemSettingKey.SITE_NAME]) {
GLOBAL_CONFIG.APP_NAME = String(settings[SystemSettingKey.SITE_NAME])
}
// 设置其他全局配置项
if (settings[SystemSettingKey.SITE_FAVICON]) {
GLOBAL_CONFIG.DEFAULT_THEME = String(settings[SystemSettingKey.SITE_FAVICON])
}
if (settings[SystemSettingKey.SITE_LOGO]) {
GLOBAL_CONFIG.MAP_CONFIG.KEY = String(settings[SystemSettingKey.SITE_LOGO])
}
if (settings[SystemSettingKey.SITE_DESCRIPTION]) {
GLOBAL_CONFIG.CHART_THEME = String(settings[SystemSettingKey.SITE_DESCRIPTION])
}
// 设置主题配置开关
if (settings[SystemSettingKey.ENABLE_THEME_CONFIG]) {
GLOBAL_CONFIG.ENABLE_THEME_CONFIG = settings[SystemSettingKey.ENABLE_THEME_CONFIG] === 'true'
}
// 查询ID1管理员的主题配置
const adminTheme = await apiClient.database.table('theme_settings')
.where('user_id', 1)
.first()
if (adminTheme) {
GLOBAL_CONFIG.THEME = adminTheme.settings
}
return settings
} catch (error) {
log.app('获取系统设置失败:', error)
return {} as SystemSettingRecord
}
}
// 中间件:验证认证
const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
try {
const auth = c.get('auth')
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token) {
const userData = await auth.verifyToken(token)
if (userData) {
c.set('user', userData)
await next()
return
}
}
return c.json({ error: '未授权' }, 401)
} catch (error) {
log.auth('认证失败:', error)
return c.json({ error: '无效凭证' }, 401)
}
}
// 导出withAuth类型定义
export type WithAuth = typeof withAuth;
// 定义模块参数接口
interface ModuleParams {
apiClient: APIClient
app: Hono<{ Variables: Variables }>
moduleDir: string
}
export default function({ apiClient, app, moduleDir }: ModuleParams) {
const honoApp = app
// 添加CORS中间件
honoApp.use('/*', cors())
// 创建API路由
const api = new Hono<{ Variables: Variables }>()
// // 使用数据库中间件
// api.use('/*', withDatabase)
// 设置环境变量
api.use('*', async (c, next) => {
c.set('apiClient', apiClient)
c.set('moduleDir', moduleDir)
c.set('auth', await initAuth(apiClient))
c.set('systemSettings', await initSystemSettings(apiClient))
await next()
})
// 查询仪表盘数据
api.get('/dashboard', withAuth, async (c) => {
try {
const user = c.get('user')!
const apiClient = c.get('apiClient')
const lastLogin = await apiClient.database.table('login_history')
.where('user_id', user.id)
.orderBy('login_time', 'desc')
.limit(1)
.first()
// 获取登录总次数
const loginCount = await apiClient.database.table('login_history')
.where('user_id', user.id)
.count()
// 获取系统数据统计
const fileCount = await apiClient.database.table('file_library')
.where('is_deleted', 0)
.count()
const userCount = await apiClient.database.table('users')
.where('is_deleted', 0)
.count()
// 返回仪表盘数据
const dashboardData: DashboardData = {
lastLogin: lastLogin ? lastLogin.login_time : new Date().toISOString(),
loginCount: loginCount,
fileCount: Number(fileCount),
userCount: Number(userCount),
systemInfo: {
version: '1.0.0',
lastUpdate: new Date().toISOString()
}
}
return c.json(dashboardData)
} catch (error) {
log.api('获取仪表盘数据失败:', error)
return c.json({ error: '获取仪表盘数据失败' }, 500)
}
})
// 注册基础路由
api.route('/auth', createAuthRoutes(withAuth))
api.route('/users', createUserRoutes(withAuth))
api.route('/know-info', createKnowInfoRoutes(withAuth))
api.route('/upload', createFileUploadRoutes(withAuth)) // 添加文件上传路由
api.route('/file-categories', createFileCategoryRoutes(withAuth)) // 添加文件分类管理路由
api.route('/theme', createThemeRoutes(withAuth)) // 添加主题设置路由
api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
api.route('/messages', createMessagesRoutes(withAuth)) // 添加消息路由
api.route('/migrations', createMigrationsRoutes(withAuth)) // 添加数据库迁移路由
api.route('/home', createHomeRoutes(withAuth)) // 添加首页路由
// 注册API路由
honoApp.route('/api', api)
// 首页路由 - SSR
honoApp.get('/', async (c: HonoContext) => {
const systemName = GLOBAL_CONFIG.APP_NAME
return c.html(
<html>
<head>
<title>{systemName}</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* 系统介绍区域 */}
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
{systemName}
</h1>
<p className="text-lg text-gray-600 mb-8">
Starter
</p>
<p className="text-base text-gray-500 mb-8">
Hono和React的应用Starter
</p>
</div>
{/* 管理入口按钮 */}
<div className="space-y-4">
<a
href="/admin"
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"
>
</a>
{/* 移动端入口按钮 */}
<a
href="/mobile"
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"
>
</a>
</div>
</div>
</div>
</body>
</html>
)
})
// 创建一个函数用于生成包含全局配置的HTML页面
const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
return (c: HonoContext) => {
const isProd = GLOBAL_CONFIG.ENV === 'production';
return c.html(
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
{isProd ? (
<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>
)}
{isProd ? (<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)};` }} />
{!isProd && (
<>
<script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
<script dangerouslySetInnerHTML={{ __html: `
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('vconsole')) {
var vConsole = new VConsole({
theme: urlParams.get('vconsole_theme') || 'light',
onReady: function() {
console.log('vConsole is ready');
}
});
}
`}} />
</>
)}
</head>
<body className="bg-gray-50">
<div id="root"></div>
</body>
</html>
)
}
}
// 后台管理路由
honoApp.get('/admin', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/admin/web_app.tsx",
denoJson: "/deno.json",
refresh: true,
prodPath: "admin/web_app.js"
}, GLOBAL_CONFIG.APP_NAME))
honoApp.get('/admin/*', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/admin/web_app.tsx",
denoJson: "/deno.json",
refresh: true,
prodPath: "admin/web_app.js"
}, 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,
prodPath: "mobile/mobile_app.js"
}, 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,
prodPath: "mobile/mobile_app.js"
}, GLOBAL_CONFIG.APP_NAME))
const staticRoutes = serveStatic({
root: moduleDir,
onFound: async (path: string, c: HonoContext) => {
const fileExt = path.split('.').pop()?.toLowerCase()
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}`)
}
const fileInfo = await Deno.stat(path)
c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
},
})
// 静态资源路由
honoApp.get('/deno.json', staticRoutes)
honoApp.get('/client/*', staticRoutes)
honoApp.get('/amap/*', staticRoutes)
honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
honoApp.get('/client_dist/*', staticRoutes)
return honoApp
}