فهرست منبع

Merge remote-tracking branch 'upstream/main'

yourname 7 ماه پیش
والد
کامیت
e390578836

+ 27 - 0
HISTORY.md

@@ -0,0 +1,27 @@
+
+待实现
+迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开
+
+2025.05.14 0.1.5
+优化ErrorPage样式,补充了NotFoundPage
+deno.json中去掉 没用的 @testing-library,jsDom
+
+2025.05.14 0.1.4
+开发环境中,前端页面默认打开vConsole
+
+2025.05.14 0.1.3
+web_app.tsx 模块化拆分路由、菜单、布局
+
+2025.05.14 0.1.2
+将 server/migrations.ts 中的静态加载逻辑改为异步函数 loadMigrations()
+
+2025.05.14 0.1.1
+创建 server/middlewares.ts - 集中管理所有中间件配置
+创建 server/router.ts - 统一处理路由注册逻辑
+重构 server/app.tsx - 仅保留应用初始化和服务启动逻辑
+保持原有功能不变,同时提高代码可维护性和扩展性
+
+2025.05.13 0.1.0
+将admin api.ts 拆开
+打开迁移管理页面时,将迁移历史读取出来
+首页添加了迁移管理入口按钮, 无需登录即可访问

+ 3 - 4
client/admin/api/maps.ts

@@ -1,5 +1,4 @@
 import axios from 'axios';
-import { API_BASE_URL } from './index.ts';
 import type {
  LoginLocation, LoginLocationDetail,
 } from '../../share/types.ts';
