Browse Source

新增首页API相关类型定义及实现,整合轮播图、新闻和通知数据的获取逻辑,优化数据结构以提升用户体验和代码可维护性。

zyh 8 tháng trước cách đây
mục cha
commit
07924bb142
6 tập tin đã thay đổi với 266 bổ sung123 xóa
  1. 74 1
      client/mobile/api.ts
  2. 55 109
      client/mobile/pages_index.tsx
  3. 16 13
      client/share/types.ts
  4. 2 0
      server/app.tsx
  5. 2 0
      server/migrations.ts
  6. 117 0
      server/routes_home.ts

+ 74 - 1
client/mobile/api.ts

@@ -5,7 +5,8 @@ import type {
   User, FileLibrary, FileCategory, ThemeSettings,
   SystemSetting, SystemSettingGroupData, 
   LoginLocation, LoginLocationDetail,
-  Message, MessageType, MessageStatus, UserMessage
+  MessageType, MessageStatus, UserMessage,
+  KnowInfo
 } from '../share/types.ts';
 
 
@@ -497,6 +498,78 @@ export const ChartAPI = {
     }
   }
 };
+// 首页API相关类型定义
+interface HomeBannersResponse {
+  message: string;
+  data: KnowInfo[];
+}
+
+interface HomeNewsResponse {
+  message: string;
+  data: KnowInfo[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface HomeNoticesResponse {
+  message: string;
+    data: {
+      id: number;
+      title: string;
+      content: string;
+      created_at: string;
+    }[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+// 首页API
+export const HomeAPI = {
+  // 获取轮播图
+  getBanners: async (): Promise<HomeBannersResponse> => {
+    try {
+      const response = await axios.get(`${API_BASE_URL}/home/banners`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取新闻列表
+  getNews: async (params?: {
+    page?: number,
+    pageSize?: number,
+    category?: string
+  }): Promise<HomeNewsResponse> => {
+    try {
+      const response = await axios.get(`${API_BASE_URL}/home/news`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取通知列表
+  getNotices: async (params?: {
+    page?: number,
+    pageSize?: number
+  }): Promise<HomeNoticesResponse> => {
+    try {
+      const response = await axios.get(`${API_BASE_URL}/home/notices`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};
 
 // 地图相关API的接口类型定义
 export interface LoginLocationResponse {

+ 55 - 109
client/mobile/pages_index.tsx

@@ -1,37 +1,16 @@
 import React, { useState, useEffect } from 'react';
 import { useNavigate, useLocation } from 'react-router';
-import { 
-  HomeIcon, 
-  UserIcon, 
-  NewspaperIcon, 
-  BellIcon 
+import { HomeAPI } from './api.ts';
+import { MessageAPI } from './api.ts';
+import {
+  HomeIcon,
+  UserIcon,
+  NewspaperIcon,
+  BellIcon
 } from '@heroicons/react/24/outline';
 import { useAuth } from './hooks.tsx';
 import { formatRelativeTime } from './utils.ts';
-
-interface BannerItem {
-  id: number;
-  title: string;
-  image: string;
-  link: string;
-}
-
-interface NewsItem {
-  id: number;
-  title: string;
-  summary: string;
-  publish_date: string;
-  cover?: string;
-  category: string;
-}
-
-interface NoticeItem {
-  id: number;
-  title: string;
-  content: string;
-  created_at: string;
-  is_read: boolean;
-}
+import { KnowInfo, UserMessage, MessageType, MessageStatus } from '../share/types.ts';
 
 // 首页组件
 const HomePage: React.FC = () => {
@@ -39,78 +18,43 @@ const HomePage: React.FC = () => {
   const navigate = useNavigate();
   const location = useLocation();
   const [loading, setLoading] = useState(true);
-  const [banners, setBanners] = useState<BannerItem[]>([]);
-  const [news, setNews] = useState<NewsItem[]>([]);
-  const [notices, setNotices] = useState<NoticeItem[]>([]);
+  const [banners, setBanners] = useState<KnowInfo[]>([]);
+  const [news, setNews] = useState<KnowInfo[]>([]);
+  const [notices, setNotices] = useState<UserMessage[]>([]);
   const [activeTab, setActiveTab] = useState('news');
   
   // 模拟加载数据
   useEffect(() => {
-    // 模拟API请求
-    setTimeout(() => {
-      // 模拟轮播图数据
-      setBanners([
-        { 
-          id: 1, 
-          title: '欢迎使用移动端应用', 
-          image: 'https://images.unsplash.com/photo-1518655048521-f130df041f66?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80',
-          link: '/welcome'
-        },
-        { 
-          id: 2, 
-          title: '新功能上线了', 
-          image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80',
-          link: '/new-features'
-        }
-      ]);
-      
-      // 模拟新闻数据
-      setNews([
-        {
-          id: 1,
-          title: '用户体验升级,新版本发布',
-          summary: '我们很高兴地宣布,新版本已经发布,带来了更好的用户体验和更多新功能。',
-          publish_date: '2023-05-01T08:30:00',
-          cover: 'https://images.unsplash.com/photo-1496171367470-9ed9a91ea931?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHRlY2h8ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60',
-          category: '产品更新'
-        },
-        {
-          id: 2,
-          title: '新的数据分析功能上线',
-          summary: '新的数据分析功能让您更深入地了解您的业务数据,提供更好的决策支持。',
-          publish_date: '2023-04-25T14:15:00',
-          cover: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNoYXJ0fGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60',
-          category: '功能介绍'
-        },
-        {
-          id: 3,
-          title: '如何提高工作效率的5个小技巧',
-          summary: '这篇文章分享了5个可以立即实施的小技巧,帮助您提高日常工作效率。',
-          publish_date: '2023-04-20T09:45:00',
-          category: '使用技巧'
-        }
-      ]);
-      
-      // 模拟通知数据
-      setNotices([
-        {
-          id: 1,
-          title: '系统维护通知',
-          content: '我们将于本周六凌晨2点至4点进行系统维护,期间系统可能会出现短暂不可用。',
-          created_at: '2023-05-02T10:00:00',
-          is_read: false
-        },
-        {
-          id: 2,
-          title: '您的账户信息已更新',
-          content: '您的账户信息已成功更新,如非本人操作,请及时联系客服。',
-          created_at: '2023-05-01T16:30:00',
-          is_read: true
-        }
-      ]);
-      
-      setLoading(false);
-    }, 800);
+    const fetchData = async () => {
+      try {
+        // 获取数据
+        const [bannersRes, newsRes, messagesRes] = await Promise.all([
+          HomeAPI.getBanners(),
+          HomeAPI.getNews(),
+          MessageAPI.getMessages({ type: MessageType.ANNOUNCE })
+        ]);
+        
+        setBanners(bannersRes.data.map((item: KnowInfo) => ({
+          id: item.id,
+          title: item.title,
+          cover_url: item.cover_url,
+          content: item.content,
+          category: 'banner',
+          created_at: new Date().toISOString(),
+          updated_at: new Date().toISOString(),
+          sort_order: item.sort_order || 0
+        } as KnowInfo)));
+        setNews(newsRes.data);
+        setNotices(messagesRes.data);
+        
+        setLoading(false);
+      } catch (error) {
+        console.error('获取首页数据失败:', error);
+        setLoading(false);
+      }
+    };
+    
+    fetchData();
   }, []);
   
   // 处理轮播图点击
@@ -157,7 +101,7 @@ const HomePage: React.FC = () => {
           
           <div className="relative">
             <BellIcon className="w-6 h-6" />
-            {notices.some(notice => !notice.is_read) && (
+            {notices.some(notice => notice.user_status === MessageStatus.UNREAD) && (
               <span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
             )}
           </div>
@@ -173,10 +117,10 @@ const HomePage: React.FC = () => {
               <div 
                 key={banner.id}
                 className="w-full h-40 flex-shrink-0 relative"
-                onClick={() => handleBannerClick(banner.link)}
+                onClick={() => handleBannerClick(banner.content || '')}
               >
-                <img 
-                  src={banner.image} 
+                <img
+                  src={banner.cover_url || ''}
                   alt={banner.title}
                   className="w-full h-full object-cover"
                 />
@@ -251,22 +195,24 @@ const HomePage: React.FC = () => {
                     className="bg-white p-3 rounded-lg shadow flex items-start space-x-3"
                     onClick={() => handleNewsClick(item.id)}
                   >
-                    {item.cover && (
-                      <img 
-                        src={item.cover} 
+                    {item.cover_url && (
+                      <img
+                        src={item.cover_url}
                         alt={item.title}
                         className="w-20 h-20 object-cover rounded-md flex-shrink-0"
                       />
                     )}
-                    <div className={item.cover ? '' : 'w-full'}>
+                    <div className={item.cover_url ? '' : 'w-full'}>
                       <h3 className="font-medium text-gray-900 line-clamp-2">{item.title}</h3>
-                      <p className="text-sm text-gray-500 mt-1 line-clamp-2">{item.summary}</p>
+                      <p className="text-sm text-gray-500 mt-1 line-clamp-2">
+                        {item.content?.substring(0, 100)}...
+                      </p>
                       <div className="flex justify-between items-center mt-2">
                         <span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
                           {item.category}
                         </span>
                         <span className="text-xs text-gray-400">
-                          {formatRelativeTime(item.publish_date)}
+                          {formatRelativeTime(item.created_at)}
                         </span>
                       </div>
                     </div>
@@ -288,8 +234,8 @@ const HomePage: React.FC = () => {
                     onClick={() => handleNoticeClick(item.id)}
                   >
                     <div className="flex justify-between items-start">
-                      <h3 className={`font-medium ${item.is_read ? 'text-gray-700' : 'text-blue-600'}`}>
-                        {!item.is_read && (
+                      <h3 className={`font-medium ${item.user_status === MessageStatus.READ ? 'text-gray-700' : 'text-blue-600'}`}>
+                        {item.user_status === MessageStatus.UNREAD && (
                           <span className="inline-block w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
                         )}
                         {item.title}

+ 16 - 13
client/share/types.ts

@@ -378,35 +378,38 @@ export interface KnowInfo {
   /** 主键ID */
   id: number;
   
-  /** 文章的标题 */
-  title?: string;
-  
-  /** 文章的标签 */
-  tags?: string;
+  /** 标题 */
+  title: string;
   
-  /** 文章的内容 */
+  /** 内容 */
   content?: string;
   
-  /** 文章的作者 */
+  /** 作者 */
   author?: string;
   
-  /** 文章的分类 */
-  category?: string;
+  /** 分类 */
+  category: string;
   
-  /** 文章的封面图片URL */
+  /** 标签 */
+  tags?: string;
+  
+  /** 封面图片URL */
   cover_url?: string;
   
   /** 审核状态 */
   audit_status?: number;
   
-  /** 是否被删除 (0否 1是) */
+  /** 排序权重 */
+  sort_order?: number;
+  
+  /** 是否删除 (0否 1是) */
   is_deleted?: number;
   
   /** 创建时间 */
-  created_at: Date;
+  created_at: string;
   
   /** 更新时间 */
-  updated_at: Date;
+  updated_at: string;
 }
 
 // 登录位置详细信息

+ 2 - 0
server/app.tsx

@@ -35,6 +35,7 @@ import { createAuthRoutes } from "./routes_auth.ts";
 import { createUserRoutes } from "./routes_users.ts";
 import { createMessagesRoutes } from "./routes_messages.ts";
 import { createMigrationsRoutes } from "./routes_migrations.ts";
+import { createHomeRoutes } from "./routes_home.ts";
 dayjs.extend(utc)
 // 初始化debug实例
 const log = {
@@ -305,6 +306,7 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) {
   api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
   api.route('/messages', createMessagesRoutes(withAuth)) // 添加消息路由
   api.route('/migrations', createMigrationsRoutes(withAuth)) // 添加数据库迁移路由
+  api.route('/home', createHomeRoutes(withAuth)) // 添加首页路由
   
   // 注册API路由
   honoApp.route('/api', api)

+ 2 - 0
server/migrations.ts

@@ -73,6 +73,7 @@ const createKnowInfoTable: MigrationLiveDefinition = {
       table.string('category').comment('分类');
       table.string('cover_url').comment('封面图片URL');
       table.integer('audit_status').defaultTo(AuditStatus.PENDING).comment('审核状态');
+      table.integer('sort_order').defaultTo(0).comment('排序权重');
       table.integer('is_deleted').defaultTo(0).comment('是否被删除 (0否 1是)');
       table.timestamps(true, true);
       
@@ -82,6 +83,7 @@ const createKnowInfoTable: MigrationLiveDefinition = {
       table.index('author');
       table.index('category');
       table.index('audit_status');
+      table.index('sort_order');
       table.index('is_deleted');
     });
   },

+ 117 - 0
server/routes_home.ts

@@ -0,0 +1,117 @@
+import { Hono } from 'hono'
+import type { Variables } from './app.tsx'
+import type { WithAuth } from './app.tsx'
+import { AuditStatus } from '../client/share/types.ts'
+
+export function createHomeRoutes(withAuth: WithAuth) {
+  const homeRoutes = new Hono<{ Variables: Variables }>()
+
+  // 获取轮播图数据
+  homeRoutes.get('/banners', async (c) => {
+    try {
+      const apiClient = c.get('apiClient')
+      
+      const banners = await apiClient.database.table('know_info')
+        .where('is_deleted', 0)
+        .where('audit_status', AuditStatus.APPROVED) // 使用审核状态替代启用状态
+        .where('category', 'banner') // 轮播图类型
+        .orderBy('created_at', 'asc') // 使用创建时间排序
+        .select('id', 'title', 'cover_url as image', 'content as link')
+
+      return c.json({
+        message: '获取轮播图成功',
+        data: banners
+      })
+    } catch (error) {
+      console.error('获取轮播图失败:', error)
+      return c.json({ error: '获取轮播图失败' }, 500)
+    }
+  })
+
+  // 获取新闻列表
+  homeRoutes.get('/news', 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 category = c.req.query('category')
+
+      const query = apiClient.database.table('know_info')
+        .where('is_deleted', 0)
+        .where('audit_status', AuditStatus.APPROVED) // 使用审核状态替代发布状态
+        .where('category', 'news') // 新闻类型
+        .orderBy('created_at', 'desc') // 使用创建时间替代发布时间
+        .limit(pageSize)
+        .offset((page - 1) * pageSize)
+
+      if (category) query.where('sub_category', category)
+
+      const countQuery = query.clone()
+      const news = await query
+      
+      // 获取总数用于分页
+      const total = await countQuery.count()
+      const totalCount = Number(total)
+      const totalPages = Math.ceil(totalCount / pageSize)
+
+      return c.json({
+        message: '获取新闻成功',
+        data: news,
+        pagination: {
+          total: totalCount,
+          current: page,
+          pageSize,
+          totalPages
+        }
+      })
+    } catch (error) {
+      console.error('获取新闻失败:', error)
+      return c.json({ error: '获取新闻失败' }, 500)
+    }
+  })
+
+  // 获取通知列表
+  homeRoutes.get('/notices', 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 notices = await apiClient.database.table('know_info')
+        .where('is_deleted', 0)
+        .where('status', 1) // 1表示已发布
+        .where('category', 'notice') // 通知类型
+        .orderBy('created_at', 'desc')
+        .limit(pageSize)
+        .offset((page - 1) * pageSize)
+        .select('id', 'title', 'content', 'created_at')
+
+      const total = await apiClient.database.table('know_info')
+        .where('is_deleted', 0)
+        .where('status', 1)
+        .where('category', 'notice')
+        .count()
+      
+      const totalCount = Number(total)
+      const totalPages = Math.ceil(totalCount / pageSize)
+
+      return c.json({
+        message: '获取通知成功',
+        data: notices,
+        pagination: {
+          total: totalCount,
+          current: page,
+          pageSize,
+          totalPages
+        }
+      })
+    } catch (error) {
+      console.error('获取通知失败:', error)
+      return c.json({ error: '获取通知失败' }, 500)
+    }
+  })
+
+  return homeRoutes
+}