新增首页API相关类型定义及实现,整合轮播图、新闻和通知数据的获取逻辑,优化数据结构以提升用户体验和代码可维护性。
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 登录位置详细信息
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
server/routes_home.ts
Normal file
117
server/routes_home.ts
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user