@@ -30,7 +29,7 @@ export const MapAPI = {
     userId?: number 
   }): Promise<LoginLocationResponse> => {
     try {
-      const response = await axios.get(`${API_BASE_URL}/map/markers`, { params });
+      const response = await axios.get(`/map/markers`, { params });
       return response.data;
     } catch (error) {
       throw error;
@@ -40,7 +39,7 @@ export const MapAPI = {
   // 获取登录位置详情
   getLocationDetail: async (locationId: number): Promise<LoginLocationDetailResponse> => {
     try {
-      const response = await axios.get(`${API_BASE_URL}/map/location/${locationId}`);
+      const response = await axios.get(`/map/location/${locationId}`);
       return response.data;
     } catch (error) {
       throw error;
@@ -54,7 +53,7 @@ export const MapAPI = {
     location_name?: string; 
   }): Promise<LoginLocationUpdateResponse> => {
     try {
-      const response = await axios.put(`${API_BASE_URL}/map/location/${locationId}`, data);
+      const response = await axios.put(`/map/location/${locationId}`, data);
       return response.data;
     } catch (error) {
       throw error;

+ 46 - 0
client/admin/components/ErrorPage.tsx

@@ -0,0 +1,46 @@
+import React from 'react';
+import { useRouteError, useNavigate } from 'react-router';
+import { Alert, Button } from 'antd';
+import { useTheme } from '../hooks_sys.tsx';
+
+export const ErrorPage = () => {
+  const navigate = useNavigate();
+  const { isDark } = useTheme();
+  const error = useRouteError() as any;
+  const errorMessage = error?.statusText || error?.message || '未知错误';
+  
+  return (
+    <div className="flex flex-col items-center justify-center flex-grow p-4"
+      style={{ color: isDark ? '#fff' : 'inherit' }}
+    >
+      <div className="max-w-3xl w-full">
+        <h1 className="text-2xl font-bold mb-4">发生错误</h1>
+        <Alert 
+          type="error"
+          message={error?.message || '未知错误'}
+          description={
+            error?.stack ? (
+              <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
+                {error.stack}
+              </pre>
+            ) : null
+          }
+          className="mb-4"
+        />
+        <div className="flex gap-4">
+          <Button 
+            type="primary" 
+            onClick={() => navigate(0)}
+          >
+            重新加载
+          </Button>
+          <Button 
+            onClick={() => navigate('/admin')}
+          >
+            返回首页
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 30 - 0
client/admin/components/NotFoundPage.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+import { Button } from 'antd';
+import { useTheme } from '../hooks_sys.tsx';
+
+export const NotFoundPage = () => {
+  const navigate = useNavigate();
+  const { isDark } = useTheme();
+  
+  return (
+    <div className="flex flex-col items-center justify-center flex-grow p-4"
+      style={{ color: isDark ? '#fff' : 'inherit' }}
+    >
+      <div className="max-w-3xl w-full">
+        <h1 className="text-2xl font-bold mb-4">404 - 页面未找到</h1>
+        <p className="mb-6 text-gray-600 dark:text-gray-300">
+          您访问的页面不存在或已被移除
+        </p>
+        <div className="flex gap-4">
+          <Button 
+            type="primary" 
+            onClick={() => navigate('/admin')}
+          >
+            返回首页
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 222 - 0
client/admin/layouts/MainLayout.tsx

@@ -0,0 +1,222 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+  Outlet,
+  useLocation,
+} from 'react-router';
+import {
+  Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
+} from 'antd';
+import {
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+  BellOutlined,
+  VerticalAlignTopOutlined,
+  UserOutlined
+} from '@ant-design/icons';
+import { useAuth, useTheme } from '../hooks_sys.tsx';
+import { useMenu, useMenuSearch, type MenuItem } from '../menu.tsx';
+
+const { Header, Sider, Content } = Layout;
+
+/**
+ * 主布局组件
+ * 包含侧边栏、顶部导航和内容区域
+ */
+export const MainLayout = () => {
+  const { user } = useAuth();
+  const { isDark } = useTheme();
+  const [showBackTop, setShowBackTop] = useState(false);
+  const location = useLocation();
+  
+  // 使用菜单hook
+  const {
+    menuItems,
+    userMenuItems,
+    openKeys,
+    collapsed,
+    setCollapsed,
+    handleMenuClick: handleRawMenuClick,
+    onOpenChange
+  } = useMenu();
+  
+  // 处理菜单点击
+  const handleMenuClick = (key: string) => {
+    const item = findMenuItem(menuItems, key);
+    if (item && 'label' in item) {
+      handleRawMenuClick(item);
+    }
+  };
+  
+  // 查找菜单项
+  const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
+    for (const item of items) {
+      if (!item) continue;
+      if (item.key === key) return item;
+      if (item.children) {
+        const found = findMenuItem(item.children, key);
+        if (found) return found;
+      }
+    }
+    return null;
+  };
+  
+  // 使用菜单搜索hook
+  const {
+    searchText,
+    setSearchText,
+    filteredMenuItems
+  } = useMenuSearch(menuItems);
+  
+  // 获取当前选中的菜单项
+  const selectedKey = useMemo(() => {
+    const findSelectedKey = (items: MenuItem[]): string | null => {
+      for (const item of items) {
+        if (!item) continue;
+        if (item.path === location.pathname) return item.key || null;
+        if (item.children) {
+          const childKey = findSelectedKey(item.children);
+          if (childKey) return childKey;
+        }
+      }
+      return null;
+    };
+    
+    return findSelectedKey(menuItems) || '';
+  }, [location.pathname, menuItems]);
+  
+  // 检测滚动位置,控制回到顶部按钮显示
+  useEffect(() => {
+    const handleScroll = () => {
+      setShowBackTop(window.pageYOffset > 300);
+    };
+    
+    window.addEventListener('scroll', handleScroll);
+    return () => window.removeEventListener('scroll', handleScroll);
+  }, []);
+  
+  // 回到顶部
+  const scrollToTop = () => {
+    window.scrollTo({
+      top: 0,
+      behavior: 'smooth'
+    });
+  };
+
+  
+  // 应用名称 - 从CONFIG中获取或使用默认值
+  const appName = window.CONFIG?.APP_NAME || '应用Starter';
+  
+  return (
+    <Layout style={{ minHeight: '100vh' }}>
+      <Sider 
+        trigger={null} 
+        collapsible 
+        collapsed={collapsed}
+        width={240}
+        className="custom-sider"
+        style={{
+          overflow: 'auto',
+          height: '100vh',
+          position: 'fixed',
+          left: 0,
+          top: 0,
+          bottom: 0,
+          zIndex: 100,
+        }}
+      >
+        <div className="p-4">
+          <Typography.Title level={2} className="text-xl font-bold truncate">
+            {collapsed ? '应用' : appName}
+          </Typography.Title>
+          
+          {/* 菜单搜索框 */}
+          {!collapsed && (
+            <div className="mb-4">
+              <Input.Search
+                placeholder="搜索菜单..."
+                allowClear
+                value={searchText}
+                onChange={(e) => setSearchText(e.target.value)}
+              />
+            </div>
+          )}
+        </div>
+        
+        {/* 菜单列表 */}
+        <Menu
+          theme={isDark ? 'dark' : 'light'}
+          mode="inline"
+          items={filteredMenuItems}
+          openKeys={openKeys}
+          selectedKeys={[selectedKey]}
+          onOpenChange={onOpenChange}
+          onClick={({ key }) => handleMenuClick(key)}
+          inlineCollapsed={collapsed}
+        />
+      </Sider>
+      
+      <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
+        <Header className="p-0 flex justify-between items-center" 
+          style={{ 
+            position: 'sticky', 
+            top: 0, 
+            zIndex: 99, 
+            boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
+          }}
+        >
+          <Button
+            type="text"
+            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
+            onClick={() => setCollapsed(!collapsed)}
+            className="w-16 h-16"
+          />
+          
+          <Space size="middle" className="mr-4">
+            <Badge count={5} offset={[0, 5]}>
+              <Button 
+                type="text" 
+                icon={<BellOutlined />}
+              />
+            </Badge>
+            
+            <Dropdown menu={{ items: userMenuItems }}>
+              <Space className="cursor-pointer">
+                <Avatar 
+                  src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
+                  icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
+                />
+                <span>
+                  {user?.nickname || user?.username}
+                </span>
+              </Space>
+            </Dropdown>
+          </Space>
+        </Header>
+        
+        <Content className="m-6" style={{ overflow: 'initial' }}>
+          <div className="site-layout-content p-6 rounded-lg">
+            <Outlet />
+          </div>
+          
+          {/* 回到顶部按钮 */}
+          {showBackTop && (
+            <Button
+              type="primary"
+              shape="circle"
+              icon={<VerticalAlignTopOutlined />}
+              size="large"
+              onClick={scrollToTop}
+              style={{
+                position: 'fixed',
+                right: 30,
+                bottom: 30,
+                zIndex: 1000,
+                boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
+              }}
+            />
+          )}
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};

+ 199 - 0
client/admin/menu.tsx

@@ -0,0 +1,199 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+import type { MenuProps } from 'antd';
+import {
+  UserOutlined,
+  DashboardOutlined,
+  TeamOutlined,
+  SettingOutlined,
+  FileOutlined,
+  MessageOutlined,
+  InfoCircleOutlined,
+  BarChartOutlined,
+  EnvironmentOutlined,
+  MoonOutlined,
+  SunOutlined
+} from '@ant-design/icons';
+import { useTheme } from './hooks_sys.tsx';
+
+export interface MenuItem {
+  key: string;
+  label: string;
+  icon?: React.ReactNode;
+  children?: MenuItem[];
+  path?: string;
+  permission?: string;
+}
+
+/**
+ * 菜单搜索 Hook
+ * 封装菜单搜索相关逻辑
+ */
+export const useMenuSearch = (menuItems: MenuItem[]) => {
+  const [searchText, setSearchText] = React.useState('');
+
+  // 过滤菜单项
+  const filteredMenuItems = React.useMemo(() => {
+    if (!searchText) return menuItems;
+    
+    const filterItems = (items: MenuItem[]): MenuItem[] => {
+      return items
+        .map(item => {
+          // 克隆对象避免修改原数据
+          const newItem = { ...item };
+          if (newItem.children) {
+            newItem.children = filterItems(newItem.children);
+          }
+          return newItem;
+        })
+        .filter(item => {
+          // 保留匹配项或其子项匹配的项
+          const match = item.label.toLowerCase().includes(searchText.toLowerCase());
+          if (match) return true;
+          if (item.children?.length) return true;
+          return false;
+        });
+    };
+    
+    return filterItems(menuItems);
+  }, [menuItems, searchText]);
+
+  // 清除搜索
+  const clearSearch = () => {
+    setSearchText('');
+  };
+
+  return {
+    searchText,
+    setSearchText,
+    filteredMenuItems,
+    clearSearch
+  };
+};
+
+export const useMenu = () => {
+  const { isDark, toggleTheme } = useTheme();
+  const navigate = useNavigate();
+  const [collapsed, setCollapsed] = React.useState(false);
+  const [openKeys, setOpenKeys] = React.useState<string[]>([]);
+
+  // 基础菜单项配置
+  const menuItems: MenuItem[] = [
+    {
+      key: 'dashboard',
+      label: '控制台',
+      icon: <DashboardOutlined />,
+      path: '/admin/dashboard'
+    },
+    {
+      key: 'users',
+      label: '用户管理',
+      icon: <TeamOutlined />,
+      path: '/admin/users',
+      permission: 'user:manage'
+    },
+    {
+      key: 'settings',
+      label: '系统设置',
+      icon: <SettingOutlined />,
+      children: [
+        {
+          key: 'theme-settings',
+          label: '主题设置',
+          path: '/admin/theme-settings',
+          permission: 'system:settings'
+        },
+        {
+          key: 'system-settings',
+          label: '系统配置',
+          path: '/admin/settings',
+          permission: 'system:settings'
+        }
+      ]
+    },
+    {
+      key: 'content',
+      label: '内容管理',
+      icon: <FileOutlined />,
+      children: [
+        {
+          key: 'know-info',
+          label: '知识库',
+          path: '/admin/know-info',
+          permission: 'content:manage'
+        },
+        {
+          key: 'file-library',
+          label: '文件库',
+          path: '/admin/file-library',
+          permission: 'content:manage'
+        }
+      ]
+    },
+    {
+      key: 'messages',
+      label: '消息中心',
+      icon: <MessageOutlined />,
+      path: '/admin/messages',
+      permission: 'message:view'
+    },
+    {
+      key: 'charts',
+      label: '数据图表',
+      icon: <BarChartOutlined />,
+      path: '/admin/chart-dashboard',
+      permission: 'chart:view'
+    },
+    {
+      key: 'maps',
+      label: '地图',
+      icon: <EnvironmentOutlined />,
+      path: '/admin/map-dashboard',
+      permission: 'map:view'
+    }
+  ];
+
+  // 用户菜单项
+  const userMenuItems: MenuProps['items'] = [
+    {
+      key: 'profile',
+      label: '个人资料',
+      icon: <UserOutlined />
+    },
+    {
+      key: 'theme',
+      label: isDark ? '切换到亮色模式' : '切换到暗色模式',
+      icon: isDark ? <SunOutlined /> : <MoonOutlined />,
+      onClick: () => toggleTheme()
+    },
+    {
+      key: 'logout',
+      label: '退出登录',
+      icon: <InfoCircleOutlined />,
+      danger: true
+    }
+  ];
+
+  // 处理菜单点击
+  const handleMenuClick = (item: MenuItem) => {
+    if (item.path) {
+      navigate(item.path);
+    }
+  };
+
+  // 处理菜单展开变化
+  const onOpenChange = (keys: string[]) => {
+    const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
+    setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
+  };
+
+  return {
+    menuItems,
+    userMenuItems,
+    openKeys,
+    collapsed,
+    setCollapsed,
+    handleMenuClick,
+    onOpenChange
+  };
+};

+ 91 - 0
client/admin/routes.tsx

@@ -0,0 +1,91 @@
+import React from 'react';
+import { createBrowserRouter, Navigate } from 'react-router';
+import { ProtectedRoute } from './components_protected_route.tsx';
+import { MainLayout } from './layouts/MainLayout.tsx';
+import { ErrorPage } from './components/ErrorPage.tsx';
+import { NotFoundPage } from './components/NotFoundPage.tsx';
+import { DashboardPage } from './pages_dashboard.tsx';
+import { UsersPage } from './pages_users.tsx';
+import { FileLibraryPage } from './pages_file_library.tsx';
+import { KnowInfoPage } from './pages_know_info.tsx';
+import { MessagesPage } from './pages_messages.tsx';
+import { SettingsPage } from './pages_settings.tsx';
+import { ThemeSettingsPage } from './pages_theme_settings.tsx';
+import { ChartDashboardPage } from './pages_chart.tsx';
+import { LoginMapPage } from './pages_map.tsx';
+import { LoginPage } from './pages_login_reg.tsx';
+
+export const router = createBrowserRouter([
+  {
+    path: '/',
+    element: <Navigate to="/admin" replace />
+  },
+  {
+    path: '/admin/login',
+    element: <LoginPage />
+  },
+  {
+    path: '/admin',
+    element: (
+      <ProtectedRoute>
+        <MainLayout />
+      </ProtectedRoute>
+    ),
+    children: [
+      {
+        index: true,
+        element: <Navigate to="/admin/dashboard" />
+      },
+      {
+        path: 'dashboard',
+        element: <DashboardPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'users',
+        element: <UsersPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'settings',
+        element: <SettingsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'theme-settings',
+        element: <ThemeSettingsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'chart-dashboard',
+        element: <ChartDashboardPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'map-dashboard',
+        element: <LoginMapPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'know-info',
+        element: <KnowInfoPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'file-library',
+        element: <FileLibraryPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'messages',
+        element: <MessagesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: '*',
+        element: <NotFoundPage />,
+        errorElement: <ErrorPage />
+      },
+    ],
+  },
+]);

+ 7 - 523
client/admin/web_app.tsx

@@ -1,76 +1,16 @@
-import React, { useState, useEffect} from 'react';
+import React from 'react';
 import { createRoot } from 'react-dom/client';
-import { 
-  createBrowserRouter,
-  RouterProvider,
-  Outlet,
-  useNavigate,
-  useLocation,
-  Navigate,
-  useParams,
-  useRouteError
-} from 'react-router';
-import { 
-  Layout, Menu, Button, Table, Space,
-  Form, Input, Select, message, Modal,
-  Card, Spin, Row, Col, Breadcrumb, Avatar,
-  Dropdown, ConfigProvider, theme, Typography,
-  Switch, Badge, Image, Upload, Divider, Descriptions,
-  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
-} from 'antd';
-import {
-  MenuFoldOutlined,
-  MenuUnfoldOutlined,
-  UserOutlined,
-  DashboardOutlined,
-  TeamOutlined,
-  SettingOutlined,
-  LogoutOutlined,
-  BellOutlined,
-  BookOutlined,
-  FileOutlined,
-  PieChartOutlined,
-  VerticalAlignTopOutlined,
-  CloseOutlined,
-  SearchOutlined
-} from '@ant-design/icons';   
-import { 
-  QueryClient,
-  QueryClientProvider,
-} from '@tanstack/react-query';
+import { RouterProvider } from 'react-router';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import dayjs from 'dayjs';
 import weekday from 'dayjs/plugin/weekday';
 import localeData from 'dayjs/plugin/localeData';
 import 'dayjs/locale/zh-cn';
-import type { 
-  GlobalConfig
-} from '../share/types.ts';
 
-
-import {
-  AuthProvider,
-  useAuth,
-  ThemeProvider,
-  useTheme,
-} from './hooks_sys.tsx';
-
-import {
-  DashboardPage
-} from './pages_dashboard.tsx';
-import {
-  UsersPage
-} from './pages_users.tsx';
-import {
-  FileLibraryPage
-} from './pages_file_library.tsx';
-import { KnowInfoPage } from './pages_know_info.tsx';
-import { MessagesPage } from './pages_messages.tsx';
-import {SettingsPage } from './pages_settings.tsx';
-import {ThemeSettingsPage} from './pages_theme_settings.tsx'
-import { ChartDashboardPage } from './pages_chart.tsx';
-import { LoginMapPage } from './pages_map.tsx';
-import { LoginPage } from './pages_login_reg.tsx';
-import { ProtectedRoute } from './components_protected_route.tsx';
+import { AuthProvider } from './hooks_sys.tsx';
+import { ThemeProvider } from './hooks_sys.tsx';
+import { router } from './routes.tsx';
+import type { GlobalConfig } from '../share/types.ts';
 
 // 配置 dayjs 插件
 dayjs.extend(weekday);
@@ -79,12 +19,9 @@ dayjs.extend(localeData);
 // 设置 dayjs 语言
 dayjs.locale('zh-cn');
 
-const { Header, Sider, Content } = Layout;
-
 // 创建QueryClient实例
 const queryClient = new QueryClient();
 
-
 // 声明全局配置对象类型
 declare global {
   interface Window {
@@ -92,460 +29,8 @@ declare global {
   }
 }
 
-
-// 主布局组件
-const MainLayout = () => {
-  const [collapsed, setCollapsed] = useState(false);
-  const { user, logout } = useAuth();
-  const { isDark, toggleTheme } = useTheme();
-  const navigate = useNavigate();
-  const location = useLocation();
-  const [openKeys, setOpenKeys] = useState<string[]>([]);
-  const [showBackTop, setShowBackTop] = useState(false);
-  const [searchText, setSearchText] = useState('');
-  const [filteredMenuItems, setFilteredMenuItems] = useState<any[]>([]);
-  
-  // 检测滚动位置,控制回到顶部按钮显示
-  useEffect(() => {
-    const handleScroll = () => {
-      setShowBackTop(window.pageYOffset > 300);
-    };
-    
-    window.addEventListener('scroll', handleScroll);
-    return () => window.removeEventListener('scroll', handleScroll);
-  }, []);
-  
-  // 回到顶部
-  const scrollToTop = () => {
-    window.scrollTo({
-      top: 0,
-      behavior: 'smooth'
-    });
-  };
-  
-  // 菜单项配置
-  const menuItems = [
-    {
-      key: '/dashboard',
-      icon: <DashboardOutlined />,
-      label: '仪表盘',
-    },
-    {
-      key: '/analysis',
-      icon: <PieChartOutlined />,
-      label: '数据分析',
-      children: [
-        {
-          key: '/chart-dashboard',
-          label: '图表统计',
-        },
-        {
-          key: '/map-dashboard',
-          label: '地图概览',
-        },
-      ],
-    },
-    {
-      key: '/files',
-      icon: <FileOutlined />,
-      label: '文件管理',
-      children: [
-        {
-          key: '/file-library',
-          label: '文件库',
-        },
-      ],
-    },
-    {
-      key: '/know-info',
-      icon: <BookOutlined />,
-      label: '知识库',
-    },
-    {
-      key: '/users',
-      icon: <TeamOutlined />,
-      label: '用户管理',
-    },
-    {
-      key: '/messages',
-      icon: <BellOutlined />,
-      label: '消息管理',
-    },
-    {
-      key: '/settings',
-      icon: <SettingOutlined />,
-      label: '系统设置',
-      children: [
-        {
-          key: '/theme-settings',
-          label: '主题设置',
-        },
-        {
-          key: '/settings',
-          label: '基本设置',
-        },
-      ],
-    },
-  ];
-  
-  // 初始化filteredMenuItems
-  useEffect(() => {
-    setFilteredMenuItems(menuItems);
-  }, []);
-
-  // 搜索菜单项
-  const handleSearch = (value: string) => {
-    setSearchText(value);
-    
-    if (!value.trim()) {
-      setFilteredMenuItems(menuItems);
-      return;
-    }
-    
-    // 搜索功能 - 过滤菜单项
-    const filtered = menuItems.reduce((acc: any[], item) => {
-      // 检查主菜单项是否匹配
-      const mainItemMatch = item.label.toString().toLowerCase().includes(value.toLowerCase());
-      
-      // 如果有子菜单,检查子菜单中是否有匹配项
-      if (item.children) {
-        const matchedChildren = item.children.filter(child => 
-          child.label.toString().toLowerCase().includes(value.toLowerCase())
-        );
-        
-        if (matchedChildren.length > 0) {
-          // 如果有匹配的子菜单,创建包含匹配子菜单的副本
-          acc.push({
-            ...item,
-            children: matchedChildren
-          });
-          return acc;
-        }
-      }
-      
-      // 如果主菜单项匹配,添加整个项
-      if (mainItemMatch) {
-        acc.push(item);
-      }
-      
-      return acc;
-    }, []);
-    
-    setFilteredMenuItems(filtered);
-  };
-  
-  // 清除搜索
-  const clearSearch = () => {
-    setSearchText('');
-    setFilteredMenuItems(menuItems);
-  };
-
-  const handleMenuClick = ({ key }: { key: string }) => {
-    navigate(`/admin${key}`);
-    // 如果有搜索文本,清除搜索
-    if (searchText) {
-      clearSearch();
-    }
-  };
-  
-  // 处理登出
-  const handleLogout = async () => {
-    await logout();
-    navigate('/admin/login');
-  };
-  
-  // 处理菜单展开/收起
-  const onOpenChange = (keys: string[]) => {
-    // 当侧边栏折叠时不保存openKeys状态
-    if (!collapsed) {
-      setOpenKeys(keys);
-    }
-  };
-  
-  // 当侧边栏折叠状态改变时,控制菜单打开状态
-  useEffect(() => {
-    if (collapsed) {
-      setOpenKeys([]);
-    } else {
-      // 找到当前路径所属的父菜单
-      const currentPath = location.pathname.replace('/admin', '');
-      const parentKeys = menuItems
-        .filter(item => item.children && item.children.some(child => child.key === currentPath))
-        .map(item => item.key);
-      
-      // 仅展开当前所在的菜单组
-      if (parentKeys.length > 0) {
-        setOpenKeys(parentKeys);
-      } else {
-        // 初始时可以根据需要设置要打开的菜单组
-        setOpenKeys([]);
-      }
-    }
-  }, [collapsed, location.pathname]);
-  
-  // 用户下拉菜单项
-  const userMenuItems = [
-    {
-      key: 'profile',
-      label: '个人信息',
-      icon: <UserOutlined />
-    },
-    {
-      key: 'theme',
-      label: isDark ? '切换到亮色模式' : '切换到暗色模式',
-      icon: <SettingOutlined />,
-      onClick: toggleTheme
-    },
-    {
-      key: 'logout',
-      label: '退出登录',
-      icon: <LogoutOutlined />,
-      onClick: handleLogout
-    }
-  ];
-  
-  // 应用名称 - 从CONFIG中获取或使用默认值
-  const appName = window.CONFIG?.APP_NAME || '应用Starter';
-  
-  return (
-    <Layout style={{ minHeight: '100vh' }}>
-      <Sider 
-        trigger={null} 
-        collapsible 
-        collapsed={collapsed}
-        width={240}
-        className="custom-sider"
-        style={{
-          overflow: 'auto',
-          height: '100vh',
-          position: 'fixed',
-          left: 0,
-          top: 0,
-          bottom: 0,
-          zIndex: 100,
-        }}
-      >
-        <div className="p-4">
-          <Typography.Title level={2} className="text-xl font-bold truncate">
-            {collapsed ? '应用' : appName}
-          </Typography.Title>
-        </div>
-        
-        {/* 搜索框 - 仅在展开状态下显示 */}
-        {!collapsed && (
-          <div style={{ padding: '0 16px 16px' }}>
-            <Input 
-              placeholder="搜索菜单..." 
-              value={searchText}
-              onChange={(e) => handleSearch(e.target.value)}
-              suffix={
-                searchText ? 
-                <Button 
-                  type="text" 
-                  size="small" 
-                  icon={<CloseOutlined />} 
-                  onClick={clearSearch}
-                /> : 
-                <SearchOutlined />
-              }
-            />
-          </div>
-        )}
-        
-        <Menu
-          theme={isDark ? "light" : "light"}
-          mode="inline"
-          selectedKeys={[location.pathname.replace('/admin', '')]}
-          openKeys={openKeys}
-          onOpenChange={onOpenChange}
-          items={filteredMenuItems}
-          onClick={handleMenuClick}
-        />
-      </Sider>
-      
-      <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
-        <Header className="p-0 flex justify-between items-center" 
-          style={{ 
-            position: 'sticky', 
-            top: 0, 
-            zIndex: 99, 
-            boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
-          }}
-        >
-          <Button
-            type="text"
-            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
-            onClick={() => setCollapsed(!collapsed)}
-            className="w-16 h-16"
-          />
-          
-          <Space size="middle" className="mr-4">
-            <Badge count={5} offset={[0, 5]}>
-              <Button 
-                type="text" 
-                icon={<BellOutlined />}
-              />
-            </Badge>
-            
-            <Dropdown menu={{ items: userMenuItems }}>
-              <Space className="cursor-pointer">
-                <Avatar 
-                  src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
-                  icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
-                />
-                <span>
-                  {user?.nickname || user?.username}
-                </span>
-              </Space>
-            </Dropdown>
-          </Space>
-        </Header>
-        
-        <Content className="m-6" style={{ overflow: 'initial' }}>
-          <div className="site-layout-content p-6 rounded-lg">
-            <Outlet />
-          </div>
-          
-          {/* 回到顶部按钮 */}
-          {showBackTop && (
-            <Button
-              type="primary"
-              shape="circle"
-              icon={<VerticalAlignTopOutlined />}
-              size="large"
-              onClick={scrollToTop}
-              style={{
-                position: 'fixed',
-                right: 30,
-                bottom: 30,
-                zIndex: 1000,
-                boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
-              }}
-            />
-          )}
-        </Content>
-      </Layout>
-    </Layout>
-  );
-};
-
-
-
-// 错误页面组件
-const ErrorPage = () => {
-  const { isDark } = useTheme();
-  const error = useRouteError() as any;
-  const errorMessage = error?.statusText || error?.message || '未知错误';
-  
-  
-  return (
-    <div className="flex flex-col items-center justify-center min-h-screen p-4"
-      style={{ color: isDark ? '#fff' : 'inherit' }}
-    >
-      <div className="max-w-3xl w-full">
-        <h1 className="text-2xl font-bold mb-4">发生错误</h1>
-        <Alert 
-          type="error"
-          message={error?.message || '未知错误'}
-          description={
-            error?.stack ? (
-              <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
-                {error.stack}
-              </pre>
-            ) : null
-          }
-          className="mb-4"
-        />
-        <div className="flex gap-4">
-          <Button 
-            type="primary" 
-            onClick={() => window.location.reload()}
-          >
-            重新加载
-          </Button>
-          <Button 
-            onClick={() => window.location.href = '/admin'}
-          >
-            返回首页
-          </Button>
-        </div>
-      </div>
-    </div>
-  );
-};
-
 // 应用入口组件
 const App = () => {
-  // 路由配置
-  const router = createBrowserRouter([
-    {
-      path: '/',
-      element: <Navigate to="/admin" replace />
-    },
-    {
-      path: '/admin/login',
-      element: <LoginPage />
-    },
-    {
-      path: '/admin',
-      element: (
-        <ProtectedRoute>
-          <MainLayout />
-        </ProtectedRoute>
-      ),
-      children: [
-        {
-          index: true,
-          element: <Navigate to="/admin/dashboard" />
-        },
-        {
-          path: 'dashboard',
-          element: <DashboardPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'users',
-          element: <UsersPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'settings',
-          element: <SettingsPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'theme-settings',
-          element: <ThemeSettingsPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'chart-dashboard',
-          element: <ChartDashboardPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'map-dashboard',
-          element: <LoginMapPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'know-info',
-          element: <KnowInfoPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'file-library',
-          element: <FileLibraryPage />,
-          errorElement: <ErrorPage />
-        },
-        {
-          path: 'messages',
-          element: <MessagesPage />,
-          errorElement: <ErrorPage />
-        },
-      ],
-    },
-  ]);
   return <RouterProvider router={router} />
 };
 
@@ -560,4 +45,3 @@ root.render(
     </ThemeProvider>
   </QueryClientProvider>
 );
-

+ 1 - 0
client/share/types.ts

@@ -33,6 +33,7 @@ export interface User {
   role: string;
   avatar?: string;
   password?: string;
+  permissions?: string[];
 }
 
 export interface MenuItem {

+ 21 - 257
server/app.tsx

@@ -1,45 +1,15 @@
 /** @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 {
-  createClassRoomRoutes
-} from "./routes_classroom.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";
+import type { GlobalConfig } from '../client/share/types.ts';
+import { OssType, MapMode } from '../client/share/types.ts';
+import { createRouter } from './router.ts'
 dayjs.extend(utc)
 // 初始化debug实例
 const log = {
@@ -72,35 +42,6 @@ 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
@@ -111,122 +52,6 @@ interface EsmScriptConfig {
   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
@@ -236,85 +61,10 @@ interface ModuleParams {
 
 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-infos', 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.route('/classroom', createClassRoomRoutes(withAuth)) // 添加课堂路由
   
-  // 注册API路由
-  honoApp.route('/api', api)
+  // 创建路由
+  const router = createRouter(apiClient, moduleDir)
+  honoApp.route('/', router)
  
   // 首页路由 - SSR
   honoApp.get('/', async (c: HonoContext) => {
@@ -410,7 +160,7 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
 
             <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
             
-            {!isProd && (
+            {isProd && (
               <>
                 <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
                 <script dangerouslySetInnerHTML={{ __html: `
@@ -426,6 +176,20 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
                 `}} />
               </>
             )}
+            {!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);
+                  var vConsole = new VConsole({
+                    theme: urlParams.get('vconsole_theme') || 'light',
+                    onReady: function() {
+                      console.log('vConsole is ready');
+                    }
+                  });
+                `}} />
+              </>
+            )}
             <script src="https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js"></script>
           </head>
           <body className="bg-gray-50">

+ 111 - 0
server/middlewares.ts

@@ -0,0 +1,111 @@
+import { Hono } from 'hono'
+import { cors } from 'hono/cors'
+import type { Context as HonoContext } from 'hono'
+import { Auth } from '@d8d-appcontainer/auth'
+import type { User as AuthUser } from '@d8d-appcontainer/auth'
+import { APIClient } from '@d8d-appcontainer/api'
+import type { SystemSettingRecord } from '../client/share/types.ts'
+import debug from "debug"
+
+const log = {
+  auth: debug('auth:server')
+}
+
+// 定义自定义上下文类型
+export interface Variables {
+  auth: Auth
+  user?: AuthUser
+  apiClient: APIClient
+  moduleDir: string
+  systemSettings?: SystemSettingRecord
+}
+// 认证中间件
+export 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;
+
+// 环境变量设置中间件
+export const setEnvVariables = (apiClient: APIClient, moduleDir: string) => {
+  return async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
+    c.set('apiClient', apiClient)
+    c.set('moduleDir', moduleDir)
+    c.set('auth', await initAuth(apiClient))
+    c.set('systemSettings', await initSystemSettings(apiClient))
+    await next()
+  }
+}
+
+// CORS中间件
+export const corsMiddleware = cors()
+
+// 初始化Auth实例
+const initAuth = async (apiClient: APIClient) => {
+  try {
+    log.auth('正在初始化Auth实例')
+    
+    const auth = 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 auth
+    
+  } 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
+    
+    return settings
+    
+  } catch (error) {
+    log.auth('获取系统设置失败:', error)
+    return {} as SystemSettingRecord
+  }
+}

+ 41 - 37
server/migrations.ts

@@ -1,46 +1,50 @@
 import type { MigrationLiveDefinition } from '@d8d-appcontainer/types'
 
-// 动态加载迁移文件
-const migrations: MigrationLiveDefinition[] = [];
+/**
+ * 异步加载所有迁移文件
+ * @returns Promise<MigrationLiveDefinition[]> 返回加载并排序后的迁移数组
+ */
+export async function loadMigrations(): Promise<MigrationLiveDefinition[]> {
+  const migrations: MigrationLiveDefinition[] = [];
 
-try {
-  // 读取并加载所有迁移文件
-  const migrationsDir = import.meta.dirname + '/migrations'; 
-  
-  for await (const entry of Deno.readDir(migrationsDir)) {
-    if (!entry.isFile || !entry.name.endsWith('.ts')) continue;
+  try {
+    // 读取并加载所有迁移文件
+    const migrationsDir = import.meta.dirname + '/migrations';
     
-    // 匹配文件名格式:数字前缀_描述.ts
-    const match = entry.name.match(/^(\d+)_(.+)\.ts$/);
-    if (!match) continue;
-    
-    const [_, prefix, name] = match;
-    try {
-      const migration = await import(`${migrationsDir}/${entry.name}`);
+    for await (const entry of Deno.readDir(migrationsDir)) {
+      if (!entry.isFile || !entry.name.endsWith('.ts')) continue;
+      
+      // 匹配文件名格式:数字前缀_描述.ts
+      const match = entry.name.match(/^(\d+)_(.+)\.ts$/);
+      if (!match) continue;
       
-      // 确保导出的迁移对象有效
-      if (migration?.default?.name && migration?.default?.up && migration?.default?.down) {
-        migrations.push(migration.default);
-        console.log(`✅ Loaded migration: ${entry.name}`);
-      } else {
-        console.warn(`⚠️ Invalid migration format in ${entry.name}`);
+      const [_, prefix, name] = match;
+      try {
+        const migration = await import(`${migrationsDir}/${entry.name}`);
+        
+        // 确保导出的迁移对象有效
+        if (migration?.default?.name && migration?.default?.up && migration?.default?.down) {
+          migrations.push(migration.default);
+          console.log(`✅ Loaded migration: ${entry.name}`);
+        } else {
+          console.warn(`⚠️ Invalid migration format in ${entry.name}`);
+        }
+      } catch (err) {
+        console.error(`❌ Failed to load migration ${entry.name}:`, err);
       }
-    } catch (err) {
-      console.error(`❌ Failed to load migration ${entry.name}:`, err);
     }
-  }
 
-  // 按数字前缀排序
-  migrations.sort((a, b) => {
-    const aNum = parseInt(a.name.match(/^(\d+)_/)?.[1] || '0');
-    const bNum = parseInt(b.name.match(/^(\d+)_/)?.[1] || '0');
-    return aNum - bNum;
-  });
+    // 按数字前缀排序
+    migrations.sort((a, b) => {
+      const aNum = parseInt(a.name.match(/^(\d+)_/)?.[1] || '0');
+      const bNum = parseInt(b.name.match(/^(\d+)_/)?.[1] || '0');
+      return aNum - bNum;
+    });
 
-  console.log(`🎉 Successfully loaded ${migrations.length} migrations`);
-} catch (err) {
-  console.error('❌ Failed to load migrations:', err);
-}
-
-// 导出所有迁移
-export { migrations };
+    console.log(`🎉 Successfully loaded ${migrations.length} migrations`);
+    return migrations;
+  } catch (err) {
+    console.error('❌ Failed to load migrations:', err);
+    throw err;
+  }
+}

+ 50 - 0
server/router.ts

@@ -0,0 +1,50 @@
+/** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
+import { Hono } from 'hono'
+import { corsMiddleware, withAuth, setEnvVariables } from './middlewares.ts'
+import type { APIClient } from '@d8d-appcontainer/api'
+
+// 导入路由模块
+import { createAuthRoutes } from "./routes_auth.ts"
+import { createUserRoutes } from "./routes_users.ts"
+import { createKnowInfoRoutes } from "./routes_know_info.ts"
+import { createFileUploadRoutes } from "./routes_file_upload.ts"
+import { createFileCategoryRoutes } from "./routes_file_category.ts"
+import { createThemeRoutes } from "./routes_theme.ts"
+import { createChartRoutes } from "./routes_charts.ts"
+import { createMapRoutes } from "./routes_maps.ts"
+import { createSystemSettingsRoutes } from "./routes_system_settings.ts"
+import { createMessagesRoutes } from "./routes_messages.ts"
+import { createMigrationsRoutes } from "./routes_migrations.ts"
+import { createHomeRoutes } from "./routes_home.ts"
+
+export function createRouter(apiClient: APIClient, moduleDir: string) {
+  const router = new Hono()
+
+  // 添加CORS中间件
+  router.use('/*', corsMiddleware)
+
+  // 创建API路由
+  const api = new Hono()
+  
+  // 设置环境变量
+  api.use('*', setEnvVariables(apiClient, moduleDir))
+
+  // 注册所有路由
+  api.route('/auth', createAuthRoutes(withAuth))
+  api.route('/users', createUserRoutes(withAuth))
+  api.route('/know-infos', 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路由到主路由器
+  router.route('/api', api)
+
+  return router
+}

+ 1 - 2
server/routes_auth.ts

@@ -1,6 +1,5 @@
 import { Hono } from 'hono'
-import type { Variables } from './app.tsx'
-import type { WithAuth } from './app.tsx'
+import type { Variables, WithAuth } from "./middlewares.ts";
 
 export function createAuthRoutes(withAuth: WithAuth) {
   const authRoutes = new Hono<{ Variables: Variables }>()

+ 1 - 1
server/routes_charts.ts

@@ -4,7 +4,7 @@ import {
   DeleteStatus,
 } from "../client/share/types.ts";
 
-import type { Variables, WithAuth } from "./app.tsx";
+import type { Variables, WithAuth } from "./middlewares.ts";
 
 const log = {
   api: debug("api:sys"),

+ 147 - 0
server/routes_file_category.ts

@@ -0,0 +1,147 @@
+import { Hono } from "hono";
+import debug from "debug";
+import type {
+  FileCategory,
+} from "../client/share/types.ts";
+
+import {
+  DeleteStatus,
+} from "../client/share/types.ts";
+
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+const log = {
+  api: debug("api:sys"),
+};
+
+// 创建文件分类路由
+export function createFileCategoryRoutes(withAuth: WithAuth) {
+  const fileCategoryRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取文件分类列表
+  fileCategoryRoutes.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("file_category").orderBy("id", "desc");
+
+      if (search) {
+        query = query.where("name", "like", `%${search}%`);
+      }
+
+      const total = await query.clone().count();
+
+      const categories = await query
+        .select("id", "name", "code", "description")
+        .limit(pageSize)
+        .offset(offset);
+
+      return c.json({
+        data: categories,
+        total: Number(total),
+        page,
+        pageSize,
+      });
+    } catch (error) {
+      log.api("获取文件分类列表失败:", error);
+      return c.json({ error: "获取文件分类列表失败" }, 500);
+    }
+  });
+
+  // 创建文件分类
+  fileCategoryRoutes.post("/", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const data = (await c.req.json()) as Partial<FileCategory>;
+
+      // 验证必填字段
+      if (!data.name) {
+        return c.json({ error: "分类名称不能为空" }, 400);
+      }
+
+      // 插入文件分类
+      const [id] = await apiClient.database.table("file_category").insert({
+        ...data,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now(),
+      });
+
+      return c.json({
+        message: "文件分类创建成功",
+        data: {
+          id,
+          ...data,
+        },
+      });
+    } catch (error) {
+      log.api("创建文件分类失败:", error);
+      return c.json({ error: "创建文件分类失败" }, 500);
+    }
+  });
+
+  // 更新文件分类
+  fileCategoryRoutes.put("/:id", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const id = Number(c.req.param("id"));
+
+      if (!id || isNaN(id)) {
+        return c.json({ error: "无效的分类ID" }, 400);
+      }
+
+      const data = (await c.req.json()) as Partial<FileCategory>;
+
+      // 更新文件分类
+      await apiClient.database
+        .table("file_category")
+        .where("id", id)
+        .update({
+          ...data,
+          updated_at: apiClient.database.fn.now(),
+        });
+
+      return c.json({
+        message: "文件分类更新成功",
+        data: {
+          id,
+          ...data,
+        },
+      });
+    } catch (error) {
+      log.api("更新文件分类失败:", error);
+      return c.json({ error: "更新文件分类失败" }, 500);
+    }
+  });
+
+  // 删除文件分类
+  fileCategoryRoutes.delete("/:id", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const id = Number(c.req.param("id"));
+
+      if (!id || isNaN(id)) {
+        return c.json({ error: "无效的分类ID" }, 400);
+      }
+
+      await apiClient.database.table("file_category").where("id", id).update({
+        is_deleted: DeleteStatus.DELETED,
+        updated_at: apiClient.database.fn.now(),
+      });
+
+      return c.json({
+        message: "文件分类删除成功",
+      });
+    } catch (error) {
+      log.api("删除文件分类失败:", error);
+      return c.json({ error: "删除文件分类失败" }, 500);
+    }
+  });
+  
+  return fileCategoryRoutes;
+}

+ 258 - 0
server/routes_file_upload.ts

@@ -0,0 +1,258 @@
+import { Hono } from "hono";
+import debug from "debug";
+import type {
+  FileLibrary,
+} from "../client/share/types.ts";
+
+import {
+  DeleteStatus,
+  EnableStatus,
+} from "../client/share/types.ts";
+
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+const log = {
+  api: debug("api:sys"),
+};
+
+// 创建文件上传路由
+export function createFileUploadRoutes(withAuth: WithAuth) {
+  const fileUploadRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取 MinIO 上传策略
+  fileUploadRoutes.get("/policy", withAuth, async (c) => {
+    try {
+      const prefix = c.req.query("prefix") || "uploads/";
+      const filename = c.req.query("filename");
+      const maxSize = Number(c.req.query("maxSize")) || 10 * 1024 * 1024; // 默认10MB
+
+      if (!filename) {
+        return c.json({ error: "文件名不能为空" }, 400);
+      }
+
+      const apiClient = c.get('apiClient');
+      const policy = await apiClient.storage.getUploadPolicy(
+        prefix,
+        filename,
+        maxSize
+      );
+
+      return c.json({
+        message: "获取上传策略成功",
+        data: policy,
+      });
+    } catch (error) {
+      log.api("获取上传策略失败:", error);
+      return c.json({ error: "获取上传策略失败" }, 500);
+    }
+  });
+
+  // 保存文件信息到文件库
+  fileUploadRoutes.post("/save", withAuth, async (c) => {
+    try {
+      const fileData = (await c.req.json()) as Partial<FileLibrary>;
+      const user = c.get("user");
+
+      // 验证必填字段
+      if (!fileData.file_name || !fileData.file_path || !fileData.file_type) {
+        return c.json({ error: "文件名、路径和类型不能为空" }, 400);
+      }
+
+      const apiClient = c.get('apiClient');
+
+      // 设置上传者信息
+      if (user) {
+        fileData.uploader_id = user.id;
+        fileData.uploader_name = user.nickname || user.username;
+      }
+
+      // 插入文件库记录
+      const [id] = await apiClient.database.table("file_library").insert({
+        ...fileData,
+        download_count: 0,
+        is_disabled: EnableStatus.ENABLED,
+        is_deleted: DeleteStatus.NOT_DELETED,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now(),
+      });
+
+      // 获取插入的数据
+      const [insertedFile] = await apiClient.database
+        .table("file_library")
+        .where("id", id);
+
+      return c.json({
+        message: "文件信息保存成功",
+        data: insertedFile,
+      });
+    } catch (error) {
+      log.api("保存文件信息失败:", error);
+      return c.json({ error: "保存文件信息失败" }, 500);
+    }
+  });
+
+  // 获取文件列表
+  fileUploadRoutes.get("/list", withAuth, async (c) => {
+    try {
+      const page = Number(c.req.query("page")) || 1;
+      const pageSize = Number(c.req.query("pageSize")) || 10;
+      const category_id = c.req.query("category_id");
+      const fileType = c.req.query("fileType");
+      const keyword = c.req.query("keyword");
+
+      const apiClient = c.get('apiClient');
+      let query = apiClient.database
+        .table("file_library")
+        .where("is_deleted", DeleteStatus.NOT_DELETED)
+        .orderBy("created_at", "desc");
+
+      // 应用过滤条件
+      if (category_id) {
+        query = query.where("category_id", category_id);
+      }
+
+      if (fileType) {
+        query = query.where("file_type", fileType);
+      }
+
+      if (keyword) {
+        query = query.where((builder) => {
+          builder
+            .where("file_name", "like", `%${keyword}%`)
+            .orWhere("description", "like", `%${keyword}%`)
+            .orWhere("tags", "like", `%${keyword}%`);
+        });
+      }
+
+      // 获取总数
+      const total = await query.clone().count();
+
+      // 分页查询
+      const files = await query.limit(pageSize).offset((page - 1) * pageSize);
+
+      return c.json({
+        message: "获取文件列表成功",
+        data: {
+          list: files,
+          pagination: {
+            current: page,
+            pageSize,
+            total: Number(total),
+          },
+        },
+      });
+    } catch (error) {
+      log.api("获取文件列表失败:", error);
+      return c.json({ error: "获取文件列表失败" }, 500);
+    }
+  });
+
+  // 获取单个文件信息
+  fileUploadRoutes.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 file = await apiClient.database
+        .table("file_library")
+        .where("id", id)
+        .where("is_deleted", DeleteStatus.NOT_DELETED)
+        .first();
+
+      if (!file) {
+        return c.json({ error: "文件不存在" }, 404);
+      }
+
+      return c.json({
+        message: "获取文件信息成功",
+        data: file,
+      });
+    } catch (error) {
+      log.api("获取文件信息失败:", error);
+      return c.json({ error: "获取文件信息失败" }, 500);
+    }
+  });
+
+  // 增加文件下载计数
+  fileUploadRoutes.post("/:id/download", 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 file = await apiClient.database
+        .table("file_library")
+        .where("id", id)
+        .where("is_deleted", DeleteStatus.NOT_DELETED)
+        .first();
+
+      if (!file) {
+        return c.json({ error: "文件不存在" }, 404);
+      }
+
+      // 增加下载计数
+      await apiClient.database
+        .table("file_library")
+        .where("id", id)
+        .update({
+          download_count: apiClient.database.raw("download_count + 1"),
+          updated_at: apiClient.database.fn.now(),
+        });
+
+      return c.json({
+        message: "更新下载计数成功",
+      });
+    } catch (error) {
+      log.api("更新下载计数失败:", error);
+      return c.json({ error: "更新下载计数失败" }, 500);
+    }
+  });
+
+  // 删除文件
+  fileUploadRoutes.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 file = await apiClient.database
+        .table("file_library")
+        .where("id", id)
+        .where("is_deleted", DeleteStatus.NOT_DELETED)
+        .first();
+
+      if (!file) {
+        return c.json({ error: "文件不存在" }, 404);
+      }
+
+      // 软删除文件
+      await apiClient.database.table("file_library").where("id", id).update({
+        is_deleted: DeleteStatus.DELETED,
+        updated_at: apiClient.database.fn.now(),
+      });
+
+      return c.json({
+        message: "文件删除成功",
+      });
+    } catch (error) {
+      log.api("删除文件失败:", error);
+      return c.json({ error: "删除文件失败" }, 500);
+    }
+  });
+  
+  return fileUploadRoutes;
+}

+ 1 - 2
server/routes_home.ts

@@ -1,6 +1,5 @@
 import { Hono } from 'hono'
-import type { Variables } from './app.tsx'
-import type { WithAuth } from './app.tsx'
+import type { Variables, WithAuth } from "./middlewares.ts";
 import { AuditStatus } from '../client/share/types.ts'
 
 export function createHomeRoutes(withAuth: WithAuth) {

+ 228 - 0
server/routes_know_info.ts

@@ -0,0 +1,228 @@
+import { Hono } from "hono";
+import debug from "debug";
+import type {
+  KnowInfo,
+} from "../client/share/types.ts";
+
+import {
+  EnableStatus,
+  DeleteStatus,
+} from "../client/share/types.ts";
+
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+const log = {
+  api: debug("api:sys"),
+};
+
+// 创建知识库管理路由
+export function createKnowInfoRoutes(withAuth: WithAuth) {
+  const knowInfoRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取知识库文章列表
+  knowInfoRoutes.get("/", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+
+      // 获取分页参数
+      const page = Number(c.req.query("page")) || 1;
+      const limit = Number(c.req.query("limit")) || 10;
+      const offset = (page - 1) * limit;
+
+      // 获取筛选参数
+      const title = c.req.query("title");
+      const category = c.req.query("category");
+      const tags = c.req.query("tags");
+
+      // 构建查询
+      let query = apiClient.database
+        .table("know_info")
+        .where("is_deleted", 0)
+        .orderBy("id", "desc");
+
+      // 应用筛选条件
+      if (title) {
+        query = query.where("title", "like", `%${title}%`);
+      }
+
+      if (category) {
+        query = query.where("category", category);
+      }
+
+      if (tags) {
+        query = query.where("tags", "like", `%${tags}%`);
+      }
+
+      // 克隆查询以获取总数
+      const countQuery = query.clone();
+
+      // 执行分页查询
+      const articles = await query.limit(limit).offset(offset);
+
+      // 获取总数
+      const count = await countQuery.count();
+
+      return c.json({
+        data: articles,
+        pagination: {
+          total: Number(count),
+          current: page,
+          pageSize: limit,
+          totalPages: Math.ceil(Number(count) / limit),
+        },
+      });
+    } catch (error) {
+      log.api("获取知识库文章列表失败:", error);
+      return c.json({ error: "获取知识库文章列表失败" }, 500);
+    }
+  });
+
+  // 获取单个知识库文章
+  knowInfoRoutes.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 [article] = await apiClient.database
+        .table("know_info")
+        .where({ id, is_deleted: 0 });
+
+      if (!article) {
+        return c.json({ error: "文章不存在" }, 404);
+      }
+
+      return c.json({message: '获取知识库文章详情成功', data: article});
+    } catch (error) {
+      log.api("获取知识库文章详情失败:", error);
+      return c.json({ error: "获取知识库文章详情失败" }, 500);
+    }
+  });
+
+  // 创建知识库文章
+  knowInfoRoutes.post("/", withAuth, async (c) => {
+    try {
+      const articleData = (await c.req.json()) as Partial<KnowInfo>;
+
+      // 验证必填字段
+      if (!articleData.title) {
+        return c.json({ error: "文章标题不能为空" }, 400);
+      }
+
+      // 如果作者为空,则使用当前用户的用户名
+      if (!articleData.author) {
+        const user = c.get("user");
+        articleData.author = user ? user.username : "unknown";
+      }
+
+      const apiClient = c.get('apiClient');
+      const [id] = await apiClient.database
+        .table("know_info")
+        .insert(articleData);
+
+      // 获取创建的文章
+      const [createdArticle] = await apiClient.database
+        .table("know_info")
+        .where("id", id);
+
+      return c.json({
+        message: "知识库文章创建成功",
+        data: createdArticle,
+      });
+    } catch (error) {
+      log.api("创建知识库文章失败:", error);
+      return c.json({ error: "创建知识库文章失败" }, 500);
+    }
+  });
+
+  // 更新知识库文章
+  knowInfoRoutes.put("/:id", withAuth, async (c) => {
+    try {
+      const id = Number(c.req.param("id"));
+
+      if (!id || isNaN(id)) {
+        return c.json({ error: "无效的文章ID" }, 400);
+      }
+
+      const articleData = (await c.req.json()) as Partial<KnowInfo>;
+
+      // 验证必填字段
+      if (!articleData.title) {
+        return c.json({ error: "文章标题不能为空" }, 400);
+      }
+
+      const apiClient = c.get('apiClient');
+
+      // 检查文章是否存在
+      const [existingArticle] = await apiClient.database
+        .table("know_info")
+        .where({ id, is_deleted: 0 });
+
+      if (!existingArticle) {
+        return c.json({ error: "文章不存在" }, 404);
+      }
+
+      // 更新文章
+      await apiClient.database
+        .table("know_info")
+        .where("id", id)
+        .update({
+          ...articleData,
+          updated_at: apiClient.database.fn.now(),
+        });
+
+      // 获取更新后的文章
+      const [updatedArticle] = await apiClient.database
+        .table("know_info")
+        .where("id", id);
+
+      return c.json({
+        message: "知识库文章更新成功",
+        data: updatedArticle,
+      });
+    } catch (error) {
+      log.api("更新知识库文章失败:", error);
+      return c.json({ error: "更新知识库文章失败" }, 500);
+    }
+  });
+
+  // 删除知识库文章(软删除)
+  knowInfoRoutes.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 [existingArticle] = await apiClient.database
+        .table("know_info")
+        .where({ id, is_deleted: 0 });
+
+      if (!existingArticle) {
+        return c.json({ error: "文章不存在" }, 404);
+      }
+
+      // 软删除文章
+      await apiClient.database.table("know_info").where("id", id).update({
+        is_deleted: 1,
+        updated_at: apiClient.database.fn.now(),
+      });
+
+      return c.json({
+        message: "知识库文章删除成功",
+      });
+    } catch (error) {
+      log.api("删除知识库文章失败:", error);
+      return c.json({ error: "删除知识库文章失败" }, 500);
+    }
+  });
+  
+  return knowInfoRoutes;
+}

+ 1 - 1
server/routes_maps.ts

@@ -1,7 +1,7 @@
 import { Hono } from "hono";
 import debug from "debug";
 
-import type { Variables, WithAuth } from "./app.tsx";
+import type { Variables, WithAuth } from "./middlewares.ts";
 
 const log = {
   api: debug("api:sys"),

+ 1 - 2
server/routes_messages.ts

@@ -1,6 +1,5 @@
 import { Hono } from 'hono'
-import type { Variables } from './app.tsx'
-import type { WithAuth } from './app.tsx'
+import type { Variables, WithAuth } from "./middlewares.ts";
 import { MessageType, MessageStatus } from '../client/share/types.ts'
 
 export function createMessagesRoutes(withAuth: WithAuth) {

+ 4 - 4
server/routes_migrations.ts

@@ -1,8 +1,7 @@
 import { Hono } from 'hono'
 import { APIClient } from '@d8d-appcontainer/api'
-import type { Variables } from './app.tsx'
-import type { WithAuth } from './app.tsx'
-import { migrations } from './migrations.ts'
+import type { Variables, WithAuth } from "./middlewares.ts";
+import { loadMigrations } from './migrations.ts'
 import debug from "debug";
 const log = {
   api: debug("api:migrations"),
@@ -14,6 +13,7 @@ export function createMigrationsRoutes(withAuth: WithAuth) {
   migrationsRoutes.get('/', async (c) => {
     const apiClient = c.get('apiClient')
     log.api('正在执行数据库迁移...')
+    const migrations = await loadMigrations()
     const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
     // log.app('数据库迁移完成 %O',migrationsResult)
     
@@ -48,7 +48,7 @@ export function createMigrationsRoutes(withAuth: WithAuth) {
     const apiClient = c.get('apiClient')
     const all = c.req.query('all') === 'true'
     log.api('正在执行数据库回滚...')
-    
+    const migrations = await loadMigrations()
     const rollbackResult = await apiClient.database.rollbackLiveMigrations(migrations, all)
     
     const failedResult = rollbackResult?.find((migration) => migration.status === 'failed')

+ 0 - 1063
server/routes_sys.ts

@@ -1,1063 +0,0 @@
-import { Hono } from "hono";
-import debug from "debug";
-import type {
-  FileLibrary,
-  FileCategory,
-  KnowInfo,
-  ThemeSettings,
-  SystemSetting,
-  SystemSettingGroupData,
-} from "../client/share/types.ts";
-
-import {
-  EnableStatus,
-  DeleteStatus,
-  ThemeMode,
-  FontSize,
-  CompactMode,
-} from "../client/share/types.ts";
-
-import type { Variables, WithAuth } from "./app.tsx";
-
-const log = {
-  api: debug("api:sys"),
-};
-
-// 创建知识库管理路由
-export function createKnowInfoRoutes(withAuth: WithAuth) {
-  const knowInfoRoutes = new Hono<{ Variables: Variables }>();
-
-  // 获取知识库文章列表
-  knowInfoRoutes.get("/", withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-
-      // 获取分页参数
-      const page = Number(c.req.query("page")) || 1;
-      const limit = Number(c.req.query("limit")) || 10;
-      const offset = (page - 1) * limit;
-
-      // 获取筛选参数
-      const title = c.req.query("title");
-      const category = c.req.query("category");
-      const tags = c.req.query("tags");
-
-      // 构建查询
-      let query = apiClient.database
-        .table("know_info")
-        .where("is_deleted", 0)
-        .orderBy("id", "desc");
-
-      // 应用筛选条件
-      if (title) {
-        query = query.where("title", "like", `%${title}%`);
-      }
-
-      if (category) {
-        query = query.where("category", category);
-      }
-
-      if (tags) {
-        query = query.where("tags", "like", `%${tags}%`);
-      }
-
-      // 克隆查询以获取总数
-      const countQuery = query.clone();
-
-      // 执行分页查询
-      const articles = await query.limit(limit).offset(offset);
-
-      // 获取总数
-      const count = await countQuery.count();
-
-      return c.json({
-        data: articles,
-        pagination: {
-          total: Number(count),
-          current: page,
-          pageSize: limit,
-          totalPages: Math.ceil(Number(count) / limit),
-        },
-      });
-    } catch (error) {
-      log.api("获取知识库文章列表失败:", error);
-      return c.json({ error: "获取知识库文章列表失败" }, 500);
-    }
-  });
-
-  // 获取单个知识库文章
-  knowInfoRoutes.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 [article] = await apiClient.database
-        .table("know_info")
-        .where({ id, is_deleted: 0 });
-
-      if (!article) {
-        return c.json({ error: "文章不存在" }, 404);
-      }
-
-      return c.json({message: '获取知识库文章详情成功', data: article});
-    } catch (error) {
-      log.api("获取知识库文章详情失败:", error);
-      return c.json({ error: "获取知识库文章详情失败" }, 500);
-    }
-  });
-
-  // 创建知识库文章
-  knowInfoRoutes.post("/", withAuth, async (c) => {
-    try {
-      const articleData = (await c.req.json()) as Partial<KnowInfo>;
-
-      // 验证必填字段
-      if (!articleData.title) {
-        return c.json({ error: "文章标题不能为空" }, 400);
-      }
-
-      // 如果作者为空,则使用当前用户的用户名
-      if (!articleData.author) {
-        const user = c.get("user");
-        articleData.author = user ? user.username : "unknown";
-      }
-
-      const apiClient = c.get('apiClient');
-      const [id] = await apiClient.database
-        .table("know_info")
-        .insert(articleData);
-
-      // 获取创建的文章
-      const [createdArticle] = await apiClient.database
-        .table("know_info")
-        .where("id", id);
-
-      return c.json({
-        message: "知识库文章创建成功",
-        data: createdArticle,
-      });
-    } catch (error) {
-      log.api("创建知识库文章失败:", error);
-      return c.json({ error: "创建知识库文章失败" }, 500);
-    }
-  });
-
-  // 更新知识库文章
-  knowInfoRoutes.put("/:id", withAuth, async (c) => {
-    try {
-      const id = Number(c.req.param("id"));
-
-      if (!id || isNaN(id)) {
-        return c.json({ error: "无效的文章ID" }, 400);
-      }
-
-      const articleData = (await c.req.json()) as Partial<KnowInfo>;
-
-      // 验证必填字段
-      if (!articleData.title) {
-        return c.json({ error: "文章标题不能为空" }, 400);
-      }
-
-      const apiClient = c.get('apiClient');
-
-      // 检查文章是否存在
-      const [existingArticle] = await apiClient.database
-        .table("know_info")
-        .where({ id, is_deleted: 0 });
-
-      if (!existingArticle) {
-        return c.json({ error: "文章不存在" }, 404);
-      }
-
-      // 更新文章
-      await apiClient.database
-        .table("know_info")
-        .where("id", id)
-        .update({
-          ...articleData,
-          updated_at: apiClient.database.fn.now(),
-        });
-
-      // 获取更新后的文章
-      const [updatedArticle] = await apiClient.database
-        .table("know_info")
-        .where("id", id);
-
-      return c.json({
-        message: "知识库文章更新成功",
-        data: updatedArticle,
-      });
-    } catch (error) {
-      log.api("更新知识库文章失败:", error);
-      return c.json({ error: "更新知识库文章失败" }, 500);
-    }
-  });
-
-  // 删除知识库文章(软删除)
-  knowInfoRoutes.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 [existingArticle] = await apiClient.database
-        .table("know_info")
-        .where({ id, is_deleted: 0 });
-
-      if (!existingArticle) {
-        return c.json({ error: "文章不存在" }, 404);
-      }
-
-      // 软删除文章
-      await apiClient.database.table("know_info").where("id", id).update({
-        is_deleted: 1,
-        updated_at: apiClient.database.fn.now(),
-      });
-
-      return c.json({
-        message: "知识库文章删除成功",
-      });
-    } catch (error) {
-      log.api("删除知识库文章失败:", error);
-      return c.json({ error: "删除知识库文章失败" }, 500);
-    }
-  });
-  
-  return knowInfoRoutes;
-}
-
-// 创建文件分类路由
-export function createFileCategoryRoutes(withAuth: WithAuth) {
-  const fileCategoryRoutes = new Hono<{ Variables: Variables }>();
-
-  // 获取文件分类列表
-  fileCategoryRoutes.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("file_category").orderBy("id", "desc");
-
-      if (search) {
-        query = query.where("name", "like", `%${search}%`);
-      }
-
-      const total = await query.clone().count();
-
-      const categories = await query
-        .select("id", "name", "code", "description")
-        .limit(pageSize)
-        .offset(offset);
-
-      return c.json({
-        data: categories,
-        total: Number(total),
-        page,
-        pageSize,
-      });
-    } catch (error) {
-      log.api("获取文件分类列表失败:", error);
-      return c.json({ error: "获取文件分类列表失败" }, 500);
-    }
-  });
-
-  // 创建文件分类
-  fileCategoryRoutes.post("/", withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const data = (await c.req.json()) as Partial<FileCategory>;
-
-      // 验证必填字段
-      if (!data.name) {
-        return c.json({ error: "分类名称不能为空" }, 400);
-      }
-
-      // 插入文件分类
-      const [id] = await apiClient.database.table("file_category").insert({
-        ...data,
-        created_at: apiClient.database.fn.now(),
-        updated_at: apiClient.database.fn.now(),
-      });
-
-      return c.json({
-        message: "文件分类创建成功",
-        data: {
-          id,
-          ...data,
-        },
-      });
-    } catch (error) {
-      log.api("创建文件分类失败:", error);
-      return c.json({ error: "创建文件分类失败" }, 500);
-    }
-  });
-
-  // 更新文件分类
-  fileCategoryRoutes.put("/:id", withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const id = Number(c.req.param("id"));
-
-      if (!id || isNaN(id)) {
-        return c.json({ error: "无效的分类ID" }, 400);
-      }
-
-      const data = (await c.req.json()) as Partial<FileCategory>;
-
-      // 更新文件分类
-      await apiClient.database
-        .table("file_category")
-        .where("id", id)
-        .update({
-          ...data,
-          updated_at: apiClient.database.fn.now(),
-        });
-
-      return c.json({
-        message: "文件分类更新成功",
-        data: {
-          id,
-          ...data,
-        },
-      });
-    } catch (error) {
-      log.api("更新文件分类失败:", error);
-      return c.json({ error: "更新文件分类失败" }, 500);
-    }
-  });
-
-  // 删除文件分类
-  fileCategoryRoutes.delete("/:id", withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const id = Number(c.req.param("id"));
-
-      if (!id || isNaN(id)) {
-        return c.json({ error: "无效的分类ID" }, 400);
-      }
-
-      await apiClient.database.table("file_category").where("id", id).update({
-        is_deleted: DeleteStatus.DELETED,
-        updated_at: apiClient.database.fn.now(),
-      });
-
-      return c.json({
-        message: "文件分类删除成功",
-      });
-    } catch (error) {
-      log.api("删除文件分类失败:", error);
-      return c.json({ error: "删除文件分类失败" }, 500);
-    }
-  });
-  
-  return fileCategoryRoutes;
-}
-
-// 创建文件上传路由
-export function createFileUploadRoutes(withAuth: WithAuth) {
-  const fileUploadRoutes = new Hono<{ Variables: Variables }>();
-
-  // 获取 MinIO 上传策略
-  fileUploadRoutes.get("/policy", withAuth, async (c) => {
-    try {
-      const prefix = c.req.query("prefix") || "uploads/";
-      const filename = c.req.query("filename");
-      const maxSize = Number(c.req.query("maxSize")) || 10 * 1024 * 1024; // 默认10MB
-
-      if (!filename) {
-        return c.json({ error: "文件名不能为空" }, 400);
-      }
-
-      const apiClient = c.get('apiClient');
-      const policy = await apiClient.storage.getUploadPolicy(
-        prefix,
-        filename,
-        maxSize
-      );
-
-      return c.json({
-        message: "获取上传策略成功",
-        data: policy,
-      });
-    } catch (error) {
-      log.api("获取上传策略失败:", error);
-      return c.json({ error: "获取上传策略失败" }, 500);
-    }
-  });
-
-  // 保存文件信息到文件库
-  fileUploadRoutes.post("/save", withAuth, async (c) => {
-    try {
-      const fileData = (await c.req.json()) as Partial<FileLibrary>;
-      const user = c.get("user");
-
-      // 验证必填字段
-      if (!fileData.file_name || !fileData.file_path || !fileData.file_type) {
-        return c.json({ error: "文件名、路径和类型不能为空" }, 400);
-      }
-
-      const apiClient = c.get('apiClient');
-
-      // 设置上传者信息
-      if (user) {
-        fileData.uploader_id = user.id;
-        fileData.uploader_name = user.nickname || user.username;
-      }
-
-      // 插入文件库记录
-      const [id] = await apiClient.database.table("file_library").insert({
-        ...fileData,
-        download_count: 0,
-        is_disabled: EnableStatus.ENABLED,
-        is_deleted: DeleteStatus.NOT_DELETED,
-        created_at: apiClient.database.fn.now(),
-        updated_at: apiClient.database.fn.now(),
-      });
-
-      // 获取插入的数据
-      const [insertedFile] = await apiClient.database
-        .table("file_library")
-        .where("id", id);
-
-      return c.json({
-        message: "文件信息保存成功",
-        data: insertedFile,
-      });
-    } catch (error) {
-      log.api("保存文件信息失败:", error);
-      return c.json({ error: "保存文件信息失败" }, 500);
-    }
-  });
-
-  // 获取文件列表
-  fileUploadRoutes.get("/list", withAuth, async (c) => {
-    try {
-      const page = Number(c.req.query("page")) || 1;
-      const pageSize = Number(c.req.query("pageSize")) || 10;
-      const category_id = c.req.query("category_id");
-      const fileType = c.req.query("fileType");
-      const keyword = c.req.query("keyword");
-
-      const apiClient = c.get('apiClient');
-      let query = apiClient.database
-        .table("file_library")
-        .where("is_deleted", DeleteStatus.NOT_DELETED)
-        .orderBy("created_at", "desc");
-
-      // 应用过滤条件
-      if (category_id) {
-        query = query.where("category_id", category_id);
-      }
-
-      if (fileType) {
-        query = query.where("file_type", fileType);
-      }
-
-      if (keyword) {
-        query = query.where((builder) => {
-          builder
-            .where("file_name", "like", `%${keyword}%`)
-            .orWhere("description", "like", `%${keyword}%`)
-            .orWhere("tags", "like", `%${keyword}%`);
-        });
-      }
-
-      // 获取总数
-      const total = await query.clone().count();
-
-      // 分页查询
-      const files = await query.limit(pageSize).offset((page - 1) * pageSize);
-
-      return c.json({
-        message: "获取文件列表成功",
-        data: {
-          list: files,
-          pagination: {
-            current: page,
-            pageSize,
-            total: Number(total),
-          },
-        },
-      });
-    } catch (error) {
-      log.api("获取文件列表失败:", error);
-      return c.json({ error: "获取文件列表失败" }, 500);
-    }
-  });
-
-  // 获取单个文件信息
-  fileUploadRoutes.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 file = await apiClient.database
-        .table("file_library")
-        .where("id", id)
-        .where("is_deleted", DeleteStatus.NOT_DELETED)
-        .first();
-
-      if (!file) {
-        return c.json({ error: "文件不存在" }, 404);
-      }
-
-      return c.json({
-        message: "获取文件信息成功",
-        data: file,
-      });
-    } catch (error) {
-      log.api("获取文件信息失败:", error);
-      return c.json({ error: "获取文件信息失败" }, 500);
-    }
-  });
-
-  // 增加文件下载计数
-  fileUploadRoutes.post("/:id/download", 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 file = await apiClient.database
-        .table("file_library")
-        .where("id", id)
-        .where("is_deleted", DeleteStatus.NOT_DELETED)
-        .first();
-
-      if (!file) {
-        return c.json({ error: "文件不存在" }, 404);
-      }
-
-      // 增加下载计数
-      await apiClient.database
-        .table("file_library")
-        .where("id", id)
-        .update({
-          download_count: apiClient.database.raw("download_count + 1"),
-          updated_at: apiClient.database.fn.now(),
-        });
-
-      return c.json({
-        message: "更新下载计数成功",
-      });
-    } catch (error) {
-      log.api("更新下载计数失败:", error);
-      return c.json({ error: "更新下载计数失败" }, 500);
-    }
-  });
-
-  // 删除文件
-  fileUploadRoutes.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 file = await apiClient.database
-        .table("file_library")
-        .where("id", id)
-        .where("is_deleted", DeleteStatus.NOT_DELETED)
-        .first();
-
-      if (!file) {
-        return c.json({ error: "文件不存在" }, 404);
-      }
-
-      // 软删除文件
-      await apiClient.database.table("file_library").where("id", id).update({
-        is_deleted: DeleteStatus.DELETED,
-        updated_at: apiClient.database.fn.now(),
-      });
-
-      return c.json({
-        message: "文件删除成功",
-      });
-    } catch (error) {
-      log.api("删除文件失败:", error);
-      return c.json({ error: "删除文件失败" }, 500);
-    }
-  });
-  
-  return fileUploadRoutes;
-}
-
-// 创建主题设置路由
-export function createThemeRoutes(withAuth: WithAuth) {
-  const themeRoutes = new Hono<{ Variables: Variables }>();
-
-  // 获取当前主题设置
-  themeRoutes.get("/", withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const user = c.get('user');
-
-      if (!user) {
-        return c.json({ error: "未授权访问" }, 401);
-      }
-
-      // 获取用户的主题设置
-      let themeSettings = await apiClient.database
-        .table("theme_settings")
-        .where("user_id", user.id)
-        .first();
-
-      // 如果用户没有主题设置,则创建默认设置
-      if (!themeSettings) {
-        const defaultSettings = {
-          theme_mode: ThemeMode.LIGHT,
-          primary_color: '#1890ff',
-          font_size: FontSize.MEDIUM,
-          is_compact: CompactMode.NORMAL
-        };
-
-        const [id] = await apiClient.database.table("theme_settings").insert({
-          user_id: user.id,
-          settings: defaultSettings,
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now(),
-        });
-
-        themeSettings = await apiClient.database
-          .table("theme_settings")
-          .where("id", id)
-          .first();
-      }
-
-      return c.json({
-        message: "获取主题设置成功",
-        data: themeSettings?.settings,
-      });
-    } catch (error) {
-      log.api("获取主题设置失败:", error);
-      return c.json({ error: "获取主题设置失败" }, 500);
-    }
-  });
-
-  // 更新主题设置
-  themeRoutes.put("/", withAuth, async (c) => {
-    try {
-      const user = c.get('user');
-      const apiClient = c.get('apiClient');
-
-      if (!user) {
-        return c.json({ error: "未授权访问" }, 401);
-      }
-
-      const themeData = (await c.req.json()) as Partial<ThemeSettings>;
-
-      // 检查用户是否已有主题设置
-      const existingTheme = await apiClient.database
-        .table("theme_settings")
-        .where("user_id", user.id)
-        .first();
-
-      if (existingTheme) {
-        // 更新现有设置
-        const currentSettings = existingTheme.settings || {};
-        const updatedSettings = {
-          ...currentSettings,
-          ...themeData
-        };
-
-        await apiClient.database
-          .table("theme_settings")
-          .where("user_id", user.id)
-          .update({
-            settings: JSON.stringify(updatedSettings),
-            updated_at: apiClient.database.fn.now(),
-          });
-      } else {
-        // 创建新设置
-        const defaultSettings = {
-          theme_mode: ThemeMode.LIGHT,
-          primary_color: '#1890ff',
-          font_size: FontSize.MEDIUM,
-          is_compact: CompactMode.NORMAL
-        };
-
-        const updatedSettings = {
-          ...defaultSettings,
-          ...themeData
-        };
-
-        await apiClient.database.table("theme_settings").insert({
-          user_id: user.id,
-          settings: updatedSettings,
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now(),
-        });
-      }
-
-      // 获取更新后的主题设置
-      const updatedTheme = await apiClient.database
-        .table("theme_settings")
-        .where("user_id", user.id)
-        .first();
-
-      return c.json({
-        message: "主题设置更新成功",
-        data: updatedTheme,
-      });
-    } catch (error) {
-      log.api("更新主题设置失败:", error);
-      return c.json({ error: "更新主题设置失败" }, 500);
-    }
-  });
-
-  // 重置主题设置为默认值
-  themeRoutes.post("/reset", withAuth, async (c) => {
-    try {
-      const user = c.get('user');
-      const apiClient = c.get('apiClient');
-
-      if (!user) {
-        return c.json({ error: "未授权访问" }, 401);
-      }
-
-      // 默认主题设置
-      const defaultSettings = {
-        theme_mode: ThemeMode.LIGHT,
-        primary_color: '#1890ff',
-        font_size: FontSize.MEDIUM,
-        is_compact: CompactMode.NORMAL
-      };
-
-      // 更新用户的主题设置
-      await apiClient.database
-        .table("theme_settings")
-        .where("user_id", user.id)
-        .update({
-          settings: JSON.stringify(defaultSettings),
-          updated_at: apiClient.database.fn.now(),
-        });
-
-      // 获取更新后的主题设置
-      const updatedTheme = await apiClient.database
-        .table("theme_settings")
-        .where("user_id", user.id)
-        .first();
-
-      return c.json({
-        message: "主题设置已重置为默认值",
-        data: updatedTheme,
-      });
-    } catch (error) {
-      log.api("重置主题设置失败:", error);
-      return c.json({ error: "重置主题设置失败" }, 500);
-    }
-  });
-
-  return themeRoutes;
-}
-
-// 创建系统设置路由
-export function createSystemSettingsRoutes(withAuth: WithAuth) {
-  const settingsRoutes = new Hono<{ Variables: Variables }>();
-
-  // 获取所有系统设置(按分组)
-  settingsRoutes.get('/', withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const settings = await apiClient.database
-        .table('system_settings')
-        .select('*');
-
-      // 按分组整理数据
-      const groupedSettings = settings.reduce((acc: SystemSettingGroupData[], setting) => {
-        const groupIndex = acc.findIndex((g: SystemSettingGroupData) => g.name === setting.group);
-        if (groupIndex === -1) {
-          acc.push({
-            name: setting.group,
-            description: `${setting.group}组设置`,
-            settings: [{
-              id: setting.id,
-              key: setting.key,
-              value: setting.value,
-              description: setting.description,
-              group: setting.group
-            }]
-          });
-        } else {
-          acc[groupIndex].settings.push({
-            id: setting.id,
-            key: setting.key,
-            value: setting.value,
-            description: setting.description,
-            group: setting.group
-          });
-        }
-        return acc;
-      }, []);
-
-      return c.json({
-        message: '获取系统设置成功',
-        data: groupedSettings
-      });
-    } catch (error) {
-      log.api('获取系统设置失败:', error);
-      return c.json({ error: '获取系统设置失败' }, 500);
-    }
-  });
-
-  // 获取指定分组的系统设置
-  settingsRoutes.get('/group/:group', withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const group = c.req.param('group');
-      
-      const settings = await apiClient.database
-        .table('system_settings')
-        .where('group', group)
-        .select('*');
-
-      return c.json({
-        message: '获取分组设置成功',
-        data: settings
-      });
-    } catch (error) {
-      log.api('获取分组设置失败:', error);
-      return c.json({ error: '获取分组设置失败' }, 500);
-    }
-  });
-
-  // 更新系统设置
-  settingsRoutes.put('/:key', withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const key = c.req.param('key');
-      const settingData = await c.req.json();
-
-      // 验证设置是否存在
-      const existingSetting = await apiClient.database
-        .table('system_settings')
-        .where('key', key)
-        .first();
-
-      if (!existingSetting) {
-        return c.json({ error: '设置项不存在' }, 404);
-      }
-
-      // 更新设置
-      await apiClient.database
-        .table('system_settings')
-        .where('key', key)
-        .update({
-          value: settingData.value,
-          updated_at: apiClient.database.fn.now()
-        });
-
-      // 获取更新后的设置
-      const updatedSetting = await apiClient.database
-        .table('system_settings')
-        .where('key', key)
-        .first();
-
-      return c.json({
-        message: '系统设置已更新',
-        data: updatedSetting
-      });
-    } catch (error) {
-      log.api('更新系统设置失败:', error);
-      return c.json({ error: '更新系统设置失败' }, 500);
-    }
-  });
-
-  // 批量更新系统设置
-  settingsRoutes.put('/', withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-      const settingsData = await c.req.json();
-
-      // 验证数据格式
-      if (!Array.isArray(settingsData)) {
-        return c.json({ error: '无效的请求数据格式,应为数组' }, 400);
-      }
-
-      const trxProvider = apiClient.database.transactionProvider();
-      const trx = await trxProvider();
-
-      for (const setting of settingsData) {
-        if (!setting.key) continue;
-
-        // 验证设置是否存在
-        const existingSetting = await trx.table('system_settings')
-          .where('key', setting.key)
-          .first();
-
-        if (!existingSetting) {
-          throw new Error(`设置项 ${setting.key} 不存在`);
-        }
-
-        // 更新设置
-        await trx.table('system_settings')
-          .where('key', setting.key)
-          .update({
-            value: setting.value,
-            updated_at: trx.fn.now()
-          });
-      }
-
-      await trx.commit();
-
-      // 获取所有更新后的设置
-      const updatedSettings = await apiClient.database
-        .table('system_settings')
-        .whereIn('key', settingsData.map(s => s.key))
-        .select('*');
-
-      return c.json({
-        message: '系统设置已批量更新',
-        data: updatedSettings
-      });
-    } catch (error) {
-      log.api('批量更新系统设置失败:', error);
-      return c.json({ error: '批量更新系统设置失败' }, 500);
-    }
-  });
-
-  // 重置系统设置
-  settingsRoutes.post('/reset', withAuth, async (c) => {
-    try {
-      const apiClient = c.get('apiClient');
-
-      // 重置为迁移文件中定义的初始值
-      const trxProvider = apiClient.database.transactionProvider();
-      const trx = await trxProvider();
-
-      // 清空现有设置
-      await trx.table('system_settings').delete();
-      
-      // 插入默认设置
-      await trx.table('system_settings').insert([
-        // 基础设置组
-        {
-          key: 'SITE_NAME',
-          value: '应用管理系统',
-          description: '站点名称',
-          group: 'basic',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'SITE_DESCRIPTION',
-          value: '一个强大的应用管理系统',
-          description: '站点描述',
-          group: 'basic',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'SITE_KEYWORDS',
-          value: '应用管理,系统管理,后台管理',
-          description: '站点关键词',
-          group: 'basic',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        // 功能设置组
-        {
-          key: 'ENABLE_REGISTER',
-          value: 'true',
-          description: '是否开启注册',
-          group: 'feature',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'ENABLE_CAPTCHA',
-          value: 'true',
-          description: '是否开启验证码',
-          group: 'feature',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'LOGIN_ATTEMPTS',
-          value: '5',
-          description: '登录尝试次数',
-          group: 'feature',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        // 上传设置组
-        {
-          key: 'UPLOAD_MAX_SIZE',
-          value: '10',
-          description: '最大上传大小(MB)',
-          group: 'upload',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'ALLOWED_FILE_TYPES',
-          value: 'jpg,jpeg,png,gif,doc,docx,xls,xlsx,pdf',
-          description: '允许的文件类型',
-          group: 'upload',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        // 通知设置组
-        {
-          key: 'NOTIFY_ON_LOGIN',
-          value: 'true',
-          description: '登录通知',
-          group: 'notify',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        },
-        {
-          key: 'NOTIFY_ON_ERROR',
-          value: 'true',
-          description: '错误通知',
-          group: 'notify',
-          created_at: apiClient.database.fn.now(),
-          updated_at: apiClient.database.fn.now()
-        }
-      ]);
-
-      await trx.commit();
-
-      const resetSettings = await apiClient.database
-        .table('system_settings')
-        .select('*');
-
-      return c.json({
-        message: '系统设置已重置',
-        data: resetSettings
-      });
-    } catch (error) {
-      log.api('重置系统设置失败:', error);
-      return c.json({ error: '重置系统设置失败' }, 500);
-    }
-  });
-
-  return settingsRoutes;
-}

+ 296 - 0
server/routes_system_settings.ts

@@ -0,0 +1,296 @@
+import { Hono } from "hono";
+import debug from "debug";
+import type {
+  SystemSetting,
+  SystemSettingGroupData,
+} from "../client/share/types.ts";
+
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+const log = {
+  api: debug("api:sys"),
+};
+
+// 创建系统设置路由
+export function createSystemSettingsRoutes(withAuth: WithAuth) {
+  const settingsRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取所有系统设置(按分组)
+  settingsRoutes.get('/', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const settings = await apiClient.database
+        .table('system_settings')
+        .select('*');
+
+      // 按分组整理数据
+      const groupedSettings = settings.reduce((acc: SystemSettingGroupData[], setting) => {
+        const groupIndex = acc.findIndex((g: SystemSettingGroupData) => g.name === setting.group);
+        if (groupIndex === -1) {
+          acc.push({
+            name: setting.group,
+            description: `${setting.group}组设置`,
+            settings: [{
+              id: setting.id,
+              key: setting.key,
+              value: setting.value,
+              description: setting.description,
+              group: setting.group
+            }]
+          });
+        } else {
+          acc[groupIndex].settings.push({
+            id: setting.id,
+            key: setting.key,
+            value: setting.value,
+            description: setting.description,
+            group: setting.group
+          });
+        }
+        return acc;
+      }, []);
+
+      return c.json({
+        message: '获取系统设置成功',
+        data: groupedSettings
+      });
+    } catch (error) {
+      log.api('获取系统设置失败:', error);
+      return c.json({ error: '获取系统设置失败' }, 500);
+    }
+  });
+
+  // 获取指定分组的系统设置
+  settingsRoutes.get('/group/:group', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const group = c.req.param('group');
+      
+      const settings = await apiClient.database
+        .table('system_settings')
+        .where('group', group)
+        .select('*');
+
+      return c.json({
+        message: '获取分组设置成功',
+        data: settings
+      });
+    } catch (error) {
+      log.api('获取分组设置失败:', error);
+      return c.json({ error: '获取分组设置失败' }, 500);
+    }
+  });
+
+  // 更新系统设置
+  settingsRoutes.put('/:key', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const key = c.req.param('key');
+      const settingData = await c.req.json();
+
+      // 验证设置是否存在
+      const existingSetting = await apiClient.database
+        .table('system_settings')
+        .where('key', key)
+        .first();
+
+      if (!existingSetting) {
+        return c.json({ error: '设置项不存在' }, 404);
+      }
+
+      // 更新设置
+      await apiClient.database
+        .table('system_settings')
+        .where('key', key)
+        .update({
+          value: settingData.value,
+          updated_at: apiClient.database.fn.now()
+        });
+
+      // 获取更新后的设置
+      const updatedSetting = await apiClient.database
+        .table('system_settings')
+        .where('key', key)
+        .first();
+
+      return c.json({
+        message: '系统设置已更新',
+        data: updatedSetting
+      });
+    } catch (error) {
+      log.api('更新系统设置失败:', error);
+      return c.json({ error: '更新系统设置失败' }, 500);
+    }
+  });
+
+  // 批量更新系统设置
+  settingsRoutes.put('/', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const settingsData = await c.req.json();
+
+      // 验证数据格式
+      if (!Array.isArray(settingsData)) {
+        return c.json({ error: '无效的请求数据格式,应为数组' }, 400);
+      }
+
+      const trxProvider = apiClient.database.transactionProvider();
+      const trx = await trxProvider();
+
+      for (const setting of settingsData) {
+        if (!setting.key) continue;
+
+        // 验证设置是否存在
+        const existingSetting = await trx.table('system_settings')
+          .where('key', setting.key)
+          .first();
+
+        if (!existingSetting) {
+          throw new Error(`设置项 ${setting.key} 不存在`);
+        }
+
+        // 更新设置
+        await trx.table('system_settings')
+          .where('key', setting.key)
+          .update({
+            value: setting.value,
+            updated_at: trx.fn.now()
+          });
+      }
+
+      await trx.commit();
+
+      // 获取所有更新后的设置
+      const updatedSettings = await apiClient.database
+        .table('system_settings')
+        .whereIn('key', settingsData.map(s => s.key))
+        .select('*');
+
+      return c.json({
+        message: '系统设置已批量更新',
+        data: updatedSettings
+      });
+    } catch (error) {
+      log.api('批量更新系统设置失败:', error);
+      return c.json({ error: '批量更新系统设置失败' }, 500);
+    }
+  });
+
+  // 重置系统设置
+  settingsRoutes.post('/reset', withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+
+      // 重置为迁移文件中定义的初始值
+      const trxProvider = apiClient.database.transactionProvider();
+      const trx = await trxProvider();
+
+      // 清空现有设置
+      await trx.table('system_settings').delete();
+      
+      // 插入默认设置
+      await trx.table('system_settings').insert([
+        // 基础设置组
+        {
+          key: 'SITE_NAME',
+          value: '应用管理系统',
+          description: '站点名称',
+          group: 'basic',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'SITE_DESCRIPTION',
+          value: '一个强大的应用管理系统',
+          description: '站点描述',
+          group: 'basic',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'SITE_KEYWORDS',
+          value: '应用管理,系统管理,后台管理',
+          description: '站点关键词',
+          group: 'basic',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        // 功能设置组
+        {
+          key: 'ENABLE_REGISTER',
+          value: 'true',
+          description: '是否开启注册',
+          group: 'feature',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'ENABLE_CAPTCHA',
+          value: 'true',
+          description: '是否开启验证码',
+          group: 'feature',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'LOGIN_ATTEMPTS',
+          value: '5',
+          description: '登录尝试次数',
+          group: 'feature',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        // 上传设置组
+        {
+          key: 'UPLOAD_MAX_SIZE',
+          value: '10',
+          description: '最大上传大小(MB)',
+          group: 'upload',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'ALLOWED_FILE_TYPES',
+          value: 'jpg,jpeg,png,gif,doc,docx,xls,xlsx,pdf',
+          description: '允许的文件类型',
+          group: 'upload',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        // 通知设置组
+        {
+          key: 'NOTIFY_ON_LOGIN',
+          value: 'true',
+          description: '登录通知',
+          group: 'notify',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        },
+        {
+          key: 'NOTIFY_ON_ERROR',
+          value: 'true',
+          description: '错误通知',
+          group: 'notify',
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        }
+      ]);
+
+      await trx.commit();
+
+      const resetSettings = await apiClient.database
+        .table('system_settings')
+        .select('*');
+
+      return c.json({
+        message: '系统设置已重置',
+        data: resetSettings
+      });
+    } catch (error) {
+      log.api('重置系统设置失败:', error);
+      return c.json({ error: '重置系统设置失败' }, 500);
+    }
+  });
+
+  return settingsRoutes;
+}

+ 186 - 0
server/routes_theme.ts

@@ -0,0 +1,186 @@
+import { Hono } from "hono";
+import debug from "debug";
+import type {
+  ThemeSettings,
+} from "../client/share/types.ts";
+
+import {
+  ThemeMode,
+  FontSize,
+  CompactMode,
+} from "../client/share/types.ts";
+
+import type { Variables, WithAuth } from "./middlewares.ts";
+
+const log = {
+  api: debug("api:sys"),
+};
+
+// 创建主题设置路由
+export function createThemeRoutes(withAuth: WithAuth) {
+  const themeRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取当前主题设置
+  themeRoutes.get("/", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const user = c.get('user');
+
+      if (!user) {
+        return c.json({ error: "未授权访问" }, 401);
+      }
+
+      // 获取用户的主题设置
+      let themeSettings = await apiClient.database
+        .table("theme_settings")
+        .where("user_id", user.id)
+        .first();
+
+      // 如果用户没有主题设置,则创建默认设置
+      if (!themeSettings) {
+        const defaultSettings = {
+          theme_mode: ThemeMode.LIGHT,
+          primary_color: '#1890ff',
+          font_size: FontSize.MEDIUM,
+          is_compact: CompactMode.NORMAL
+        };
+
+        const [id] = await apiClient.database.table("theme_settings").insert({
+          user_id: user.id,
+          settings: defaultSettings,
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now(),
+        });
+
+        themeSettings = await apiClient.database
+          .table("theme_settings")
+          .where("id", id)
+          .first();
+      }
+
+      return c.json({
+        message: "获取主题设置成功",
+        data: themeSettings?.settings,
+      });
+    } catch (error) {
+      log.api("获取主题设置失败:", error);
+      return c.json({ error: "获取主题设置失败" }, 500);
+    }
+  });
+
+  // 更新主题设置
+  themeRoutes.put("/", withAuth, async (c) => {
+    try {
+      const user = c.get('user');
+      const apiClient = c.get('apiClient');
+
+      if (!user) {
+        return c.json({ error: "未授权访问" }, 401);
+      }
+
+      const themeData = (await c.req.json()) as Partial<ThemeSettings>;
+
+      // 检查用户是否已有主题设置
+      const existingTheme = await apiClient.database
+        .table("theme_settings")
+        .where("user_id", user.id)
+        .first();
+
+      if (existingTheme) {
+        // 更新现有设置
+        const currentSettings = existingTheme.settings || {};
+        const updatedSettings = {
+          ...currentSettings,
+          ...themeData
+        };
+
+        await apiClient.database
+          .table("theme_settings")
+          .where("user_id", user.id)
+          .update({
+            settings: JSON.stringify(updatedSettings),
+            updated_at: apiClient.database.fn.now(),
+          });
+      } else {
+        // 创建新设置
+        const defaultSettings = {
+          theme_mode: ThemeMode.LIGHT,
+          primary_color: '#1890ff',
+          font_size: FontSize.MEDIUM,
+          is_compact: CompactMode.NORMAL
+        };
+
+        const updatedSettings = {
+          ...defaultSettings,
+          ...themeData
+        };
+
+        await apiClient.database.table("theme_settings").insert({
+          user_id: user.id,
+          settings: updatedSettings,
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now(),
+        });
+      }
+
+      // 获取更新后的主题设置
+      const updatedTheme = await apiClient.database
+        .table("theme_settings")
+        .where("user_id", user.id)
+        .first();
+
+      return c.json({
+        message: "主题设置更新成功",
+        data: updatedTheme,
+      });
+    } catch (error) {
+      log.api("更新主题设置失败:", error);
+      return c.json({ error: "更新主题设置失败" }, 500);
+    }
+  });
+
+  // 重置主题设置为默认值
+  themeRoutes.post("/reset", withAuth, async (c) => {
+    try {
+      const user = c.get('user');
+      const apiClient = c.get('apiClient');
+
+      if (!user) {
+        return c.json({ error: "未授权访问" }, 401);
+      }
+
+      // 默认主题设置
+      const defaultSettings = {
+        theme_mode: ThemeMode.LIGHT,
+        primary_color: '#1890ff',
+        font_size: FontSize.MEDIUM,
+        is_compact: CompactMode.NORMAL
+      };
+
+      // 更新用户的主题设置
+      await apiClient.database
+        .table("theme_settings")
+        .where("user_id", user.id)
+        .update({
+          settings: JSON.stringify(defaultSettings),
+          updated_at: apiClient.database.fn.now(),
+        });
+
+      // 获取更新后的主题设置
+      const updatedTheme = await apiClient.database
+        .table("theme_settings")
+        .where("user_id", user.id)
+        .first();
+
+      return c.json({
+        message: "主题设置已重置为默认值",
+        data: updatedTheme,
+      });
+    } catch (error) {
+      log.api("重置主题设置失败:", error);
+      return c.json({ error: "重置主题设置失败" }, 500);
+    }
+  });
+
+  return themeRoutes;
+}

+ 1 - 2
server/routes_users.ts

@@ -1,6 +1,5 @@
 import { Hono } from 'hono'
-import type { Variables } from './app.tsx'
-import type { WithAuth } from './app.tsx'
+import type { Variables, WithAuth } from "./middlewares.ts";
 
 export function createUserRoutes(withAuth: WithAuth) {
   const usersRoutes = new Hono<{ Variables: Variables }>()