diff --git a/client/mobile/api.ts b/client/mobile/api.ts index 9324ae4..b816a89 100644 --- a/client/mobile/api.ts +++ b/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 => { + 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 => { + 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 => { + try { + const response = await axios.get(`${API_BASE_URL}/home/notices`, { params }); + return response.data; + } catch (error) { + throw error; + } + } +}; // 地图相关API的接口类型定义 export interface LoginLocationResponse { diff --git a/client/mobile/pages_index.tsx b/client/mobile/pages_index.tsx index c81bb2a..d24d316 100644 --- a/client/mobile/pages_index.tsx +++ b/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([]); - const [news, setNews] = useState([]); - const [notices, setNotices] = useState([]); + const [banners, setBanners] = useState([]); + const [news, setNews] = useState([]); + const [notices, setNotices] = useState([]); 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 = () => {
- {notices.some(notice => !notice.is_read) && ( + {notices.some(notice => notice.user_status === MessageStatus.UNREAD) && ( )}
@@ -173,10 +117,10 @@ const HomePage: React.FC = () => {
handleBannerClick(banner.link)} + onClick={() => handleBannerClick(banner.content || '')} > - {banner.title} @@ -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 && ( - {item.title} )} -
+

{item.title}

-

{item.summary}

+

+ {item.content?.substring(0, 100)}... +

{item.category} - {formatRelativeTime(item.publish_date)} + {formatRelativeTime(item.created_at)}
@@ -288,8 +234,8 @@ const HomePage: React.FC = () => { onClick={() => handleNoticeClick(item.id)} >
-

- {!item.is_read && ( +

+ {item.user_status === MessageStatus.UNREAD && ( )} {item.title} diff --git a/client/share/types.ts b/client/share/types.ts index 568deca..3a4553a 100644 --- a/client/share/types.ts +++ b/client/share/types.ts @@ -378,35 +378,38 @@ export interface KnowInfo { /** 主键ID */ id: number; - /** 文章的标题 */ - title?: string; + /** 标题 */ + title: string; - /** 文章的标签 */ - tags?: 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; } // 登录位置详细信息 diff --git a/server/app.tsx b/server/app.tsx index f8e7645..cd4b09e 100644 --- a/server/app.tsx +++ b/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) diff --git a/server/migrations.ts b/server/migrations.ts index 3187732..a9e7a72 100644 --- a/server/migrations.ts +++ b/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'); }); }, diff --git a/server/routes_home.ts b/server/routes_home.ts new file mode 100644 index 0000000..bc37027 --- /dev/null +++ b/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 +} \ No newline at end of file