添加管理端和移动端的多个新功能模块,包括文件上传、在线地图、用户认证、系统设置等,优化代码结构,提升可维护性和用户体验。
This commit is contained in:
221
server/README.md
Normal file
221
server/README.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 管理端与移动端启动模板 (Admin-Mobile Starter)
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于 Deno 和 Hono 框架开发的管理系统与移动端应用启动模板,提供了完整的用户认证、权限管理、系统设置、文件上传、地图组件、图表组件等功能,可以快速构建企业级应用。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端框架**:Deno + Hono
|
||||
- **前端框架**:React 19 + Ant Design 5
|
||||
- **状态管理**:TanStack Query
|
||||
- **认证系统**:@d8d-appcontainer/auth
|
||||
- **API客户端**:@d8d-appcontainer/api
|
||||
- **地图组件**:高德地图(支持在线/离线模式)
|
||||
- **图表组件**:Ant Design Charts
|
||||
- **日期处理**:Day.js
|
||||
- **网络请求**:Axios
|
||||
|
||||
## 功能模块
|
||||
|
||||
- **用户认证与管理** - 登录、注册、用户信息管理
|
||||
- **系统设置** - 站点信息、主题配置、全局参数设置
|
||||
- **文件管理** - 文件上传、分类管理
|
||||
- **地图组件** - 在线/离线地图、位置标记、地图交互
|
||||
- **图表组件** - 数据可视化图表
|
||||
- **移动端适配** - 响应式设计,支持移动端访问
|
||||
|
||||
## 目录结构
|
||||
|
||||
- `asset/` - 前端资源文件
|
||||
- `admin/` - 管理端资源
|
||||
- `mobile/` - 移动端资源
|
||||
- `share/` - 共享资源和类型定义
|
||||
- `routes_*.ts` - 各模块路由定义文件
|
||||
- `app.tsx` - 应用主入口
|
||||
- `migrations.ts` - 数据库迁移
|
||||
- `deno.json` - Deno配置文件
|
||||
|
||||
## 在D8D(多八多)平台运行
|
||||
|
||||
本应用在 [D8D(多八多)开发者平台](https://www.d8d.fun) 上可以直接运行,无需复杂部署:
|
||||
|
||||
1. 访问 [www.d8d.fun](https://www.d8d.fun) 网站并注册账号
|
||||
2. 登录后进入开发者控制台
|
||||
3. 点击"创建应用"按钮创建新应用
|
||||
4. 选择"管理端与移动端启动模板"作为应用模板
|
||||
5. 配置应用基本信息(名称、描述等)
|
||||
6. 完成创建后,直接点击"预览"按钮即可运行应用,无需额外部署步骤
|
||||
7. 系统会自动初始化并启动应用,可直接在浏览器中访问和使用
|
||||
|
||||
### D8D(多八多)平台专属配置
|
||||
|
||||
在D8D(多八多)平台运行时,可以通过平台的"应用配置"面板设置以下内容:
|
||||
|
||||
- 应用资源限制(CPU、内存等)
|
||||
- 公网访问设置
|
||||
- 域名绑定
|
||||
- 自动备份
|
||||
- 日志记录级别
|
||||
|
||||
## 本地开发指南
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Deno 2.2.8 或更高版本
|
||||
- 数据库(由 @d8d-appcontainer/api 支持的数据库)
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
在启动应用前,可配置以下环境变量:
|
||||
|
||||
```
|
||||
# 应用配置
|
||||
APP_NAME=应用名称
|
||||
ENV=development
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
|
||||
# OSS配置
|
||||
OSS_TYPE=aliyun # 可选值: aliyun, minio
|
||||
OSS_BASE_URL=https://your-oss-url.com
|
||||
|
||||
# 地图配置
|
||||
MAP_MODE=online # 可选值: online, offline
|
||||
AMAP_KEY=您的地图API密钥
|
||||
|
||||
# API客户端配置
|
||||
SERVER_URL=https://app-server.d8d.fun
|
||||
WORKSPACE_KEY=您的工作空间密钥 # 在多八多(www.d8d.fun)平台注册开通工作空间后获取
|
||||
```
|
||||
|
||||
### 本地启动应用
|
||||
|
||||
要在本地运行此应用,需要创建一个启动文件:
|
||||
|
||||
1. 创建一个名为`run_app.ts`的新文件(文件名可自定义)
|
||||
2. 将下面的代码复制到该文件中:
|
||||
|
||||
```typescript
|
||||
// 导入所需模块
|
||||
import { Hono } from 'hono'
|
||||
import { APIClient } from '@d8d-appcontainer/api'
|
||||
import debug from "debug"
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
// 初始化debug实例
|
||||
const log = {
|
||||
app: debug('app:server'),
|
||||
auth: debug('auth:server'),
|
||||
api: debug('api:server'),
|
||||
debug: debug('debug:server')
|
||||
}
|
||||
|
||||
// 启用所有日志
|
||||
Object.values(log).forEach(logger => logger.enabled = true)
|
||||
|
||||
// 初始化 API Client
|
||||
const getApiClient = async (workspaceKey: string, serverUrl?: string) => {
|
||||
try {
|
||||
log.api('正在初始化API Client实例')
|
||||
|
||||
const apiClient = await APIClient.getInstance({
|
||||
scope: 'user',
|
||||
config: {
|
||||
serverUrl: serverUrl || Deno.env.get('SERVER_URL') || 'https://app-server.d8d.fun',
|
||||
workspaceKey: workspaceKey,
|
||||
type: 'http',
|
||||
}
|
||||
})
|
||||
|
||||
log.api('API Client初始化成功')
|
||||
return apiClient
|
||||
} catch (error) {
|
||||
log.api('API Client初始化失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Hono应用实例
|
||||
const app = new Hono()
|
||||
|
||||
// 注册CORS中间件
|
||||
app.use('/*', cors())
|
||||
|
||||
// 动态加载并运行模板
|
||||
const runTemplate = async () => {
|
||||
try {
|
||||
// 创建基础app实例
|
||||
const moduleApp = new Hono()
|
||||
|
||||
// 初始化API Client
|
||||
// 注意:WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取
|
||||
const workspaceKey = Deno.env.get('WORKSPACE_KEY') || ''
|
||||
if (!workspaceKey) {
|
||||
console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
|
||||
}
|
||||
const apiClient = await getApiClient(workspaceKey)
|
||||
|
||||
// 导入模板主模块
|
||||
const templateModule = await import('./app.tsx')
|
||||
|
||||
if (templateModule.default) {
|
||||
// 传入必要参数并初始化应用
|
||||
const appInstance = templateModule.default({
|
||||
apiClient: apiClient,
|
||||
app: moduleApp,
|
||||
moduleDir: './admin-mobile-starter'
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
Deno.serve({ port: 8000 }, appInstance.fetch)
|
||||
console.log('应用已启动,监听端口: 8000')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('模板加载失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行模板
|
||||
runTemplate()
|
||||
```
|
||||
|
||||
3. 运行该文件:
|
||||
|
||||
```bash
|
||||
deno run -A run_app.ts
|
||||
```
|
||||
|
||||
> **注意**:上述代码用于本地运行app.tsx。SERVER_URL默认值为`app-server.d8d.fun`,WORKSPACE_KEY需要在 [多八多(www.d8d.fun)](https://www.d8d.fun) 平台注册并开通工作空间后获取。
|
||||
|
||||
## 系统配置说明
|
||||
|
||||
系统配置可通过环境变量或数据库中的系统设置进行管理,支持以下配置项:
|
||||
|
||||
- 站点名称、图标、Logo
|
||||
- 主题设置(明/暗模式)
|
||||
- 地图模式(在线/离线)
|
||||
- 图表主题
|
||||
- API 基础路径
|
||||
- 文件存储方式
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
系统首次启动时会自动执行数据库迁移,创建必要的表结构和初始数据。
|
||||
|
||||
## 开发者扩展指南
|
||||
|
||||
### 添加新路由
|
||||
|
||||
在 `routes_*.ts` 文件中定义新的路由处理函数,然后在 `app.tsx` 中引入并注册。
|
||||
|
||||
### 前端开发
|
||||
|
||||
前端资源位于 `asset/` 目录下,区分为管理端和移动端,可根据需要进行修改和扩展。
|
||||
|
||||
## 许可证
|
||||
|
||||
[License] - 请参阅LICENSE文件了解详情
|
||||
|
||||
---
|
||||
|
||||
© 2025 多八多(D8D). 保留所有权利。
|
||||
471
server/app.tsx
Normal file
471
server/app.tsx
Normal file
@@ -0,0 +1,471 @@
|
||||
/** @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 { migrations } from './migrations.ts';
|
||||
// 导入基础路由
|
||||
import { createAuthRoutes } from "./routes_auth.ts";
|
||||
import { createUserRoutes } from "./routes_users.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 initDatabase = async (apiClient: APIClient) => {
|
||||
try {
|
||||
log.app('正在执行数据库迁移...')
|
||||
|
||||
const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
|
||||
// log.app('数据库迁移完成 %O',migrationsResult)
|
||||
log.app('数据库迁移完成')
|
||||
|
||||
} catch (error) {
|
||||
log.app('数据库迁移失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 中间件:数据库初始化
|
||||
const withDatabase = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient')
|
||||
await initDatabase(apiClient)
|
||||
await next()
|
||||
} catch (error) {
|
||||
log.api('数据库操作失败:', error)
|
||||
return c.json({ error: '数据库操作失败' }, 500)
|
||||
}
|
||||
}
|
||||
|
||||
// 中间件:验证认证
|
||||
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('*', 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.use('/', withDatabase)
|
||||
|
||||
// 查询仪表盘数据
|
||||
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路由
|
||||
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>
|
||||
<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>
|
||||
</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: "/client/admin/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: "/client/admin/deno.json",
|
||||
refresh: true,
|
||||
prodPath: "admin/web_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('/client/*', staticRoutes)
|
||||
honoApp.get('/amap/*', staticRoutes)
|
||||
honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
|
||||
honoApp.get('/client_dist/*', staticRoutes)
|
||||
|
||||
return honoApp
|
||||
}
|
||||
33
server/deno.json
Normal file
33
server/deno.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"imports": {
|
||||
"hono": "https://esm.d8d.fun/hono@4.7.4",
|
||||
"hono/jsx": "https://esm.d8d.fun/hono@4.7.4/jsx",
|
||||
"hono/jsx/jsx-runtime": "https://esm.d8d.fun/hono@4.7.4/jsx/jsx-runtime",
|
||||
"hono/cors": "https://esm.d8d.fun/hono@4.7.4/cors",
|
||||
"hono/serve-static": "https://esm.d8d.fun/hono@4.7.4/serve-static",
|
||||
"hono/deno": "https://esm.d8d.fun/hono@4.7.4/deno",
|
||||
"@d8d-appcontainer/auth": "https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14",
|
||||
"debug": "https://esm.d8d.fun/debug@4.4.0",
|
||||
"dayjs/plugin/utc": "https://esm.d8d.fun/dayjs@1.11.13/plugin/utc",
|
||||
"react": "https://esm.d8d.fun/react@19.0.0",
|
||||
"react-dom": "https://esm.d8d.fun/react-dom@19.0.0",
|
||||
"react-dom/client": "https://esm.d8d.fun/react-dom@19.0.0/client",
|
||||
"react-router": "https://esm.d8d.fun/react-router@7.3.0?deps=react@19.0.0,react-dom@19.0.0",
|
||||
"antd": "https://esm.d8d.fun/antd@5.24.5?standalone&deps=react@19.0.0,react-dom@19.0.0",
|
||||
"antd/locale/zh_CN": "https://esm.d8d.fun/antd@5.24.5/locale/zh_CN?standalone&deps=react@19.0.0,react-dom@19.0.0",
|
||||
"@ant-design/icons": "https://esm.d8d.fun/@ant-design/icons@5.6.1?standalone&deps=react@19.0.0,react-dom@19.0.0",
|
||||
"@tanstack/react-query": "https://esm.d8d.fun/@tanstack/react-query@5.67.1?deps=react@19.0.0,react-dom@19.0.0",
|
||||
"axios": "https://esm.d8d.fun/axios@1.6.2",
|
||||
"dayjs": "https://esm.d8d.fun/dayjs@1.11.13",
|
||||
"dayjs/locale/zh-cn": "https://esm.d8d.fun/dayjs@1.11.13/locale/zh-cn",
|
||||
"dayjs/plugin/weekday": "https://esm.d8d.fun/dayjs@1.11.13/plugin/weekday",
|
||||
"dayjs/plugin/localeData": "https://esm.d8d.fun/dayjs@1.11.13/plugin/localeData",
|
||||
"@d8d-appcontainer/types": "https://esm.d8d.fun/@d8d-appcontainer/types@3.0.47",
|
||||
"@d8d-appcontainer/api": "https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47",
|
||||
"@ant-design/plots": "https://esm.d8d.fun/@ant-design/plots@2.1.13?deps=react@19.0.0,react-dom@19.0.0",
|
||||
"lodash": "https://esm.d8d.fun/lodash@4.17.21"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext", "deno.ns"]
|
||||
}
|
||||
}
|
||||
209
server/deno.lock
generated
Normal file
209
server/deno.lock
generated
Normal file
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"version": "4",
|
||||
"redirects": {
|
||||
"https://esm.d8d.fun/@deno/shim-deno-test@^0.5.0?target=denonext": "https://esm.d8d.fun/@deno/shim-deno-test@0.5.0?target=denonext",
|
||||
"https://esm.d8d.fun/@deno/shim-deno@~0.18.0?target=denonext": "https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext",
|
||||
"https://esm.d8d.fun/@socket.io/component-emitter@~3.1.0?target=denonext": "https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext",
|
||||
"https://esm.d8d.fun/asynckit@^0.4.0?target=denonext": "https://esm.d8d.fun/asynckit@0.4.0?target=denonext",
|
||||
"https://esm.d8d.fun/axios@^1.7.2?target=denonext": "https://esm.d8d.fun/axios@1.8.4?target=denonext",
|
||||
"https://esm.d8d.fun/bufferutil@^4.0.1?target=denonext": "https://esm.d8d.fun/bufferutil@4.0.9?target=denonext",
|
||||
"https://esm.d8d.fun/combined-stream@^1.0.8?target=denonext": "https://esm.d8d.fun/combined-stream@1.0.8?target=denonext",
|
||||
"https://esm.d8d.fun/debug?target=denonext": "https://esm.d8d.fun/debug@4.4.0?target=denonext",
|
||||
"https://esm.d8d.fun/delayed-stream@~1.0.0?target=denonext": "https://esm.d8d.fun/delayed-stream@1.0.0?target=denonext",
|
||||
"https://esm.d8d.fun/engine.io-client@~6.6.1?target=denonext": "https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext",
|
||||
"https://esm.d8d.fun/engine.io-parser@~5.2.1?target=denonext": "https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext",
|
||||
"https://esm.d8d.fun/follow-redirects@^1.15.6?target=denonext": "https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext",
|
||||
"https://esm.d8d.fun/form-data@^4.0.0?target=denonext": "https://esm.d8d.fun/form-data@4.0.2?target=denonext",
|
||||
"https://esm.d8d.fun/isexe@^3.1.1?target=denonext": "https://esm.d8d.fun/isexe@3.1.1?target=denonext",
|
||||
"https://esm.d8d.fun/jsonwebtoken@^9.0.2?target=denonext": "https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext",
|
||||
"https://esm.d8d.fun/jwa@^1.4.1?target=denonext": "https://esm.d8d.fun/jwa@1.4.1?target=denonext",
|
||||
"https://esm.d8d.fun/jws@^3.2.2?target=denonext": "https://esm.d8d.fun/jws@3.2.2?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.includes@^4.3.0?target=denonext": "https://esm.d8d.fun/lodash.includes@4.3.0?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.isboolean@^3.0.3?target=denonext": "https://esm.d8d.fun/lodash.isboolean@3.0.3?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.isinteger@^4.0.4?target=denonext": "https://esm.d8d.fun/lodash.isinteger@4.0.4?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.isnumber@^3.0.3?target=denonext": "https://esm.d8d.fun/lodash.isnumber@3.0.3?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.isplainobject@^4.0.6?target=denonext": "https://esm.d8d.fun/lodash.isplainobject@4.0.6?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.isstring@^4.0.1?target=denonext": "https://esm.d8d.fun/lodash.isstring@4.0.1?target=denonext",
|
||||
"https://esm.d8d.fun/lodash.once@^4.0.0?target=denonext": "https://esm.d8d.fun/lodash.once@4.1.1?target=denonext",
|
||||
"https://esm.d8d.fun/mime-types@^2.1.12?target=denonext": "https://esm.d8d.fun/mime-types@2.1.35?target=denonext",
|
||||
"https://esm.d8d.fun/ms@^2.1.1?target=denonext": "https://esm.d8d.fun/ms@2.1.3?target=denonext",
|
||||
"https://esm.d8d.fun/ms@^2.1.3?target=denonext": "https://esm.d8d.fun/ms@2.1.3?target=denonext",
|
||||
"https://esm.d8d.fun/nanoid@^5.1.2?target=denonext": "https://esm.d8d.fun/nanoid@5.1.5?target=denonext",
|
||||
"https://esm.d8d.fun/node-gyp-build@^4.3.0?target=denonext": "https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext",
|
||||
"https://esm.d8d.fun/proxy-from-env@^1.1.0?target=denonext": "https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext",
|
||||
"https://esm.d8d.fun/safe-buffer@^5.0.1?target=denonext": "https://esm.d8d.fun/safe-buffer@5.2.1?target=denonext",
|
||||
"https://esm.d8d.fun/semver@^7.5.4?target=denonext": "https://esm.d8d.fun/semver@7.7.1?target=denonext",
|
||||
"https://esm.d8d.fun/socket.io-client@^4.7.2?target=denonext": "https://esm.d8d.fun/socket.io-client@4.8.1?target=denonext",
|
||||
"https://esm.d8d.fun/socket.io-parser@~4.2.4?target=denonext": "https://esm.d8d.fun/socket.io-parser@4.2.4?target=denonext",
|
||||
"https://esm.d8d.fun/supports-color?target=denonext": "https://esm.d8d.fun/supports-color@10.0.0?target=denonext",
|
||||
"https://esm.d8d.fun/utf-8-validate@%3E=5.0.2?target=denonext": "https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext",
|
||||
"https://esm.d8d.fun/which@^4.0.0?target=denonext": "https://esm.d8d.fun/which@4.0.0?target=denonext",
|
||||
"https://esm.d8d.fun/ws@~8.17.1?target=denonext": "https://esm.d8d.fun/ws@8.17.1?target=denonext",
|
||||
"https://esm.d8d.fun/xmlhttprequest-ssl@~2.1.1?target=denonext": "https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext"
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.150.0/media_types/_util.ts": "ce9b4fc4ba1c447dafab619055e20fd88236ca6bdd7834a21f98bd193c3fbfa1",
|
||||
"https://deno.land/std@0.150.0/media_types/mod.ts": "2d4b6f32a087029272dc59e0a55ae3cc4d1b27b794ccf528e94b1925795b3118",
|
||||
"https://deno.land/std@0.150.0/media_types/vendor/mime-db.v1.52.0.ts": "724cee25fa40f1a52d3937d6b4fbbfdd7791ff55e1b7ac08d9319d5632c7f5af",
|
||||
"https://deno.land/x/xhr@0.3.0/mod.ts": "094aacd627fd9635cd942053bf8032b5223b909858fa9dc8ffa583752ff63b20",
|
||||
"https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47": "6f44e26f9101c9c00c374a01defa883ae9db7c851ef2b8b130cb6c2d51a41b59",
|
||||
"https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47/denonext/api.mjs": "778329c130f21a547d6726e8e13fa680a74faecd96e4d953acb481724f8db7be",
|
||||
"https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14": "4107b05a0631cc0440dce8b653ccf8e37010e67a7cb80263ac127e1a97c77dc3",
|
||||
"https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14/denonext/auth.mjs": "fafb3b08f1bfe7b0a5ecbd2856935288e8b564157d11e5157c7f217ac3f73b43",
|
||||
"https://esm.d8d.fun/@d8d-appcontainer/types@3.0.47/denonext/types.mjs": "44efd25cb0ad7942ad29d520b5f3296a908d7cefbe85382bcc45e3b3c6c9624c",
|
||||
"https://esm.d8d.fun/@deno/shim-deno-test@0.5.0/denonext/shim-deno-test.mjs": "21298ee12e8add3e8efe527aa1dd2a4fd029eb1876f5a59107bbe62b3969e282",
|
||||
"https://esm.d8d.fun/@deno/shim-deno-test@0.5.0?target=denonext": "503b73de1a14bd33782220e11fa2b33e9c87d574ac793e7addf1466c5436e66a",
|
||||
"https://esm.d8d.fun/@deno/shim-deno@0.18.2/denonext/shim-deno.mjs": "819d8ac34fdaf60658cf03d137f14adaff3f13a279ffd79cd8797d84a6ac46ab",
|
||||
"https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext": "ffa3ca347bb6b6530720158f307a2e31b16728fbb52e6432254a07d52fcbc404",
|
||||
"https://esm.d8d.fun/@socket.io/component-emitter@3.1.2/denonext/component-emitter.mjs": "3c6c5f2d64d4933b577a7117df1d8855c51ff01ab3dea8f42af1adcb1a5989e7",
|
||||
"https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext": "f6ff0f94ae3c9850a2c3a925cc2b236ec03a80fc2298d0ca48c2a90b10487db3",
|
||||
"https://esm.d8d.fun/asynckit@0.4.0/denonext/asynckit.mjs": "4ef3be6eb52c104699b90ca5524db55ec15bc76b361432f05c16b6106279ba72",
|
||||
"https://esm.d8d.fun/asynckit@0.4.0?target=denonext": "c6bd8832d6d16b648e22d124a16d33c3a7f7076e92be9444f2e4f6b27545708d",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/axios.mjs": "a0c3d648353c6a1b9864f1067ff9d366b91ccf2c8a413ec30a6b073dea05366b",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/adapters/http.mjs": "1c7e3b34ddafb39f9b36111dc81ab83a4b1dfed316efc7c9971a534f0c161e7f",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/adapters/xhr.mjs": "443f6f99410af813126a7b3b004a7fe4d3ce1eb51fb940b15a3fb0932773ff78",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/cancel/CanceledError.mjs": "6432ce6e9d09faff4439c0c5dfa1d44a79cea9901eb5c1894c7c7497e791e0fd",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/AxiosError.mjs": "2366d9c8250a030e6d82cf72db361d0df9a4311e785e2dd5dd34364c4690cbd3",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/AxiosHeaders.mjs": "f05eb0c07bf1f6d418dad3a3e310070822e5d7059e07c9cc232f365813d1f58e",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/mergeConfig.mjs": "271e93496a6e07b99694817bbcd619c4862b839f5c25538d736168697d09b846",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/defaults/transitional.mjs": "cf97aea57cebc35857e915fa922468b267947fbd8c8ee6cecc3d9878d2429b4e",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/env/data.mjs": "b7d5dde239c8a22e820d1e42b045a19d981681e0d7b3eebd3b05f8caf1523455",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/bind.mjs": "6f9b25a0abdcbdee99eea549869d4357151d21d8a004174fe5c135f82fc61412",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/parseProtocol.mjs": "3268a599f6fa29d6ce6b5e8ee2a61999351b5e414b42cc584c7ee80eae248802",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/progressEventReducer.mjs": "308616bf82ad4621ed466b2355025d108d40daac8e95831b56018fbe96c2ac1a",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/resolveConfig.mjs": "5e7d2668d1b6d93c6f860aba237e92c2b07b6f0dfedca3b9e304f1dd4578e224",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/toFormData.mjs": "9f8a95c8edd57d5e0acb598c56bfac9efae1e5ed39d6c73f94cebc603222b74d",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/platform/index.mjs": "8022c68893946f6714646f45cb87e5a8d58ac966e1d801a7abeb89f40adbb2fc",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/platform/node/classes/FormData.mjs": "3af5a3b503dafe0b26a94454685a1a68da3b78540fa1ca65a85814022d725d14",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/core/buildFullPath.mjs": "bd725f6f2e86698888721717727752302d0ae5153bf6216ba5eb6a716692fee2",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/core/settle.mjs": "131ebbb5c8592f9505988f7cc8d33b3905a5ea06425db72723d0164be0e19f0c",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/buildURL.mjs": "07eee5bbb02b63ca69bda19d9724480724b82fdc8a75a47c46a1f6dfe985f159",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/combineURLs.mjs": "a2c79317fdc707709b83dc7d3166cab69c3f9dca5ad5c4612ff1e6b29912dee8",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/isAbsoluteURL.mjs": "df75312b93206485ee501d2b51a5784b1cbf76527cb29803a7fadf1238898881",
|
||||
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/utils.mjs": "ba8669ad91f8b94b0796e13f03605a620d7fe2d05419670cfa2f6793fd5707cf",
|
||||
"https://esm.d8d.fun/axios@1.8.4?target=denonext": "3652480f46bbae591b9617f4f039537675c72ed4d8b7b44019d7bfdf173102ae",
|
||||
"https://esm.d8d.fun/buffer-equal-constant-time@1.0.1/denonext/buffer-equal-constant-time.mjs": "c83a9e435334af6863aef9f50bd7d1a855e95055ba8af6dde63eaf674791f64b",
|
||||
"https://esm.d8d.fun/bufferutil@4.0.9/denonext/bufferutil.mjs": "13dca4d5bb2c68cbe119f880fa3bd785b9a81a8e02e0834dae604b4b85295cd8",
|
||||
"https://esm.d8d.fun/bufferutil@4.0.9?target=denonext": "e32574569ab438facfcc3f412c659b0719bbf05477136ca176938c9a3ac45125",
|
||||
"https://esm.d8d.fun/combined-stream@1.0.8/denonext/combined-stream.mjs": "364b91aa4c33e5f0b4075949d93a3407b21a8695031e7c2be29999d588f9ca2c",
|
||||
"https://esm.d8d.fun/combined-stream@1.0.8?target=denonext": "a0c89b8b29494e966774c7a708e33cc2df16a0bbe2279c841d088e169e7ab3c4",
|
||||
"https://esm.d8d.fun/dayjs@1.11.13": "89c34b8b3f7b970708114b4d264c9430c30eb0c2eab1419410c77ffefa18fe2c",
|
||||
"https://esm.d8d.fun/dayjs@1.11.13/denonext/dayjs.mjs": "a6d8258bec464149ab2c9ae26e4bd3736897828586b03f8fea45403080bf8a80",
|
||||
"https://esm.d8d.fun/dayjs@1.11.13/denonext/plugin/utc.mjs": "01c663b7318d6daa10a2377306a878808a535d6dc4056fa5b60a8d31c5d2254f",
|
||||
"https://esm.d8d.fun/dayjs@1.11.13/plugin/utc": "2e41a0673e6e7c7c962983f1680911ef6feb27ded6007bc7705787ac1b2637b7",
|
||||
"https://esm.d8d.fun/debug@4.4.0": "dc29873ca5518385fcbddb2b2fa0f3b31dc6463ba52bdd790818683b9dbdc6ad",
|
||||
"https://esm.d8d.fun/debug@4.4.0/denonext/debug.mjs": "3077d1ff15cfc5b7baee65b0c00b3200aef8ab51ddddfa960972957c347c1cee",
|
||||
"https://esm.d8d.fun/debug@4.4.0?target=denonext": "dc29873ca5518385fcbddb2b2fa0f3b31dc6463ba52bdd790818683b9dbdc6ad",
|
||||
"https://esm.d8d.fun/delayed-stream@1.0.0/denonext/delayed-stream.mjs": "051a3501b7b3d3c593b78a2c7305093a8e363c518cd156f1a77117185e312abe",
|
||||
"https://esm.d8d.fun/delayed-stream@1.0.0?target=denonext": "d363b81e01f4c886114df14aa660c1a938bbb4be851ff12132260bed0db6126e",
|
||||
"https://esm.d8d.fun/ecdsa-sig-formatter@1.0.11/denonext/ecdsa-sig-formatter.mjs": "08370379943e48e31cda373711fe5528f60b3d3d992fae7c0d520ef55607640e",
|
||||
"https://esm.d8d.fun/engine.io-client@6.6.3/denonext/engine.io-client.mjs": "d127a167771015e459e79fb0eb38ee99601ae96ae98924ee407dd77d0ee8be0a",
|
||||
"https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext": "d97129d74541438ec8167b8232ff764b408b8bf4c065924c60823795fa3e038d",
|
||||
"https://esm.d8d.fun/engine.io-parser@5.2.3/denonext/engine.io-parser.mjs": "dfb40060c00806566e236f3112b950f43fa6b5e3a142f14ba2e83ad651f4a451",
|
||||
"https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext": "1dd633858ff4fd2affd2343c0f16f4d0727524919f53f0a5cf240baefd3c91fd",
|
||||
"https://esm.d8d.fun/follow-redirects@1.15.9/denonext/follow-redirects.mjs": "90866d22d80eface74d3161906835600fbb1d5c9ded05dc72fd55b40960cfce7",
|
||||
"https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext": "c8028ec9d980a1974362377614a4978d1556ba85a75bc32526e8917bede535d1",
|
||||
"https://esm.d8d.fun/form-data@4.0.2/denonext/form-data.mjs": "ad3c492eef1c6153bcfa02eb3041894e8bc8e4aa241ad4d9bd53a385f36d2c6f",
|
||||
"https://esm.d8d.fun/form-data@4.0.2?target=denonext": "83cf111a2e6f0f7b6c045508635ceae449c5d43d8d22df23693e5b996cc65899",
|
||||
"https://esm.d8d.fun/hono@4.7.4": "174d520e05dd0a3a1405b1435ee8a8cfe08b16ea80a8363879a88b8a6ff14f69",
|
||||
"https://esm.d8d.fun/hono@4.7.4/cors": "7fae6007a9ce0f5e471d3e7a9244648df6060438c74d8da418bf0cea735e17c4",
|
||||
"https://esm.d8d.fun/hono@4.7.4/deno": "8aa4c5e0f121e7cf1f754b905a40b3369315951ca2d83897b076be8752e77c09",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/cors.mjs": "b7d3ae27185d20108a146cb6f6eb3dc2d3f1010a1d5516534609a370d1e14e7e",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/deno.mjs": "55170c10ac727e9b09384d9fc05dc9d1524e89e885cd5b8bb05b2b42906866bb",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/client/utils.mjs": "e334a8ad22f831c6eda17c09ac4bc26bc8cfe16ce2d7bb7727a3519d500ec057",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/compose.mjs": "1196166f399a52036503ee70ed098af79ea34e1268233970859b2ff355bcffd1",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/context.mjs": "6eb4bfb07b2b129ec5df08ccb4ddeae28e39c91cc0a2dae5ce8e4151548d218f",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/hono.mjs": "b2701b3ab66ab91d39b5d521109c4e0cb78d88a71cdbb094ff4a5b8832548e0c",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/base.mjs": "33d27345ea175980efe2a0d4a7bf12e7c521fe9506ec081f165baf39c55a6311",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/children.mjs": "8c8ec600c04c1c7d585af870bc417ee1415dec57a83c7cc5b19c7ed779977787",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/components.mjs": "329a18bfd2823e3925aad171634839a500f88dc45cec74651046009d6ade9a89",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/constants.mjs": "a5d4f0c5a118dae37998fe58a430354c89ee5eb9206dde0b8bb0f15d3d0a3822",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/context.mjs": "ea0aeb1b21b339f96151e94418f2bf32fb5e50f320b9d8ab6273927d19e06105",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/components.mjs": "637aa2d623ae9f5ebc3109e507f8f27ef6c1ae7d4c9bbfb14ecfe8d8ce2151db",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/context.mjs": "191930e2b2129d28e9a53612d26bf6a99867370396c71d7ecb1caea3d7f7990c",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/hooks/index.mjs": "fbed9585c8f693edf92a18f2307ce7fafe36955d6899c52720d30b225d8e16a8",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/render.mjs": "44bd0364afe7b6b46df2a548e06b070777c61fc03252e7d2ae9424a50c9e21b6",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/hooks/index.mjs": "145266fb802e8fda823514b085e9653c1d0fb07ce6f67a29adbd9d16438fef23",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/intrinsic-element/common.mjs": "859e3739f0c2637d291a5aaf76f02ab0a5566a1fb693432f95f19235077849b3",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/utils.mjs": "169e8732e31a70ed1eecf31a7a1cef6c3882ad32581493d2d7812e3e62caef50",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/body.mjs": "9459ea130b33427fd1f67d9dc96900528e6ab747b296836a524c01ad6f2eb806",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/compress.mjs": "c480cdfc4b4236868fda55aba9b012ac91b4bbf237d855da2d40a8b905ee965d",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/constants.mjs": "b68b3ccd4d516d19a31f1b56a4fb65529b033deb9c8ff34d4b2e44e6e259ef72",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/handler.mjs": "bded9286b7e46345840114eb8e2a8e627ee4fffda3044163fd78d1578b82f26c",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/html.mjs": "9949836effcbff234d47fed244e8fa2c3d0a00b89c55767a16c15b7b35e12006",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/mime.mjs": "0f1f71d357f5b2b15dbe026c4d65219a78d3dd922d799673aed798f1ff18e981",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/url.mjs": "aa55b9a667876655d972b7e086ff11c05ae9327ef29f174b87d2ce87791fa0fd",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/hono-base.mjs": "29af9473270ed537bced9d32bc38a858353fe2dba647c1e7ba63dfa98d33838f",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/hono.mjs": "4d667dd609e6792e5447ea24c718ea906617b8d150675895a04ee4ba7e5e1c52",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/html.mjs": "02391a2fc736bf5b27588e34948b63581526effc738be6d05721bb7ac334d51a",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx.mjs": "7e884cd71ecba137dbeda14ba974dd7eecd870240816803883e2a7f6c06f4c2b",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/dom/jsx-dev-runtime.mjs": "a02348636d71685c20a9186e66e5f455bdb1999d2bc980e4a4f1bc83dcd7484f",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/dom/jsx-runtime.mjs": "32f78a1287f90a8afdf0c0d316af28e9b892401b19734bcabfd59f4e5fe9218c",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/jsx-dev-runtime.mjs": "9ce39e3bc5cd0188ed1ef8f4e71ab18560b29a3ea179cd647a404749a6bd0576",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/jsx-runtime.mjs": "e84ac4b5216e98c72733b1754acd23349be44a28c929f23ee121263fd11317d5",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/streaming.mjs": "a0745c3b7b37d44be9139b4879e7ccfcaaeb12ec10e7880a5c66829c0f59928e",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/request.mjs": "5e2499f74cad11ede4fd777c22479bfb2161c86e81435f20b205b9d3d02223b4",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/router.mjs": "51c7a9987e81971338d9e342a305a4c2ce231e7ab6d398aa6a0b74f46443f63b",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/router/reg-exp-router.mjs": "56a9c48e0ec3a264170db8de1c0342bec844ace172cab882223fcf809c0ce0f6",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/router/smart-router.mjs": "69dec2229dadfda412d6736719104ed3dceabe667ddff6804a028cb9650b990b",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/router/trie-router.mjs": "13d18e17ea528e9bfb85cec0221a2da76d9c18b9fe44db8caee98cb51b357234",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/serve-static.mjs": "0e233f7b5b78374defa5281a05469d326444a8c7894ac36c1ae99e5b1412619d",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/ssg.mjs": "c9e8cf200027b7c69c8a97d7bf96b64b09ca3b810198958c3f69a3dc18492730",
|
||||
"https://esm.d8d.fun/hono@4.7.4/denonext/ws.mjs": "896fdef949b92bb10c16ebb1b7977babc3c87d3db43e329a66fcd9a786f8c933",
|
||||
"https://esm.d8d.fun/hono@4.7.4/jsx": "8fb76b6158726b27e0e9be1db08b831967bd1bd8175d98f75652440254f2b0cf",
|
||||
"https://esm.d8d.fun/hono@4.7.4/jsx/jsx-runtime": "add6d23136559fd64a92cab007bc2afa9450ad1773e0cd8e8cfdbf0c97d339d3",
|
||||
"https://esm.d8d.fun/isexe@3.1.1/denonext/isexe.mjs": "59b58a950d33368749f8b2f0df8377ded09f5f30c276f79239a2542029e77a43",
|
||||
"https://esm.d8d.fun/isexe@3.1.1/denonext/posix.mjs": "f99f8d2aacd0b5424625cee480f36b47639cfbad44c64b7b21cbba18ad77a1b2",
|
||||
"https://esm.d8d.fun/isexe@3.1.1/denonext/win32.mjs": "f52981ee6555549c246db8e9e6c0ee1e2947a35367c3bcec0ba31834387991c0",
|
||||
"https://esm.d8d.fun/isexe@3.1.1?target=denonext": "b3c61e7e70b9d56865de461fbcdae702ebf93743143457079a15a60e30dfcf83",
|
||||
"https://esm.d8d.fun/jsonwebtoken@9.0.2/denonext/jsonwebtoken.mjs": "4365e66f5fa04a6903d106e17166fd93ddef582caf66f19d281dc066711c4f6e",
|
||||
"https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext": "76a17587b04be9dfee91197bb0119b0be3a6a53ec05cc63d44292077bc03c0f7",
|
||||
"https://esm.d8d.fun/jwa@1.4.1/denonext/jwa.mjs": "fb2de82e653ed6ed5432adb25567772ea9044f241d209c569b23eee07ab50176",
|
||||
"https://esm.d8d.fun/jwa@1.4.1?target=denonext": "10b18f9707bf3a08b740be74a3bf349ad0e483cf3c5bfa305a023a8c5cb0056e",
|
||||
"https://esm.d8d.fun/jws@3.2.2/denonext/jws.mjs": "ed1560f6cd91af6f57bdf5132ffc6a126619d003c5d5e7680fb0b310dbf61ad8",
|
||||
"https://esm.d8d.fun/jws@3.2.2?target=denonext": "e5b49eed4c39c26f02dafda1f75776a1ed468df4b36f8eb0f5a36e036a7af0f2",
|
||||
"https://esm.d8d.fun/lodash.includes@4.3.0/denonext/lodash.includes.mjs": "7891d8e1db31f196dad13cfea5d8c9cd7b57e556e852bde44a32b9c95f52f769",
|
||||
"https://esm.d8d.fun/lodash.includes@4.3.0?target=denonext": "52552ec9e8b0ee8dff8bd6743edb4ff57c1c1dc31fe13061c1fd6b023e26c0dd",
|
||||
"https://esm.d8d.fun/lodash.isboolean@3.0.3/denonext/lodash.isboolean.mjs": "62346e63e84456d408ad9cdd38dc122ea4fecef67b7682c9ef311a9d9376ebb4",
|
||||
"https://esm.d8d.fun/lodash.isboolean@3.0.3?target=denonext": "df362bed13151ed14dc4fea8a56582f7d5165ab057225f20f7a19933bc030603",
|
||||
"https://esm.d8d.fun/lodash.isinteger@4.0.4/denonext/lodash.isinteger.mjs": "661e55e1960486dc985a2dd833c942f79874d2c3d82d89b62d92a7c2d4fb1976",
|
||||
"https://esm.d8d.fun/lodash.isinteger@4.0.4?target=denonext": "1355063df9b6e29a874a0f22874b86abcb7520182efcd5e543ded4209ff4485c",
|
||||
"https://esm.d8d.fun/lodash.isnumber@3.0.3/denonext/lodash.isnumber.mjs": "406375117efebf18a311f7b9a9cde5b961f2b96589c6e70e7805ae8f9b233186",
|
||||
"https://esm.d8d.fun/lodash.isnumber@3.0.3?target=denonext": "129cd84408b3f202f339cfbde411d7e3573414128574123b94375b330cfd02b4",
|
||||
"https://esm.d8d.fun/lodash.isplainobject@4.0.6/denonext/lodash.isplainobject.mjs": "52baba5dc85a16aa38f54fcbbbf47ffe8b3c9d84e788a5199a20eab9135dd35a",
|
||||
"https://esm.d8d.fun/lodash.isplainobject@4.0.6?target=denonext": "aaf731d53ff27671d17a5e700371eb7d073496e7c099a537c05dd1df6606a54e",
|
||||
"https://esm.d8d.fun/lodash.isstring@4.0.1/denonext/lodash.isstring.mjs": "27f803808b3f189fa994d1b3784e13b2978976bf17c06050ee311e7f54b0db32",
|
||||
"https://esm.d8d.fun/lodash.isstring@4.0.1?target=denonext": "dc33fab7958cad340a76cc58bd2f30ad22ee63b00eac0bc2d9a4110049cdfddc",
|
||||
"https://esm.d8d.fun/lodash.once@4.1.1/denonext/lodash.once.mjs": "d98a09cbf4f8520f256be230ba50e9cae609b73bc8ffd2f6bf388099ea2f50b1",
|
||||
"https://esm.d8d.fun/lodash.once@4.1.1?target=denonext": "46682cfe5d75f572d14c6a3c4c5da23d597d80d0552fea124b72a2359bb4b37d",
|
||||
"https://esm.d8d.fun/mime-db@1.52.0/denonext/mime-db.mjs": "f93feb3d7150014b71bd0d06c5bd819db56a089b31b8b79a3b0466bb37ef005e",
|
||||
"https://esm.d8d.fun/mime-types@2.1.35/denonext/mime-types.mjs": "704bdb318816fe1360c90a196f7cb3ba6e25fe207707cc2df873f890ad2e5f44",
|
||||
"https://esm.d8d.fun/mime-types@2.1.35?target=denonext": "e4cc9a1aabecc1be22d194375ec3b99cc9d51700cc4629ab689975451c0a8ce5",
|
||||
"https://esm.d8d.fun/ms@2.1.3/denonext/ms.mjs": "9039464da1f4ae1c2042742d335c82556c048bbe49449b5d0cd5198193afa147",
|
||||
"https://esm.d8d.fun/ms@2.1.3?target=denonext": "36f5aa7503ff0ff44ce9e3155a60362d8d3ae5db8db048be5764a3a515b6a263",
|
||||
"https://esm.d8d.fun/nanoid@5.1.5/denonext/nanoid.mjs": "dc919f2d7339a244f732a0cf02e3962dd1289535668026f52fb26bd593e9358b",
|
||||
"https://esm.d8d.fun/nanoid@5.1.5?target=denonext": "33ad5b17f1290cb850164cfcf30f642d9dad489ba19909bc3cfd9eb78369f451",
|
||||
"https://esm.d8d.fun/node-gyp-build@4.8.4/denonext/node-gyp-build.mjs": "9a86f2d044fc77bd60aaa3d697c2ba1b818da5fb1b9aaeedec59a40b8e908803",
|
||||
"https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext": "261a6cedf1fdbf159798141ba1e2311ac1510682c5c8b55dacc8cf5fdee4aa06",
|
||||
"https://esm.d8d.fun/proxy-from-env@1.1.0/denonext/proxy-from-env.mjs": "f60f9c79fc3baa07c13c800798d645ae70d1b2059b8d593dcd4f8c5710b50333",
|
||||
"https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext": "bf02a050a1a6aa56ddba25dbea2c355da294630e5c5520fddea4b2f30a9292bc",
|
||||
"https://esm.d8d.fun/safe-buffer@5.2.1/denonext/safe-buffer.mjs": "51b088d69d0bbf6d7ce4179853887e105715df40e432a3bff0e9575cc2285276",
|
||||
"https://esm.d8d.fun/safe-buffer@5.2.1?target=denonext": "34028b9647c849fa96dfd3d9f217a3adca8b43b13409820ac3f43fb15eba3e20",
|
||||
"https://esm.d8d.fun/semver@7.7.1/denonext/semver.mjs": "f1d8c45a097d5f2da9662be5ff2087f1e4af9ebeb9b0c2eeeb0c90d74fa7a14c",
|
||||
"https://esm.d8d.fun/semver@7.7.1?target=denonext": "7d6e1f9de61981f17d0e5153d48b77475e3433225ce9265ad77206afe216c5c8",
|
||||
"https://esm.d8d.fun/socket.io-client@4.8.1/denonext/socket.io-client.mjs": "b902dafad93171849d6d6e9e98bfa5357513089e43b0fbf9268d394f0839f372",
|
||||
"https://esm.d8d.fun/socket.io-client@4.8.1?target=denonext": "f5543108c5018ca5904af75985dc9ff7b7210334782408cf87bdf091ce1fbf2e",
|
||||
"https://esm.d8d.fun/socket.io-parser@4.2.4/denonext/socket.io-parser.mjs": "a989568a92fa45870a4ae74fb731c5e554ef6c901b97f154d8c84267f7d5aaba",
|
||||
"https://esm.d8d.fun/socket.io-parser@4.2.4?target=denonext": "95bc48ccd83940940fb68cf3401280667a8bad2b6abc8a4c7bb5c39ec59aff16",
|
||||
"https://esm.d8d.fun/supports-color@10.0.0/denonext/supports-color.mjs": "239cd39d0828e1a018dee102748da869b1b75c38fe6a9c0c8f0bd4ffbd3e1ea1",
|
||||
"https://esm.d8d.fun/supports-color@10.0.0?target=denonext": "4895255248e4ba0cbcce9437003dccf3658b1ac1d1e8eba5225fb8194c454ee1",
|
||||
"https://esm.d8d.fun/utf-8-validate@6.0.5/denonext/utf-8-validate.mjs": "90c0c88a13bc4749b497361480d618bf4809153f5d5ba694fac79ae9dbf634a9",
|
||||
"https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext": "071bc33ba1a58297e23a34d69dd589fd06df04b0f373b382ff5da544a623f271",
|
||||
"https://esm.d8d.fun/which@4.0.0/denonext/which.mjs": "9f47207c6dc9684fe3d852f2290c474577babaeabf60616652630c0b90421a53",
|
||||
"https://esm.d8d.fun/which@4.0.0?target=denonext": "50b06c1a68e3ef88dc8e2c68c17b732a6d1917000d5d59637496da3b61549c8e",
|
||||
"https://esm.d8d.fun/ws@8.17.1/denonext/ws.mjs": "7b349f9bcf5af35a422b01ece5189ac693f84f07cc2e9be12023ec818a18ba71",
|
||||
"https://esm.d8d.fun/ws@8.17.1?target=denonext": "3c5e4dca1be73c0e7776cb033809d16c2421b785cd1b93827b76a43c5b59a0bd",
|
||||
"https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2/denonext/xmlhttprequest-ssl.mjs": "5cb537aeb44e2971f9d84c4e22e0d24ea0554eb6c33a5d10a46cf163debf60ec",
|
||||
"https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext": "5a4574293c501f0f0da3ddd653bd5d9ac00ea59647e3b20694cc05ed02e7a22f"
|
||||
}
|
||||
}
|
||||
403
server/migrations.ts
Normal file
403
server/migrations.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import type { MigrationLiveDefinition } from '@d8d-appcontainer/types'
|
||||
|
||||
import {
|
||||
EnableStatus, DeleteStatus,
|
||||
AuditStatus, ThemeMode, FontSize, CompactMode,
|
||||
SystemSettingKey,
|
||||
SystemSettingGroup,
|
||||
ALLOWED_FILE_TYPES,
|
||||
} from '../client/share/types.ts';
|
||||
|
||||
// 定义用户表迁移
|
||||
const createUsersTable: MigrationLiveDefinition = {
|
||||
name: "create_users_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('users', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('username').unique().notNullable();
|
||||
table.string('password').notNullable();
|
||||
table.string('phone').unique();
|
||||
table.string('email').unique();
|
||||
table.string('nickname');
|
||||
table.string('name');
|
||||
table.integer('is_disabled').defaultTo(0);
|
||||
table.integer('is_deleted').defaultTo(0);
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('username');
|
||||
table.index('is_disabled');
|
||||
table.index('is_deleted');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('users');
|
||||
}
|
||||
}
|
||||
|
||||
// 定义登录历史表迁移
|
||||
const createLoginHistoryTable: MigrationLiveDefinition = {
|
||||
name: "create_login_history_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('login_history', (table) => {
|
||||
table.increments('id').primary()
|
||||
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE')
|
||||
table.timestamp('login_time').defaultTo(api.fn.now())
|
||||
table.string('ip_address')
|
||||
table.text('user_agent')
|
||||
table.decimal('longitude', 10, 6).nullable() // 经度
|
||||
table.decimal('latitude', 10, 6).nullable() // 纬度
|
||||
table.string('location_name').nullable() // 地点名称
|
||||
|
||||
// 添加索引
|
||||
table.index('user_id');
|
||||
table.index('login_time');
|
||||
// table.index(['longitude', 'latitude']);
|
||||
})
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('login_history')
|
||||
}
|
||||
}
|
||||
|
||||
// 定义知识库文章表迁移
|
||||
const createKnowInfoTable: MigrationLiveDefinition = {
|
||||
name: "create_know_info_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('know_info', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('title').comment('文章标题');
|
||||
table.string('tags').comment('文章标签');
|
||||
table.text('content').comment('文章内容');
|
||||
table.string('author').comment('作者');
|
||||
table.string('category').comment('分类');
|
||||
table.string('cover_url').comment('封面图片URL');
|
||||
table.integer('audit_status').defaultTo(AuditStatus.PENDING).comment('审核状态');
|
||||
table.integer('is_deleted').defaultTo(0).comment('是否被删除 (0否 1是)');
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('title');
|
||||
table.index('tags');
|
||||
table.index('author');
|
||||
table.index('category');
|
||||
table.index('audit_status');
|
||||
table.index('is_deleted');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('know_info');
|
||||
}
|
||||
};
|
||||
|
||||
// 定义文件分类表迁移
|
||||
const createFileCategoryTable: MigrationLiveDefinition = {
|
||||
name: "create_file_category_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('file_categories', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('name').notNullable().comment('分类名称');
|
||||
table.string('code').notNullable().unique().comment('分类编码');
|
||||
table.text('description').comment('分类描述');
|
||||
table.integer('is_deleted').defaultTo(DeleteStatus.NOT_DELETED).comment('是否被删除 (0否 1是)');
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('name');
|
||||
table.index('code');
|
||||
table.index('is_deleted');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('file_categories');
|
||||
}
|
||||
};
|
||||
|
||||
// 定义文件库表迁移
|
||||
const createFileLibraryTable: MigrationLiveDefinition = {
|
||||
name: "create_file_library_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('file_library', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('file_name').notNullable().comment('文件名称');
|
||||
table.string('original_filename').comment('原始文件名');
|
||||
table.string('file_path').notNullable().comment('文件路径');
|
||||
table.string('file_type').comment('文件类型');
|
||||
table.integer('file_size').unsigned().comment('文件大小(字节)');
|
||||
table.integer('uploader_id').unsigned().references('id').inTable('users').onDelete('SET NULL').comment('上传用户ID');
|
||||
table.string('uploader_name').comment('上传者名称');
|
||||
table.integer('category_id').unsigned().references('id').inTable('file_categories').onDelete('SET NULL').comment('文件分类');
|
||||
table.string('tags').comment('文件标签');
|
||||
table.text('description').comment('文件描述');
|
||||
table.integer('download_count').defaultTo(0).comment('下载次数');
|
||||
table.integer('is_disabled').defaultTo(EnableStatus.DISABLED).comment('是否禁用 (0否 1是)');
|
||||
table.integer('is_deleted').defaultTo(DeleteStatus.NOT_DELETED).comment('是否被删除 (0否 1是)');
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('file_name');
|
||||
table.index('file_type');
|
||||
table.index('category_id');
|
||||
table.index('uploader_id');
|
||||
table.index('is_deleted');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('file_library');
|
||||
}
|
||||
};
|
||||
|
||||
// 定义主题设置表迁移
|
||||
const createThemeSettingsTable: MigrationLiveDefinition = {
|
||||
name: "create_theme_settings_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('theme_settings', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||
table.jsonb('settings').comment('主题设置');
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('user_id');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('theme_settings');
|
||||
}
|
||||
};
|
||||
|
||||
// 定义系统设置表迁移
|
||||
const createSystemSettingsTable: MigrationLiveDefinition = {
|
||||
name: "create_system_settings_table",
|
||||
up: async (api) => {
|
||||
await api.schema.createTable('system_settings', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('key').notNullable().unique().comment('设置键');
|
||||
table.text('value').notNullable().comment('设置值');
|
||||
table.string('description').nullable().comment('设置描述');
|
||||
table.string('group').notNullable().comment('设置分组');
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 添加索引
|
||||
table.index('key');
|
||||
table.index('group');
|
||||
});
|
||||
},
|
||||
down: async (api) => {
|
||||
await api.schema.dropTable('system_settings');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始测试数据迁移
|
||||
const seedInitialData: MigrationLiveDefinition = {
|
||||
name: "seed_initial_data",
|
||||
up: async (api) => {
|
||||
// 1. 添加默认用户
|
||||
const defaultUser = {
|
||||
username: 'admin',
|
||||
password: 'admin123', // 实际应用中应使用加密后的密码
|
||||
email: 'admin@example.com',
|
||||
nickname: '系统管理员',
|
||||
name: '管理员',
|
||||
is_disabled: EnableStatus.ENABLED,
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
};
|
||||
|
||||
const [userId] = await api.table('users').insert(defaultUser);
|
||||
|
||||
// 2. 添加默认主题设置
|
||||
await api.table('theme_settings').insert({
|
||||
user_id: userId,
|
||||
settings: {
|
||||
theme_mode: ThemeMode.LIGHT,
|
||||
primary_color: '#1890ff',
|
||||
font_size: FontSize.MEDIUM,
|
||||
is_compact: CompactMode.NORMAL
|
||||
},
|
||||
created_at: api.fn.now(),
|
||||
updated_at: api.fn.now()
|
||||
});
|
||||
|
||||
// 3. 添加知识库文章示例
|
||||
await api.table('know_info').insert([
|
||||
{
|
||||
title: '欢迎使用应用Starter',
|
||||
tags: 'starter,指南',
|
||||
content: '# 欢迎使用应用Starter\n\n这是一个基础的应用Starter,提供了用户认证、文件管理、知识库、主题管理等功能。\n\n## 主要功能\n\n- 用户认证与管理\n- 文件上传与管理\n- 知识库文章管理\n- 主题设置(暗黑模式/明亮模式)\n- 图表数据统计\n- 地图集成\n\n更多功能请参考文档...',
|
||||
author: '系统管理员',
|
||||
category: '使用指南',
|
||||
audit_status: AuditStatus.APPROVED,
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
title: '如何使用文件管理',
|
||||
tags: '文件,上传,管理',
|
||||
content: '# 文件管理使用指南\n\n文件管理模块可以帮助您上传、分类和管理各种文件。\n\n## 上传文件\n\n1. 点击"上传文件"按钮\n2. 选择要上传的文件\n3. 填写文件信息(分类、标签等)\n4. 点击"确定"完成上传\n\n## 文件分类\n\n您可以创建自定义的文件分类,方便管理不同类型的文件...',
|
||||
author: '系统管理员',
|
||||
category: '使用指南',
|
||||
audit_status: AuditStatus.APPROVED,
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
title: '主题设置指南',
|
||||
tags: '主题,设置,外观',
|
||||
content: '# 主题设置指南\n\n主题设置允许您自定义应用的外观和感觉,包括颜色模式、字体大小等。\n\n## 颜色模式\n\n您可以选择明亮模式或暗黑模式,适应不同的工作环境和个人偏好。\n\n## 主题颜色\n\n可以选择主题的主色调,系统会根据选择自动生成配色方案。\n\n## 字体大小\n\n提供小、中、大三种字体大小选项,满足不同用户的阅读需求。',
|
||||
author: '系统管理员',
|
||||
category: '使用指南',
|
||||
audit_status: AuditStatus.APPROVED,
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
title: '数据分析功能介绍',
|
||||
tags: '分析,图表,数据',
|
||||
content: '# 数据分析功能介绍\n\n数据分析模块提供了多种图表和可视化工具,帮助您理解和分析数据。\n\n## 图表类型\n\n支持柱状图、折线图、饼图等多种图表类型,适用于不同的数据展示需求。\n\n## 数据筛选\n\n可以根据时间范围、数据类型等条件筛选数据,获得更精确的分析结果。',
|
||||
author: '系统管理员',
|
||||
category: '使用指南',
|
||||
audit_status: AuditStatus.APPROVED,
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
}
|
||||
]);
|
||||
|
||||
// 4. 添加文件分类示例
|
||||
await api.table('file_categories').insert([
|
||||
{
|
||||
name: '文档',
|
||||
code: 'doc',
|
||||
description: '各类文档文件',
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
name: '图片',
|
||||
code: 'image',
|
||||
description: '各类图片文件',
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
name: '视频',
|
||||
code: 'video',
|
||||
description: '各类视频文件',
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
name: '音频',
|
||||
code: 'audio',
|
||||
description: '各类音频文件',
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
},
|
||||
{
|
||||
name: '其他',
|
||||
code: 'other',
|
||||
description: '其他类型文件',
|
||||
is_deleted: DeleteStatus.NOT_DELETED
|
||||
}
|
||||
]);
|
||||
|
||||
// 5. 添加系统设置示例
|
||||
await api.table('system_settings').insert([
|
||||
{
|
||||
key: SystemSettingKey.SITE_NAME,
|
||||
value: '应用管理系统',
|
||||
description: '站点名称',
|
||||
group: SystemSettingGroup.BASIC
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.SITE_DESCRIPTION,
|
||||
value: '一个功能完善的应用管理系统',
|
||||
description: '站点描述',
|
||||
group: SystemSettingGroup.BASIC
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.SITE_KEYWORDS,
|
||||
value: '应用,管理,系统',
|
||||
description: '站点关键词',
|
||||
group: SystemSettingGroup.BASIC
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.ENABLE_REGISTER,
|
||||
value: true,
|
||||
description: '是否开启注册功能',
|
||||
group: SystemSettingGroup.FEATURE
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.ENABLE_CAPTCHA,
|
||||
value: true,
|
||||
description: '是否开启验证码',
|
||||
group: SystemSettingGroup.FEATURE
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.LOGIN_ATTEMPTS,
|
||||
value: 5,
|
||||
description: '允许的登录尝试次数',
|
||||
group: SystemSettingGroup.FEATURE
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.SESSION_TIMEOUT,
|
||||
value: 120,
|
||||
description: '会话超时时间(分钟)',
|
||||
group: SystemSettingGroup.FEATURE
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.UPLOAD_MAX_SIZE,
|
||||
value: 10,
|
||||
description: '最大上传大小(MB)',
|
||||
group: SystemSettingGroup.UPLOAD
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.ALLOWED_FILE_TYPES,
|
||||
value: ALLOWED_FILE_TYPES,
|
||||
description: '允许上传的文件类型',
|
||||
group: SystemSettingGroup.UPLOAD
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.IMAGE_COMPRESS,
|
||||
value: true,
|
||||
description: '是否压缩图片',
|
||||
group: SystemSettingGroup.UPLOAD
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.IMAGE_MAX_WIDTH,
|
||||
value: 1920,
|
||||
description: '图片最大宽度',
|
||||
group: SystemSettingGroup.UPLOAD
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.NOTIFY_ON_LOGIN,
|
||||
value: true,
|
||||
description: '是否开启登录通知',
|
||||
group: SystemSettingGroup.NOTIFICATION
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.NOTIFY_ON_UPLOAD,
|
||||
value: true,
|
||||
description: '是否开启上传通知',
|
||||
group: SystemSettingGroup.NOTIFICATION
|
||||
},
|
||||
{
|
||||
key: SystemSettingKey.NOTIFY_ON_ERROR,
|
||||
value: true,
|
||||
description: '是否开启错误通知',
|
||||
group: SystemSettingGroup.NOTIFICATION
|
||||
}
|
||||
]);
|
||||
},
|
||||
down: async (api) => {
|
||||
// 删除初始数据
|
||||
await api.table('login_history').where('user_id', 1).delete();
|
||||
await api.table('theme_settings').where('user_id', 1).delete();
|
||||
await api.table('know_info').delete();
|
||||
await api.table('file_categories').delete();
|
||||
await api.table('users').where('username', 'admin').delete();
|
||||
}
|
||||
};
|
||||
|
||||
// 导出所有迁移
|
||||
export const migrations = [
|
||||
createUsersTable,
|
||||
createLoginHistoryTable,
|
||||
createKnowInfoTable,
|
||||
createFileCategoryTable,
|
||||
createFileLibraryTable,
|
||||
createThemeSettingsTable,
|
||||
createSystemSettingsTable,
|
||||
seedInitialData
|
||||
];
|
||||
103
server/routes_auth.ts
Normal file
103
server/routes_auth.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Hono } from 'hono'
|
||||
import type { Variables } from './app.tsx'
|
||||
import type { WithAuth } from './app.tsx'
|
||||
|
||||
export function createAuthRoutes(withAuth: WithAuth) {
|
||||
const authRoutes = new Hono<{ Variables: Variables }>()
|
||||
|
||||
// 登录状态检查
|
||||
authRoutes.get('/status', async (c) => {
|
||||
try {
|
||||
const auth = c.get('auth')
|
||||
const token = c.req.header('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return c.json({ isValid: false }, 200)
|
||||
}
|
||||
|
||||
const status = await auth.checkLoginStatus(token)
|
||||
return c.json(status)
|
||||
} catch (error) {
|
||||
console.error('登录状态检查失败:', error)
|
||||
return c.json({ isValid: false, error: '登录状态检查失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 注册
|
||||
authRoutes.post('/register', async (c) => {
|
||||
try {
|
||||
const auth = c.get('auth')
|
||||
const { username, email, password } = await c.req.json()
|
||||
|
||||
if (!username || !password) {
|
||||
return c.json({ error: '用户名和密码不能为空' }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.createUser({ username, password, email })
|
||||
const result = await auth.authenticate(username, password)
|
||||
|
||||
return c.json({
|
||||
message: '注册成功',
|
||||
user: result.user
|
||||
}, 201)
|
||||
} catch (authError) {
|
||||
return c.json({ error: '用户已存在或注册失败' }, 400)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
return c.json({ error: '注册失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 登录
|
||||
authRoutes.post('/login', async (c) => {
|
||||
try {
|
||||
const auth = c.get('auth')
|
||||
const { username, password } = await c.req.json()
|
||||
|
||||
if (!username || !password) {
|
||||
return c.json({ error: '用户名和密码不能为空' }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await auth.authenticate(username, password)
|
||||
|
||||
if (result.user) {
|
||||
const apiClient = c.get('apiClient')
|
||||
await apiClient.database.insert('login_history', {
|
||||
user_id: result.user.id,
|
||||
login_time: apiClient.database.fn.now(),
|
||||
ip_address: c.req.header('x-forwarded-for') || '未知',
|
||||
user_agent: c.req.header('user-agent') || '未知'
|
||||
})
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: '登录成功',
|
||||
token: result.token,
|
||||
refreshToken: result.refreshToken,
|
||||
user: result.user
|
||||
})
|
||||
} catch (authError) {
|
||||
return c.json({ error: '用户名或密码错误' }, 401)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
return c.json({ error: '登录失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前用户信息
|
||||
authRoutes.get('/me', withAuth, (c) => {
|
||||
const user = c.get('user')
|
||||
return c.json(user)
|
||||
})
|
||||
|
||||
// 登出
|
||||
authRoutes.post('/logout', async (c) => {
|
||||
return c.json({ message: '登出成功' })
|
||||
})
|
||||
|
||||
return authRoutes
|
||||
}
|
||||
180
server/routes_charts.ts
Normal file
180
server/routes_charts.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Hono } from "hono";
|
||||
import debug from "debug";
|
||||
import {
|
||||
DeleteStatus,
|
||||
} from "../client/share/types.ts";
|
||||
|
||||
import type { Variables, WithAuth } from "./app.tsx";
|
||||
|
||||
const log = {
|
||||
api: debug("api:sys"),
|
||||
};
|
||||
|
||||
|
||||
// 创建图表数据路由
|
||||
export function createChartRoutes(withAuth: WithAuth) {
|
||||
const chartRoutes = new Hono<{ Variables: Variables }>();
|
||||
|
||||
// 获取用户活跃度图表数据
|
||||
chartRoutes.get("/user-activity", withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 获取过去30天的数据
|
||||
const days = 30;
|
||||
const result = [];
|
||||
|
||||
// 当前日期
|
||||
const currentDate = new Date();
|
||||
|
||||
// 生成过去30天的日期范围
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(currentDate.getDate() - i);
|
||||
|
||||
// 格式化日期为 YYYY-MM-DD
|
||||
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// 查询当天的登录次数
|
||||
const loginCount = await apiClient.database
|
||||
.table('login_history')
|
||||
.whereRaw(`DATE(login_time) = ?`, [formattedDate])
|
||||
.count();
|
||||
|
||||
result.push({
|
||||
date: formattedDate,
|
||||
count: Number(loginCount),
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: "获取用户活跃度数据成功",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取用户活跃度数据失败:", error);
|
||||
return c.json({ error: "获取用户活跃度数据失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取文件上传统计图表数据
|
||||
chartRoutes.get("/file-uploads", withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 获取过去12个月的数据
|
||||
const months = 12;
|
||||
const result = [];
|
||||
|
||||
// 当前日期
|
||||
const currentDate = new Date();
|
||||
|
||||
// 生成过去12个月的月份范围
|
||||
for (let i = months - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setMonth(currentDate.getMonth() - i);
|
||||
|
||||
// 获取年月
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
|
||||
// 月份标签
|
||||
const monthLabel = `${year}-${String(month).padStart(2, '0')}`;
|
||||
|
||||
// 查询当月的文件上传数量
|
||||
const fileCount = await apiClient.database
|
||||
.table('file_library')
|
||||
.whereRaw(`YEAR(created_at) = ? AND MONTH(created_at) = ?`, [year, month])
|
||||
.count();
|
||||
|
||||
result.push({
|
||||
month: monthLabel,
|
||||
count: Number(fileCount),
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: "获取文件上传统计数据成功",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取文件上传统计数据失败:", error);
|
||||
return c.json({ error: "获取文件上传统计数据失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取文件类型分布图表数据
|
||||
chartRoutes.get("/file-types", withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 查询不同文件类型的数量
|
||||
const fileTypeStats = await apiClient.database
|
||||
.table('file_library')
|
||||
.select('file_type',apiClient.database.raw('count(id) as count'))
|
||||
.where('is_deleted', DeleteStatus.NOT_DELETED)
|
||||
.groupBy('file_type');
|
||||
|
||||
// 将结果转换为饼图所需格式
|
||||
const result = fileTypeStats.map(item => ({
|
||||
type: item.file_type || '未知',
|
||||
value: Number(item.count),
|
||||
}));
|
||||
|
||||
return c.json({
|
||||
message: "获取文件类型分布数据成功",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取文件类型分布数据失败:", error);
|
||||
return c.json({ error: "获取文件类型分布数据失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取仪表盘概览数据
|
||||
chartRoutes.get("/dashboard-overview", withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 获取用户总数
|
||||
const userCount = await apiClient.database
|
||||
.table('users')
|
||||
.where('is_deleted', DeleteStatus.NOT_DELETED)
|
||||
.count();
|
||||
|
||||
// 获取文件总数
|
||||
const fileCount = await apiClient.database
|
||||
.table('file_library')
|
||||
.where('is_deleted', DeleteStatus.NOT_DELETED)
|
||||
.count();
|
||||
|
||||
// 获取知识库文章总数
|
||||
const articleCount = await apiClient.database
|
||||
.table('know_info')
|
||||
.where('is_deleted', DeleteStatus.NOT_DELETED)
|
||||
.count();
|
||||
|
||||
// 获取今日登录次数
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayLoginCount = await apiClient.database
|
||||
.table('login_history')
|
||||
.whereRaw(`DATE(login_time) = ?`, [today])
|
||||
.count();
|
||||
|
||||
return c.json({
|
||||
message: "获取仪表盘概览数据成功",
|
||||
data: {
|
||||
userCount: Number(userCount),
|
||||
fileCount: Number(fileCount),
|
||||
articleCount: Number(articleCount),
|
||||
todayLoginCount: Number(todayLoginCount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取仪表盘概览数据失败:", error);
|
||||
return c.json({ error: "获取仪表盘概览数据失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return chartRoutes;
|
||||
}
|
||||
161
server/routes_maps.ts
Normal file
161
server/routes_maps.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Hono } from "hono";
|
||||
import debug from "debug";
|
||||
|
||||
import type { Variables, WithAuth } from "./app.tsx";
|
||||
|
||||
const log = {
|
||||
api: debug("api:sys"),
|
||||
};
|
||||
|
||||
// 创建地图数据路由
|
||||
export function createMapRoutes(withAuth: WithAuth) {
|
||||
const mapRoutes = new Hono<{ Variables: Variables }>();
|
||||
|
||||
// 获取地图标记点数据
|
||||
mapRoutes.get("/markers", withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 从登录历史表中查询有经纬度的登录记录
|
||||
const locations = await apiClient.database
|
||||
.table('login_history')
|
||||
.select(
|
||||
'id',
|
||||
'user_id',
|
||||
'location_name',
|
||||
'longitude',
|
||||
'latitude',
|
||||
'login_time',
|
||||
'ip_address'
|
||||
)
|
||||
.whereNotNull('longitude')
|
||||
.whereNotNull('latitude')
|
||||
.orderBy('login_time', 'desc')
|
||||
.limit(100); // 限制返回最近100条记录
|
||||
|
||||
// 获取相关用户信息
|
||||
const userIds = [...new Set(locations.map(loc => loc.user_id))];
|
||||
const users = await apiClient.database
|
||||
.table('users')
|
||||
.select('id', 'username', 'nickname')
|
||||
.whereIn('id', userIds);
|
||||
|
||||
// 构建用户信息映射
|
||||
const userMap = new Map(users.map(user => [user.id, user]));
|
||||
|
||||
// 转换为地图标记点数据格式
|
||||
const markers = locations.map(location => ({
|
||||
id: location.id,
|
||||
name: location.location_name || '未知地点',
|
||||
longitude: location.longitude,
|
||||
latitude: location.latitude,
|
||||
loginTime: location.login_time,
|
||||
ipAddress: location.ip_address,
|
||||
user: userMap.get(location.user_id)
|
||||
}));
|
||||
|
||||
return c.json({
|
||||
message: "获取登录位置数据成功",
|
||||
data: markers,
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取登录位置数据失败:", error);
|
||||
return c.json({ error: "获取登录位置数据失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取登录位置详情数据
|
||||
mapRoutes.get("/location/:id", withAuth, async (c) => {
|
||||
try {
|
||||
const id = Number(c.req.param("id"));
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return c.json({ error: "无效的登录记录ID" }, 400);
|
||||
}
|
||||
|
||||
const apiClient = c.get('apiClient');
|
||||
|
||||
// 查询登录记录详情
|
||||
const location = await apiClient.database
|
||||
.table('login_history')
|
||||
.where('id', id)
|
||||
.first();
|
||||
|
||||
if (!location) {
|
||||
return c.json({ error: "登录记录不存在" }, 404);
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const [user] = await apiClient.database
|
||||
.table('users')
|
||||
.select('id', 'username', 'nickname')
|
||||
.where('id', location.user_id);
|
||||
|
||||
return c.json({
|
||||
message: "获取登录位置详情成功",
|
||||
data: {
|
||||
...location,
|
||||
user
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("获取登录位置详情失败:", error);
|
||||
return c.json({ error: "获取登录位置详情失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新登录位置信息
|
||||
mapRoutes.put("/location/:id", withAuth, async (c) => {
|
||||
try {
|
||||
const id = Number(c.req.param("id"));
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return c.json({ error: "无效的登录记录ID" }, 400);
|
||||
}
|
||||
|
||||
const apiClient = c.get('apiClient');
|
||||
const data = await c.req.json();
|
||||
|
||||
// 验证经纬度
|
||||
if (!data.longitude || !data.latitude) {
|
||||
return c.json({ error: "经度和纬度不能为空" }, 400);
|
||||
}
|
||||
|
||||
// 检查登录记录是否存在
|
||||
const location = await apiClient.database
|
||||
.table('login_history')
|
||||
.where('id', id)
|
||||
.first();
|
||||
|
||||
if (!location) {
|
||||
return c.json({ error: "登录记录不存在" }, 404);
|
||||
}
|
||||
|
||||
// 更新位置信息
|
||||
await apiClient.database
|
||||
.table('login_history')
|
||||
.where('id', id)
|
||||
.update({
|
||||
longitude: data.longitude,
|
||||
latitude: data.latitude,
|
||||
location_name: data.location_name
|
||||
});
|
||||
|
||||
// 获取更新后的登录记录
|
||||
const updatedLocation = await apiClient.database
|
||||
.table('login_history')
|
||||
.where('id', id)
|
||||
.first();
|
||||
|
||||
return c.json({
|
||||
message: "登录位置信息更新成功",
|
||||
data: updatedLocation,
|
||||
});
|
||||
} catch (error) {
|
||||
log.api("更新登录位置信息失败:", error);
|
||||
return c.json({ error: "更新登录位置信息失败" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return mapRoutes;
|
||||
}
|
||||
1063
server/routes_sys.ts
Normal file
1063
server/routes_sys.ts
Normal file
File diff suppressed because it is too large
Load Diff
241
server/routes_users.ts
Normal file
241
server/routes_users.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Hono } from 'hono'
|
||||
import type { Variables } from './app.tsx'
|
||||
import type { WithAuth } from './app.tsx'
|
||||
|
||||
export function createUserRoutes(withAuth: WithAuth) {
|
||||
const usersRoutes = new Hono<{ Variables: Variables }>()
|
||||
|
||||
// 获取用户列表
|
||||
usersRoutes.get('/', withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient')
|
||||
|
||||
const page = Number(c.req.query('page')) || 1
|
||||
const pageSize = Number(c.req.query('pageSize')) || 10
|
||||
const offset = (page - 1) * pageSize
|
||||
|
||||
const search = c.req.query('search') || ''
|
||||
|
||||
let query = apiClient.database.table('users')
|
||||
.orderBy('id', 'desc')
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
builder.where('username', 'like', `%${search}%`)
|
||||
.orWhere('nickname', 'like', `%${search}%`)
|
||||
.orWhere('email', 'like', `%${search}%`)
|
||||
})
|
||||
}
|
||||
|
||||
const total = await query.clone().count()
|
||||
const users = await query.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
|
||||
.limit(pageSize).offset(offset)
|
||||
|
||||
return c.json({
|
||||
data: users,
|
||||
pagination: {
|
||||
total: Number(total),
|
||||
current: page,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
return c.json({ error: '获取用户列表失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取单个用户详情
|
||||
usersRoutes.get('/:id', withAuth, async (c) => {
|
||||
try {
|
||||
const id = Number(c.req.param('id'))
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
return c.json({ error: '无效的用户ID' }, 400)
|
||||
}
|
||||
|
||||
const apiClient = c.get('apiClient')
|
||||
const user = await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
|
||||
.first()
|
||||
|
||||
if (!user) {
|
||||
return c.json({ error: '用户不存在' }, 404)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: user,
|
||||
message: '获取用户详情成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户详情失败:', error)
|
||||
return c.json({ error: '获取用户详情失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建用户
|
||||
usersRoutes.post('/', withAuth, async (c) => {
|
||||
try {
|
||||
const apiClient = c.get('apiClient')
|
||||
const body = await c.req.json()
|
||||
|
||||
// 验证必填字段
|
||||
const { username, nickname, email, password, role } = body
|
||||
if (!username || !nickname || !email || !password || !role) {
|
||||
return c.json({ error: '缺少必要的用户信息' }, 400)
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await apiClient.database.table('users')
|
||||
.where('username', username)
|
||||
.first()
|
||||
|
||||
if (existingUser) {
|
||||
return c.json({ error: '用户名已存在' }, 400)
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
const [id] = await apiClient.database.table('users').insert({
|
||||
username,
|
||||
nickname,
|
||||
email,
|
||||
password: password, // 加密密码
|
||||
role,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
})
|
||||
|
||||
const newUser = await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.select('id', 'username', 'nickname', 'email', 'role', 'created_at')
|
||||
.first()
|
||||
|
||||
return c.json({
|
||||
data: newUser,
|
||||
message: '创建用户成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
return c.json({ error: '创建用户失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户
|
||||
usersRoutes.put('/:id', withAuth, async (c) => {
|
||||
try {
|
||||
const id = Number(c.req.param('id'))
|
||||
if (!id || isNaN(id)) {
|
||||
return c.json({ error: '无效的用户ID' }, 400)
|
||||
}
|
||||
|
||||
const apiClient = c.get('apiClient')
|
||||
const body = await c.req.json()
|
||||
|
||||
// 验证必填字段
|
||||
const { username, nickname, email, role } = body
|
||||
if (!username || !nickname || !email || !role) {
|
||||
return c.json({ error: '缺少必要的用户信息' }, 400)
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
|
||||
if (!existingUser) {
|
||||
return c.json({ error: '用户不存在' }, 404)
|
||||
}
|
||||
|
||||
// 如果修改了用户名,检查新用户名是否已被使用
|
||||
if (username !== existingUser.username) {
|
||||
const userWithSameName = await apiClient.database.table('users')
|
||||
.where('username', username)
|
||||
.whereNot('id', id.toString())
|
||||
.first()
|
||||
|
||||
if (userWithSameName) {
|
||||
return c.json({ error: '用户名已存在' }, 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateData: any = {
|
||||
username,
|
||||
nickname,
|
||||
email,
|
||||
role,
|
||||
updated_at: new Date()
|
||||
}
|
||||
|
||||
// 如果提供了新密码,则更新密码
|
||||
if (body.password) {
|
||||
updateData.password = body.password
|
||||
}
|
||||
|
||||
await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.update(updateData)
|
||||
|
||||
const updatedUser = await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.select('id', 'username', 'nickname', 'email', 'role', 'created_at')
|
||||
.first()
|
||||
|
||||
return c.json({
|
||||
data: updatedUser,
|
||||
message: '更新用户成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
return c.json({ error: '更新用户失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户
|
||||
usersRoutes.delete('/:id', withAuth, async (c) => {
|
||||
try {
|
||||
const id = Number(c.req.param('id'))
|
||||
if (!id || isNaN(id)) {
|
||||
return c.json({ error: '无效的用户ID' }, 400)
|
||||
}
|
||||
|
||||
const apiClient = c.get('apiClient')
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.first()
|
||||
|
||||
if (!existingUser) {
|
||||
return c.json({ error: '用户不存在' }, 404)
|
||||
}
|
||||
|
||||
// 检查是否为最后一个管理员
|
||||
if (existingUser.role === 'admin') {
|
||||
const adminCount = await apiClient.database.table('users')
|
||||
.where('role', 'admin')
|
||||
.count()
|
||||
|
||||
if (Number(adminCount) <= 1) {
|
||||
return c.json({ error: '不能删除最后一个管理员' }, 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
await apiClient.database.table('users')
|
||||
.where('id', id)
|
||||
.delete()
|
||||
|
||||
return c.json({
|
||||
message: '删除用户成功',
|
||||
id
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
return c.json({ error: '删除用户失败' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
return usersRoutes
|
||||
}
|
||||
Reference in New Issue
Block a user