first commit

This commit is contained in:
ltProj
2025-09-24 22:57:54 +08:00
commit a732a74e05
24 changed files with 23370 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
registry=https://registry.npmjs.com/

44
.umirc.ts Normal file
View File

@@ -0,0 +1,44 @@
import { defineConfig } from "umi";
export default defineConfig({
routes: [
{
path: "/",
component: "index",
name: "首页"
},
{
path: "/topic/:id",
component: "topic/detail",
name: "话题详情"
},
{
path: "/publish",
component: "topic/publish",
name: "发布话题"
},
],
npmClient: 'npm',
plugins: [
'@umijs/plugins/dist/antd',
'@umijs/plugins/dist/locale',
'@umijs/plugins/dist/layout',
],
antd: {},
theme: {
colorPrimary: '#6f8cff',
colorPrimaryActive: '#6f8cff',
colorPrimaryHover: '#a7bfff',
},
locale: {
default: 'zh-CN',
antd: true,
},
layout: false,
title: 'AutoBeeAgent',
favicons: ['/favicon.ico'],
metas: [
{ name: 'keywords', content: 'AutoBeeAgent,热门话题,讨论,交流' },
{ name: 'description', content: 'AutoBeeAgent用户热门话题讨论平台' },
],
});

20422
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"private": true,
"author": "ltProj <15648664+ltproj@user.noreply.gitee.com>",
"scripts": {
"dev": "umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^6.0.2",
"@umijs/plugins": "^4.5.0",
"antd": "^5.27.4",
"dayjs": "^1.11.18",
"umi": "^4.5.0"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"typescript": "^5.0.3"
}
}

BIN
src/assets/yay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,138 @@
.hotTopicsCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
padding: 16px 20px;
.ant-card-head-title {
padding: 0;
}
}
.ant-card-body {
padding: 0;
}
}
.cardTitle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.fireIcon {
color: #ff4d4f;
margin-right: 8px;
font-size: 16px;
}
.viewAllButton {
cursor: pointer;
padding: 0;
height: auto;
color: #6f8cff;
font-size: 12px;
&:hover {
color: #40a9ff;
}
}
}
.hotTopicsList {
.ant-list-item {
padding: 16px 20px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #f8f9fa;
}
&:last-child {
border-bottom: none;
}
}
}
.hotTopicItem {
display: flex;
align-items: center;
gap: 12px;
}
.topicRank {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
.rankIcon {
color: #ff4d4f;
font-size: 16px;
}
.rankNumber {
font-size: 14px;
font-weight: 600;
color: #666;
}
}
.topicContent {
flex: 1;
min-width: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.topicTitle {
display: block;
font-size: 14px;
line-height: 1.4;
color: #333;
font-weight: 500;
}
.topicMeta {
width: 45px;
display: flex;
align-items: center;
justify-content: space-between;
.heatScore {
font-size: 12px;
color: #999;
}
}
// 响应式设计
@media (max-width: 768px) {
.hotTopicsCard {
.ant-card-head {
padding: 12px 16px;
}
}
.hotTopicsList {
.ant-list-item {
padding: 12px 16px;
}
}
.topicTitle {
font-size: 13px;
}
.topicMeta {
.heatScore {
font-size: 11px;
}
}
}

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Card, List, Typography, Button } from 'antd';
import { FireOutlined, RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'umi';
import type { Topic } from '../../services/mockData';
import styles from './index.less';
const { Text } = Typography;
interface HotTopicsProps {
topics: Topic[];
loading?: boolean;
}
const HotTopics: React.FC<HotTopicsProps> = ({ topics, loading = false }) => {
const navigate = useNavigate();
const formatNumber = (num: number): string => {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}w+`;
}
return num.toString();
};
const getHeatScore = (topic: Topic): number => {
return topic.likes + topic.views + topic.comments;
};
const sortedTopics = [...topics].sort((a, b) => getHeatScore(b) - getHeatScore(a));
return (
<Card
className={styles.hotTopicsCard}
title={
<div className={styles.cardTitle}>
<div>
<FireOutlined className={styles.fireIcon} />
<span></span>
</div>
<div className={styles.viewAllButton}>
<span></span>
<span><RightOutlined /></span>
</div>
</div>
}
loading={loading}
>
<List
className={styles.hotTopicsList}
dataSource={sortedTopics}
renderItem={(topic, index) => (
<List.Item
className={styles.hotTopicItem}
onClick={() => navigate(`/topic/${topic.id}`)}
>
<div className={styles.topicRank}>
{index === 0 && <FireOutlined className={styles.rankIcon} />}
{index > 0 && <span className={styles.rankNumber}>{index + 1}</span>}
</div>
<div className={styles.topicContent}>
<Text
className={styles.topicTitle}
ellipsis={{ tooltip: topic.title }}
>
{topic.title}
</Text>
<div className={styles.topicMeta}>
<Text type="secondary" className={styles.heatScore}>
{formatNumber(getHeatScore(topic))}
</Text>
</div>
</div>
</List.Item>
)}
/>
</Card>
);
};
export default HotTopics;

View File

@@ -0,0 +1,172 @@
.topicCard {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.ant-card-body {
padding: 16px;
}
.ant-card-actions {
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
}
.cardCover {
position: relative;
height: 200px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
.categoryTag {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
}
}
.categoryTagInline {
margin-bottom: 12px;
}
.cardContent {
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.title {
flex: 1;
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
}
.favoriteButton {
margin-left: 8px;
flex-shrink: 0;
color: #faad14;
&:hover {
background-color: #fff7e6;
}
}
}
.description {
margin-bottom: 12px;
color: #666;
font-size: 14px;
line-height: 1.5;
}
.tags {
margin-bottom: 16px;
.tag {
margin-bottom: 4px;
font-size: 12px;
border-radius: 12px;
border: none;
background-color: #f0f0f0;
color: #666;
}
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
.author {
display: flex;
align-items: center;
.authorName {
margin-left: 8px;
font-size: 12px;
}
}
.meta {
.ant-space-item {
font-size: 12px;
}
}
}
}
.actionButton {
border: none;
background: transparent;
color: #666;
transition: all 0.3s;
&:hover {
color: #6f8cff;
background-color: #f0f8ff;
}
.anticon {
margin-right: 4px;
}
}
// 响应式设计
@media (max-width: 768px) {
.topicCard {
margin-bottom: 12px;
.ant-card-body {
padding: 12px;
}
}
.cardCover {
height: 160px;
}
.cardContent {
.header {
.title {
font-size: 14px;
}
}
.description {
font-size: 13px;
}
.footer {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.meta {
align-self: flex-end;
}
}
}
}

View File

@@ -0,0 +1,160 @@
import React from 'react';
import { Card, Tag, Avatar, Button, Space, Typography } from 'antd';
import { HeartOutlined, HeartFilled, EyeOutlined, MessageOutlined, ShareAltOutlined, StarOutlined, StarFilled } from '@ant-design/icons';
import { useNavigate } from 'umi';
import dayjs from 'dayjs';
import type { Topic } from '../../services/mockData';
import styles from './index.less';
const { Text, Paragraph } = Typography;
interface TopicCardProps {
topic: Topic;
onLike?: (topicId: string) => void;
onFavorite?: (topicId: string) => void;
onShare?: (topicId: string) => void;
}
const TopicCard: React.FC<TopicCardProps> = ({
topic,
onLike,
onFavorite,
onShare
}) => {
const navigate = useNavigate();
const handleCardClick = () => {
navigate(`/topic/${topic.id}`);
};
const handleLike = (e: React.MouseEvent) => {
e.stopPropagation();
onLike?.(topic.id);
};
const handleFavorite = (e: React.MouseEvent) => {
e.stopPropagation();
onFavorite?.(topic.id);
};
const handleShare = (e: React.MouseEvent) => {
e.stopPropagation();
onShare?.(topic.id);
};
const formatNumber = (num: number): string => {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}w+`;
}
return num.toString();
};
return (
<Card
className={styles.topicCard}
hoverable
onClick={handleCardClick}
cover={
topic.imageUrl && (
<div className={styles.cardCover}>
<img src={topic.imageUrl} alt={topic.title} />
<div className={styles.categoryTag}>
<Tag color="blue">{topic.category}</Tag>
</div>
</div>
)
}
actions={[
<Button
type="text"
icon={topic.isLiked ? <HeartFilled style={{ color: '#ff4d4f' }} /> : <HeartOutlined />}
onClick={handleLike}
className={styles.actionButton}
>
{formatNumber(topic.likes)}
</Button>,
<Button
type="text"
icon={<MessageOutlined />}
className={styles.actionButton}
>
{topic.comments}
</Button>,
<Button
type="text"
icon={<ShareAltOutlined />}
onClick={handleShare}
className={styles.actionButton}
>
{topic.shares}
</Button>
]}
>
<div className={styles.cardContent}>
{!topic.imageUrl && (
<div className={styles.categoryTagInline}>
<Tag color="blue">{topic.category}</Tag>
</div>
)}
<div className={styles.header}>
<Paragraph
ellipsis={{ rows: 2 }}
className={styles.title}
>
{topic.title}
</Paragraph>
<Button
type="text"
icon={topic.isFavorited ? <StarFilled style={{ color: '#faad14' }} /> : <StarOutlined />}
onClick={handleFavorite}
className={styles.favoriteButton}
/>
</div>
<Paragraph
ellipsis={{ rows: 2 }}
className={styles.description}
>
{topic.content}
</Paragraph>
<div className={styles.tags}>
{topic.tags.map(tag => (
<Tag key={tag} size="small" className={styles.tag}>
{tag}
</Tag>
))}
</div>
<div className={styles.footer}>
<div className={styles.author}>
<Avatar
src={topic.author.avatar}
size="small"
icon={<Avatar icon={<Avatar />} />}
/>
<Text type="secondary" className={styles.authorName}>
{topic.author.name}
</Text>
</div>
<div className={styles.meta}>
<Space size="middle">
<Space size={4}>
<EyeOutlined />
<Text type="secondary">{formatNumber(topic.views)}</Text>
</Space>
<Text type="secondary">
{dayjs(topic.updatedAt).format('YYYY-MM-DD')}
</Text>
</Space>
</div>
</div>
</div>
</Card>
);
};
export default TopicCard;

179
src/layouts/index.less Normal file
View File

@@ -0,0 +1,179 @@
.layout {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0;
position: sticky;
top: 0;
z-index: 1000;
}
.headerContent {
width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
height: 64px;
padding: 0 24px;
justify-content: space-between;
}
.logo {
flex-shrink: 0;
margin-right: 40px;
.logoLink {
font-size: 20px;
font-weight: bold;
color: #6f8cff;
text-decoration: none;
&:hover {
color: #40a9ff;
}
}
}
.nav {
display: flex;
align-items: center;
margin-right: 40px;
.navItem {
margin-right: 24px;
color: #333;
text-decoration: none;
font-size: 16px;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.3s;
&:hover {
color: #6f8cff;
background-color: #f0f8ff;
}
}
}
.search {
flex: 1;
max-width: 400px;
margin-right: 40px;
.ant-input {
border-radius: 20px;
padding-left: 16px;
}
.ant-input-suffix {
margin-right: 8px;
}
}
.userInfo {
flex-shrink: 0;
.userProfile {
display: flex;
align-items: center;
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: background-color 0.3s;
&:hover {
background-color: #f5f5f5;
}
.userName {
margin-left: 8px;
color: #333;
font-size: 14px;
}
}
}
.content {
width: 1200px;
margin: 0 auto;
padding: 24px;
background: transparent;
}
.floatingService {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1000;
.serviceButton {
width: 56px;
height: 56px;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
animation: float 3s ease-in-out infinite;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
}
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
// 响应式设计
@media (max-width: 768px) {
.headerContent {
padding: 0 16px;
flex-wrap: wrap;
height: auto;
min-height: 64px;
}
.logo {
margin-right: 16px;
order: 1;
}
.nav {
margin-right: 16px;
order: 2;
}
.search {
order: 4;
width: 100%;
max-width: none;
margin: 16px 0 0 0;
}
.userInfo {
order: 3;
margin-left: auto;
}
.content {
padding: 16px;
}
.floatingService {
bottom: 16px;
right: 16px;
.serviceButton {
width: 48px;
height: 48px;
}
}
}

129
src/layouts/index.tsx Normal file
View File

@@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';
import { Link, Outlet, useNavigate } from 'umi';
import { Layout as AntLayout, Input, Button, Avatar, Dropdown, Menu, Space, Badge } from 'antd';
import { SearchOutlined, UserOutlined, BellOutlined, BookOutlined, MessageOutlined } from '@ant-design/icons';
import { userApi } from '../services/api';
import type { User } from '../services/mockData';
import styles from './index.less';
const { Header, Content } = AntLayout;
export default function Layout() {
const navigate = useNavigate();
const [searchValue, setSearchValue] = useState('');
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 获取用户信息
userApi.getCurrentUser().then(res => {
if (res.success) {
setUser(res.data);
}
});
}, []);
const handleSearch = () => {
if (searchValue.trim()) {
navigate(`/?search=${encodeURIComponent(searchValue)}`);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
const userMenu = (
<Menu>
<Menu.Item key="profile">
<UserOutlined />
</Menu.Item>
<Menu.Item key="settings">
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
退
</Menu.Item>
</Menu>
);
return (
<AntLayout className={styles.layout}>
<Header className={styles.header}>
<div className={styles.headerContent}>
{/* Logo区域 */}
<div className={styles.logo}>
<Link to="/" className={styles.logoLink}>
AutoBeeAgent
</Link>
</div>
{/* 搜索框 */}
<div className={styles.search}>
<Input
placeholder="搜索热门话题..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyPress={handleKeyPress}
prefix={<SearchOutlined />}
// suffix={
// <Button
// type="primary"
// size="small"
// onClick={handleSearch}
// loading={loading}
// >
// AI搜索
// </Button>
// }
/>
</div>
{/* 用户信息 */}
<div className={styles.userInfo}>
<Space size="middle">
<Button type="text" icon={<BookOutlined />}>
</Button>
<Badge count={0}>
<Button type="text" icon={<BellOutlined />}>
</Button>
</Badge>
{user ? (
<Dropdown overlay={userMenu} placement="bottomRight">
<div className={styles.userProfile}>
<Avatar src={user.avatar} icon={<UserOutlined />} />
<span className={styles.userName}>{user.name}</span>
</div>
</Dropdown>
) : (
<Button type="primary" onClick={() => navigate('/login')}>
</Button>
)}
</Space>
</div>
</div>
</Header>
<Content className={styles.content}>
<Outlet />
</Content>
{/* 浮动客服按钮 */}
<div className={styles.floatingService}>
<Button
type="primary"
shape="circle"
icon={<MessageOutlined />}
size="large"
className={styles.serviceButton}
/>
</div>
</AntLayout>
);
}

9
src/pages/docs.tsx Normal file
View File

@@ -0,0 +1,9 @@
const DocsPage = () => {
return (
<div>
<p>This is umi docs.</p>
</div>
);
};
export default DocsPage;

180
src/pages/index.less Normal file
View File

@@ -0,0 +1,180 @@
.homePage {
min-height: 100vh;
background-color: #f5f5f5;
}
.banner {
background: linear-gradient(90deg, #a7bfff, #6f8cff);
color: white;
padding: 40px 0;
margin-bottom: 24px;
border-radius: 8px;
.bannerContent {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.bannerText {
h2 {
font-size: 28px;
font-weight: bold;
margin: 0 0 8px 0;
}
p {
font-size: 16px;
margin: 0;
opacity: 0.9;
}
}
.bannerActions {
.publishButton {
height: 40px;
padding: 0 24px;
font-size: 16px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}
}
}
}
.topicsSection {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
h3 {
font-size: 20px;
font-weight: 600;
margin: 0;
color: #333;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
.ant-select {
.ant-select-selector {
border-radius: 6px;
}
}
.ant-btn {
border-radius: 6px;
}
}
}
.topicsList {
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 0;
}
.empty {
padding: 60px 0;
}
}
.sidebar {
position: sticky;
top: 88px;
}
// 响应式设计
@media (max-width: 768px) {
.banner {
padding: 24px 0;
margin-bottom: 16px;
.bannerContent {
flex-direction: column;
gap: 16px;
text-align: center;
padding: 0 16px;
}
.bannerText {
h2 {
font-size: 24px;
}
p {
font-size: 14px;
}
}
.bannerActions {
.publishButton {
height: 36px;
padding: 0 20px;
font-size: 14px;
}
}
}
.topicsSection {
padding: 16px;
}
.sectionHeader {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.filters {
width: 100%;
justify-content: space-between;
.ant-select {
flex: 1;
}
}
}
.sidebar {
position: static;
margin-top: 16px;
}
}
@media (max-width: 576px) {
.sectionHeader {
.filters {
flex-direction: column;
gap: 8px;
.ant-select,
.ant-btn {
width: 100%;
}
}
}
}

255
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,255 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Select, Button, Input, Spin, Empty, message } from 'antd';
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { useNavigate, useSearchParams } from 'umi';
import { topicApi, configApi } from '../services/api';
import type { Topic } from '../services/mockData';
import TopicCard from '../components/TopicCard';
import HotTopics from '../components/HotTopics';
import styles from './index.less';
const { Option } = Select;
export default function HomePage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [topics, setTopics] = useState<Topic[]>([]);
const [hotTopics, setHotTopics] = useState<Topic[]>([]);
const [loading, setLoading] = useState(false);
const [hotTopicsLoading, setHotTopicsLoading] = useState(false);
const [categories, setCategories] = useState<Array<{label: string, value: string}>>([]);
const [sortOptions, setSortOptions] = useState<Array<{label: string, value: string}>>([]);
const [filters, setFilters] = useState({
category: 'all',
sort: 'hot',
search: searchParams.get('search') || '',
page: 1,
pageSize: 10
});
// 获取分类和排序选项
useEffect(() => {
Promise.all([
configApi.getCategories(),
configApi.getSortOptions()
]).then(([categoriesRes, sortRes]) => {
if (categoriesRes.success) {
setCategories(categoriesRes.data);
}
if (sortRes.success) {
setSortOptions(sortRes.data);
}
});
}, []);
// 获取话题列表
const fetchTopics = async () => {
setLoading(true);
try {
const response = await topicApi.getTopics(filters);
if (response.success) {
setTopics(response.data.list);
} else {
message.error('获取话题列表失败');
}
} catch (error) {
message.error('网络错误,请稍后重试');
} finally {
setLoading(false);
}
};
// 获取热门话题
const fetchHotTopics = async () => {
setHotTopicsLoading(true);
try {
const response = await topicApi.getHotTopics(5);
if (response.success) {
setHotTopics(response.data);
}
} catch (error) {
console.error('获取热门话题失败:', error);
} finally {
setHotTopicsLoading(false);
}
};
useEffect(() => {
fetchTopics();
}, [filters]);
useEffect(() => {
fetchHotTopics();
}, []);
const handleCategoryChange = (value: string) => {
setFilters(prev => ({ ...prev, category: value, page: 1 }));
};
const handleSortChange = (value: string) => {
setFilters(prev => ({ ...prev, sort: value, page: 1 }));
};
const handleSearch = (value: string) => {
setFilters(prev => ({ ...prev, search: value, page: 1 }));
};
const handleLike = async (topicId: string) => {
try {
const response = await topicApi.likeTopic(topicId);
if (response.success) {
// 更新本地状态
setTopics(prev => prev.map(topic =>
topic.id === topicId
? { ...topic, isLiked: response.data.liked, likes: response.data.likes }
: topic
));
setHotTopics(prev => prev.map(topic =>
topic.id === topicId
? { ...topic, isLiked: response.data.liked, likes: response.data.likes }
: topic
));
}
} catch (error) {
message.error('操作失败,请稍后重试');
}
};
const handleFavorite = async (topicId: string) => {
try {
const response = await topicApi.favoriteTopic(topicId);
if (response.success) {
// 更新本地状态
setTopics(prev => prev.map(topic =>
topic.id === topicId
? { ...topic, isFavorited: response.data.favorited }
: topic
));
setHotTopics(prev => prev.map(topic =>
topic.id === topicId
? { ...topic, isFavorited: response.data.favorited }
: topic
));
}
} catch (error) {
message.error('操作失败,请稍后重试');
}
};
const handleShare = (topicId: string) => {
// 实现分享功能
message.success('分享链接已复制到剪贴板');
};
const handleRefresh = () => {
fetchTopics();
fetchHotTopics();
};
const handlePublishTopic = () => {
navigate('/publish');
};
return (
<div className={styles.homePage}>
{/* 热门话题横幅 */}
<div className={styles.banner}>
<div className={styles.bannerContent}>
<div className={styles.bannerText}>
<h2></h2>
<p>线,!</p>
</div>
<div className={styles.bannerActions}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handlePublishTopic}
className={styles.publishButton}
>
</Button>
</div>
</div>
</div>
<Row gutter={24}>
{/* 左侧话题列表 */}
<Col xs={24} lg={16}>
<div className={styles.topicsSection}>
<div className={styles.sectionHeader}>
<h3></h3>
<div className={styles.filters}>
<Select
value={filters.category}
onChange={handleCategoryChange}
style={{ width: 120 }}
size="middle"
>
{categories.map(category => (
<Option key={category.value} value={category.value}>
{category.label}
</Option>
))}
</Select>
<Select
value={filters.sort}
onChange={handleSortChange}
style={{ width: 120 }}
size="middle"
>
{sortOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
>
</Button>
</div>
</div>
<div className={styles.topicsList}>
{loading ? (
<div className={styles.loading}>
<Spin size="large" />
</div>
) : topics.length > 0 ? (
topics.map(topic => (
<TopicCard
key={topic.id}
topic={topic}
onLike={handleLike}
onFavorite={handleFavorite}
onShare={handleShare}
/>
))
) : (
<Empty
description="暂无话题数据"
className={styles.empty}
/>
)}
</div>
</div>
</Col>
{/* 右侧热门话题排行 */}
<Col xs={24} lg={8}>
<div className={styles.sidebar}>
<HotTopics
topics={hotTopics}
loading={hotTopicsLoading}
/>
</div>
</Col>
</Row>
</div>
);
}

297
src/pages/topic/detail.less Normal file
View File

@@ -0,0 +1,297 @@
.detailPage {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 24px;
}
.backButton {
margin-bottom: 16px;
border-radius: 6px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.notFound {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
}
.topicDetail {
.topicBanner {
position: relative;
height: 300px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.1));
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding: 20px;
.bannerActions {
.favoriteButton {
background: rgba(255, 255, 255, 0.9);
border: none;
color: #faad14;
border-radius: 20px;
height: 36px;
padding: 0 16px;
&:hover {
background: white;
color: #faad14;
}
}
}
}
}
.topicContent {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
.topicHeader {
.categoryTag {
margin-bottom: 16px;
.category {
display: inline-block;
background: #6f8cff;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
}
.topicTitle {
margin-bottom: 16px;
color: #333;
line-height: 1.4;
}
.topicMeta {
color: #666;
font-size: 14px;
}
}
.topicBody {
.topicDescription {
font-size: 16px;
line-height: 1.8;
color: #333;
margin-bottom: 24px;
}
.topicTags {
display: flex;
flex-wrap: wrap;
gap: 8px;
.tag {
background: #f0f0f0;
color: #666;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
}
}
}
.topicActions {
.actionButton {
height: 40px;
padding: 0 16px;
border-radius: 20px;
transition: all 0.3s;
&:hover {
background-color: #f0f8ff;
color: #6f8cff;
}
}
}
}
.commentsSection {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.commentForm {
.commentInput {
border-radius: 8px;
margin-bottom: 12px;
}
.commentActions {
display: flex;
justify-content: flex-end;
}
}
.commentsList {
.commentItem {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.commentHeader {
margin-bottom: 8px;
}
.commentContent {
margin-bottom: 12px;
.ant-typography {
margin: 0;
color: #333;
line-height: 1.6;
}
}
.commentActions {
.ant-btn {
height: 28px;
padding: 0 8px;
font-size: 12px;
}
}
}
.emptyComments {
text-align: center;
padding: 40px 0;
}
}
}
}
.sidebar {
position: sticky;
top: 88px;
}
// 响应式设计
@media (max-width: 768px) {
.detailPage {
padding-bottom: 16px;
}
.topicDetail {
.topicBanner {
height: 200px;
margin-bottom: 16px;
.bannerOverlay {
padding: 16px;
.bannerActions {
.favoriteButton {
height: 32px;
padding: 0 12px;
font-size: 12px;
}
}
}
}
.topicContent {
margin-bottom: 16px;
.topicHeader {
.topicTitle {
font-size: 20px;
}
.topicMeta {
font-size: 13px;
}
}
.topicBody {
.topicDescription {
font-size: 15px;
}
}
}
.commentsSection {
.commentForm {
.commentInput {
margin-bottom: 8px;
}
}
.commentsList {
.commentItem {
padding: 12px 0;
.commentHeader {
margin-bottom: 6px;
}
.commentContent {
margin-bottom: 8px;
}
}
}
}
}
.sidebar {
position: static;
margin-top: 16px;
}
}
@media (max-width: 576px) {
.topicDetail {
.topicBanner {
height: 160px;
}
.topicContent {
.topicHeader {
.topicTitle {
font-size: 18px;
}
}
.topicBody {
.topicDescription {
font-size: 14px;
}
}
}
}
}

367
src/pages/topic/detail.tsx Normal file
View File

@@ -0,0 +1,367 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Card, Typography, Button, Avatar, Space, Divider, Input, message, Spin } from 'antd';
import { HeartOutlined, HeartFilled, StarOutlined, StarFilled, ShareAltOutlined, ArrowLeftOutlined, EyeOutlined, MessageOutlined } from '@ant-design/icons';
import { useParams, useNavigate } from 'umi';
import dayjs from 'dayjs';
import { topicApi, commentApi } from '../../services/api';
import type { Topic, Comment } from '../../services/mockData';
import HotTopics from '../../components/HotTopics';
import styles from './detail.less';
const { Title, Paragraph, Text } = Typography;
const { TextArea } = Input;
export default function TopicDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [topic, setTopic] = useState<Topic | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState(true);
const [commentsLoading, setCommentsLoading] = useState(true);
const [hotTopics, setHotTopics] = useState<Topic[]>([]);
const [commentText, setCommentText] = useState('');
const [submittingComment, setSubmittingComment] = useState(false);
// 获取话题详情
const fetchTopicDetail = async () => {
if (!id) return;
setLoading(true);
try {
const response = await topicApi.getTopicDetail(id);
if (response.success) {
setTopic(response.data);
} else {
message.error(response.message || '话题不存在');
navigate('/');
}
} catch (error) {
message.error('获取话题详情失败');
} finally {
setLoading(false);
}
};
// 获取评论列表
const fetchComments = async () => {
if (!id) return;
setCommentsLoading(true);
try {
const response = await topicApi.getTopicComments(id);
if (response.success) {
setComments(response.data);
}
} catch (error) {
console.error('获取评论失败:', error);
} finally {
setCommentsLoading(false);
}
};
// 获取热门话题
const fetchHotTopics = async () => {
try {
const response = await topicApi.getHotTopics(5);
if (response.success) {
setHotTopics(response.data);
}
} catch (error) {
console.error('获取热门话题失败:', error);
}
};
useEffect(() => {
fetchTopicDetail();
fetchComments();
fetchHotTopics();
}, [id]);
const handleLike = async () => {
if (!topic) return;
try {
const response = await topicApi.likeTopic(topic.id);
if (response.success) {
setTopic(prev => prev ? {
...prev,
isLiked: response.data.liked,
likes: response.data.likes
} : null);
}
} catch (error) {
message.error('操作失败,请稍后重试');
}
};
const handleFavorite = async () => {
if (!topic) return;
try {
const response = await topicApi.favoriteTopic(topic.id);
if (response.success) {
setTopic(prev => prev ? {
...prev,
isFavorited: response.data.favorited
} : null);
}
} catch (error) {
message.error('操作失败,请稍后重试');
}
};
const handleShare = () => {
message.success('分享链接已复制到剪贴板');
};
const handleSubmitComment = async () => {
if (!commentText.trim() || !topic) return;
setSubmittingComment(true);
try {
const response = await commentApi.publishComment(topic.id, commentText);
if (response.success) {
setComments(prev => [response.data, ...prev]);
setTopic(prev => prev ? { ...prev, comments: prev.comments + 1 } : null);
setCommentText('');
message.success('评论发布成功');
}
} catch (error) {
message.error('发布评论失败');
} finally {
setSubmittingComment(false);
}
};
const handleLikeComment = async (commentId: string) => {
try {
const response = await commentApi.likeComment(commentId);
if (response.success) {
setComments(prev => prev.map(comment =>
comment.id === commentId
? { ...comment, isLiked: response.data.liked, likes: response.data.likes }
: comment
));
}
} catch (error) {
message.error('操作失败');
}
};
const formatNumber = (num: number): string => {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}w+`;
}
return num.toString();
};
if (loading) {
return (
<div className={styles.loading}>
<Spin size="large" />
</div>
);
}
if (!topic) {
return (
<div className={styles.notFound}>
<Title level={3}></Title>
<Button onClick={() => navigate('/')}></Button>
</div>
);
}
return (
<div className={styles.detailPage}>
{/* 返回按钮 */}
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
className={styles.backButton}
>
</Button>
<Row gutter={24}>
{/* 左侧话题详情 */}
<Col xs={24} lg={16}>
<div className={styles.topicDetail}>
{/* 话题横幅 */}
{topic.imageUrl && (
<div className={styles.topicBanner}>
<img src={topic.imageUrl} alt={topic.title} />
<div className={styles.bannerOverlay}>
<div className={styles.bannerActions}>
<Button
type="primary"
icon={topic.isFavorited ? <StarFilled /> : <StarOutlined />}
onClick={handleFavorite}
className={styles.favoriteButton}
>
{topic.isFavorited ? '已收藏' : '收藏'}
</Button>
</div>
</div>
</div>
)}
{/* 话题内容 */}
<Card className={styles.topicContent}>
<div className={styles.topicHeader}>
<div className={styles.categoryTag}>
<span className={styles.category}>{topic.category}</span>
</div>
<Title level={2} className={styles.topicTitle}>
{topic.title}
</Title>
<div className={styles.topicMeta}>
<Space size="large">
<Space size={4}>
<Avatar src={topic.author.avatar} size="small" />
<Text strong>{topic.author.name}</Text>
</Space>
<Text type="secondary">
{dayjs(topic.createdAt).format('YYYY-MM-DD HH:mm')}
</Text>
<Space size={4}>
<EyeOutlined />
<Text type="secondary">{formatNumber(topic.views)}</Text>
</Space>
</Space>
</div>
</div>
<Divider />
<div className={styles.topicBody}>
<Paragraph className={styles.topicDescription}>
{topic.content}
</Paragraph>
{topic.tags.length > 0 && (
<div className={styles.topicTags}>
{topic.tags.map(tag => (
<span key={tag} className={styles.tag}>
{tag}
</span>
))}
</div>
)}
</div>
<Divider />
<div className={styles.topicActions}>
<Space size="large">
<Button
type="text"
icon={topic.isLiked ? <HeartFilled style={{ color: '#ff4d4f' }} /> : <HeartOutlined />}
onClick={handleLike}
className={styles.actionButton}
>
{formatNumber(topic.likes)}
</Button>
<Button
type="text"
icon={<MessageOutlined />}
className={styles.actionButton}
>
{topic.comments}
</Button>
<Button
type="text"
icon={<ShareAltOutlined />}
onClick={handleShare}
className={styles.actionButton}
>
{topic.shares}
</Button>
</Space>
</div>
</Card>
{/* 评论区域 */}
<Card className={styles.commentsSection} title="评论">
{/* 发表评论 */}
<div className={styles.commentForm}>
<TextArea
rows={4}
placeholder="发表你的看法..."
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
className={styles.commentInput}
/>
<div className={styles.commentActions}>
<Button
type="primary"
onClick={handleSubmitComment}
loading={submittingComment}
disabled={!commentText.trim()}
>
</Button>
</div>
</div>
<Divider />
{/* 评论列表 */}
<div className={styles.commentsList}>
{commentsLoading ? (
<div className={styles.loading}>
<Spin />
</div>
) : comments.length > 0 ? (
comments.map(comment => (
<div key={comment.id} className={styles.commentItem}>
<div className={styles.commentHeader}>
<Space>
<Avatar src={comment.author.avatar} size="small" />
<Text strong>{comment.author.name}</Text>
<Text type="secondary">
{dayjs(comment.createdAt).format('YYYY-MM-DD HH:mm')}
</Text>
</Space>
</div>
<div className={styles.commentContent}>
<Paragraph>{comment.content}</Paragraph>
</div>
<div className={styles.commentActions}>
<Space>
<Button
type="text"
size="small"
icon={comment.isLiked ? <HeartFilled style={{ color: '#ff4d4f' }} /> : <HeartOutlined />}
onClick={() => handleLikeComment(comment.id)}
>
{comment.likes}
</Button>
<Button type="text" size="small">
</Button>
</Space>
</div>
</div>
))
) : (
<div className={styles.emptyComments}>
<Text type="secondary"></Text>
</div>
)}
</div>
</Card>
</div>
</Col>
{/* 右侧相关话题 */}
<Col xs={24} lg={8}>
<div className={styles.sidebar}>
<HotTopics topics={hotTopics} />
</div>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,176 @@
.publishPage {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 24px;
}
.header {
display: flex;
align-items: center;
margin-bottom: 24px;
gap: 16px;
.backButton {
border-radius: 6px;
}
h2 {
margin: 0;
color: #333;
}
}
.publishCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.ant-card-body {
padding: 32px;
}
}
.publishForm {
max-width: 800px;
margin: 0 auto;
.ant-form-item-label > label {
font-weight: 600;
color: #333;
}
.titleInput {
border-radius: 6px;
font-size: 16px;
}
.categorySelect {
border-radius: 6px;
.ant-select-selector {
border-radius: 6px;
}
}
.contentInput {
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
}
.tagsInput {
border-radius: 6px;
}
.uploadComponent {
.ant-upload-select-picture-card {
width: 200px;
height: 120px;
border-radius: 8px;
border: 2px dashed #d9d9d9;
transition: border-color 0.3s;
&:hover {
border-color: #6f8cff;
}
}
.uploadImage {
width: 100%;
height: 100%;
object-fit: cover;
}
.uploadPlaceholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 14px;
.anticon {
font-size: 24px;
color: #d9d9d9;
}
}
.uploadText {
margin-top: 8px;
}
}
.submitSection {
margin-bottom: 0;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
.submitButton {
height: 44px;
padding: 0 32px;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
}
.cancelButton {
height: 44px;
padding: 0 32px;
border-radius: 22px;
font-size: 16px;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.publishPage {
padding-bottom: 16px;
}
.header {
margin-bottom: 16px;
flex-direction: column;
align-items: flex-start;
gap: 12px;
h2 {
font-size: 20px;
}
}
.publishCard {
.ant-card-body {
padding: 20px;
}
}
.publishForm {
.uploadComponent {
.ant-upload-select-picture-card {
width: 150px;
height: 90px;
}
}
.submitSection {
.submitButton,
.cancelButton {
width: 100%;
height: 40px;
font-size: 14px;
}
}
}
}
@media (max-width: 576px) {
.publishForm {
.uploadComponent {
.ant-upload-select-picture-card {
width: 120px;
height: 72px;
}
}
}
}

195
src/pages/topic/publish.tsx Normal file
View File

@@ -0,0 +1,195 @@
import React, { useState, useEffect } from 'react';
import { Card, Form, Input, Select, Button, Upload, message, Space, Typography } from 'antd';
import { PlusOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useNavigate } from 'umi';
import { topicApi, configApi } from '../../services/api';
import type { Topic } from '../../services/mockData';
import styles from './publish.less';
const { Title } = Typography;
const { TextArea } = Input;
const { Option } = Select;
export default function PublishTopicPage() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [categories, setCategories] = useState<Array<{label: string, value: string}>>([]);
const [imageUrl, setImageUrl] = useState<string>('');
// 获取分类选项
useEffect(() => {
configApi.getCategories().then(res => {
if (res.success) {
setCategories(res.data);
}
});
}, []);
const handleSubmit = async (values: any) => {
setLoading(true);
try {
const topicData: Partial<Topic> = {
title: values.title,
content: values.content,
category: values.category,
tags: values.tags ? values.tags.split(',').map((tag: string) => tag.trim()) : [],
imageUrl: imageUrl
};
const response = await topicApi.publishTopic(topicData);
if (response.success) {
message.success('话题发布成功!');
navigate(`/topic/${response.data.id}`);
} else {
message.error('发布失败,请重试');
}
} catch (error) {
message.error('发布失败,请重试');
} finally {
setLoading(false);
}
};
const handleImageUpload = (info: any) => {
if (info.file.status === 'done') {
// 模拟上传成功实际项目中需要调用上传API
const mockUrl = `https://picsum.photos/400/200?random=${Date.now()}`;
setImageUrl(mockUrl);
message.success('图片上传成功');
} else if (info.file.status === 'error') {
message.error('图片上传失败');
}
};
const handleCancel = () => {
navigate('/');
};
return (
<div className={styles.publishPage}>
<div className={styles.header}>
<Button
icon={<ArrowLeftOutlined />}
onClick={handleCancel}
className={styles.backButton}
>
</Button>
<Title level={2}></Title>
</div>
<Card className={styles.publishCard}>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
className={styles.publishForm}
>
<Form.Item
label="话题标题"
name="title"
rules={[
{ required: true, message: '请输入话题标题' },
{ max: 100, message: '标题不能超过100个字符' }
]}
>
<Input
placeholder="请输入话题标题"
size="large"
className={styles.titleInput}
/>
</Form.Item>
<Form.Item
label="话题分类"
name="category"
rules={[{ required: true, message: '请选择话题分类' }]}
>
<Select
placeholder="请选择话题分类"
size="large"
className={styles.categorySelect}
>
{categories.map(category => (
<Option key={category.value} value={category.value}>
{category.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
label="话题内容"
name="content"
rules={[
{ required: true, message: '请输入话题内容' },
{ min: 10, message: '内容至少需要10个字符' }
]}
>
<TextArea
rows={8}
placeholder="请详细描述你的话题内容..."
className={styles.contentInput}
/>
</Form.Item>
<Form.Item
label="话题标签"
name="tags"
extra="多个标签请用逗号分隔"
>
<Input
placeholder="例如AI,智能客服,产品发布"
className={styles.tagsInput}
/>
</Form.Item>
<Form.Item
label="话题封面"
name="cover"
>
<Upload
listType="picture-card"
showUploadList={false}
onChange={handleImageUpload}
beforeUpload={() => false} // 阻止自动上传,这里只是模拟
className={styles.uploadComponent}
>
{imageUrl ? (
<img src={imageUrl} alt="cover" className={styles.uploadImage} />
) : (
<div className={styles.uploadPlaceholder}>
<PlusOutlined />
<div className={styles.uploadText}></div>
</div>
)}
</Upload>
</Form.Item>
<Form.Item className={styles.submitSection}>
<Space size="middle">
<Button
type="primary"
htmlType="submit"
loading={loading}
size="large"
className={styles.submitButton}
>
</Button>
<Button
onClick={handleCancel}
size="large"
className={styles.cancelButton}
>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
</div>
);
}

272
src/services/api.ts Normal file
View File

@@ -0,0 +1,272 @@
import { mockTopics, mockComments, mockUser, categories, sortOptions, Topic, Comment, User } from './mockData';
// API响应类型
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 模拟网络延迟
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// 话题相关API
export const topicApi = {
// 获取话题列表
async getTopics(params: {
page?: number;
pageSize?: number;
category?: string;
sort?: string;
search?: string;
} = {}): Promise<ApiResponse<PaginatedResponse<Topic>>> {
await delay(500); // 模拟网络请求
let filteredTopics = [...mockTopics];
// 分类筛选
if (params.category && params.category !== 'all') {
filteredTopics = filteredTopics.filter(topic => topic.category === params.category);
}
// 搜索筛选
if (params.search) {
const searchLower = params.search.toLowerCase();
filteredTopics = filteredTopics.filter(topic =>
topic.title.toLowerCase().includes(searchLower) ||
topic.content.toLowerCase().includes(searchLower) ||
topic.tags.some(tag => tag.toLowerCase().includes(searchLower))
);
}
// 排序
if (params.sort) {
switch (params.sort) {
case 'hot':
filteredTopics.sort((a, b) => (b.likes + b.views + b.comments) - (a.likes + a.views + a.comments));
break;
case 'latest':
filteredTopics.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
break;
case 'comments':
filteredTopics.sort((a, b) => b.comments - a.comments);
break;
case 'likes':
filteredTopics.sort((a, b) => b.likes - a.likes);
break;
}
}
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const start = (page - 1) * pageSize;
const end = start + pageSize;
return {
success: true,
data: {
list: filteredTopics.slice(start, end),
total: filteredTopics.length,
page,
pageSize
}
};
},
// 获取热门话题排行
async getHotTopics(limit: number = 5): Promise<ApiResponse<Topic[]>> {
await delay(300);
const hotTopics = [...mockTopics]
.sort((a, b) => (b.likes + b.views + b.comments) - (a.likes + a.views + a.comments))
.slice(0, limit);
return {
success: true,
data: hotTopics
};
},
// 获取话题详情
async getTopicDetail(id: string): Promise<ApiResponse<Topic>> {
await delay(400);
const topic = mockTopics.find(t => t.id === id);
if (!topic) {
return {
success: false,
data: null as any,
message: '话题不存在'
};
}
return {
success: true,
data: topic
};
},
// 获取话题评论
async getTopicComments(topicId: string): Promise<ApiResponse<Comment[]>> {
await delay(300);
// 模拟根据话题ID返回评论
return {
success: true,
data: mockComments
};
},
// 点赞话题
async likeTopic(id: string): Promise<ApiResponse<{ liked: boolean; likes: number }>> {
await delay(200);
const topic = mockTopics.find(t => t.id === id);
if (topic) {
topic.isLiked = !topic.isLiked;
topic.likes += topic.isLiked ? 1 : -1;
}
return {
success: true,
data: {
liked: topic?.isLiked || false,
likes: topic?.likes || 0
}
};
},
// 收藏话题
async favoriteTopic(id: string): Promise<ApiResponse<{ favorited: boolean }>> {
await delay(200);
const topic = mockTopics.find(t => t.id === id);
if (topic) {
topic.isFavorited = !topic.isFavorited;
}
return {
success: true,
data: {
favorited: topic?.isFavorited || false
}
};
},
// 发布话题
async publishTopic(topic: Partial<Topic>): Promise<ApiResponse<Topic>> {
await delay(1000);
const newTopic: Topic = {
id: Date.now().toString(),
title: topic.title || '',
content: topic.content || '',
author: topic.author || mockUser,
category: topic.category || '其他',
tags: topic.tags || [],
imageUrl: topic.imageUrl,
likes: 0,
views: 0,
comments: 0,
shares: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isLiked: false,
isFavorited: false
};
mockTopics.unshift(newTopic);
return {
success: true,
data: newTopic
};
}
};
// 评论相关API
export const commentApi = {
// 发布评论
async publishComment(topicId: string, content: string, parentId?: string): Promise<ApiResponse<Comment>> {
await delay(500);
const newComment: Comment = {
id: Date.now().toString(),
content,
author: mockUser,
likes: 0,
replies: [],
createdAt: new Date().toISOString(),
isLiked: false
};
mockComments.unshift(newComment);
return {
success: true,
data: newComment
};
},
// 点赞评论
async likeComment(id: string): Promise<ApiResponse<{ liked: boolean; likes: number }>> {
await delay(200);
const comment = mockComments.find(c => c.id === id);
if (comment) {
comment.isLiked = !comment.isLiked;
comment.likes += comment.isLiked ? 1 : -1;
}
return {
success: true,
data: {
liked: comment?.isLiked || false,
likes: comment?.likes || 0
}
};
}
};
// 用户相关API
export const userApi = {
// 获取当前用户信息
async getCurrentUser(): Promise<ApiResponse<User>> {
await delay(200);
return {
success: true,
data: mockUser
};
}
};
// 配置相关API
export const configApi = {
// 获取分类选项
async getCategories(): Promise<ApiResponse<typeof categories>> {
await delay(100);
return {
success: true,
data: categories
};
},
// 获取排序选项
async getSortOptions(): Promise<ApiResponse<typeof sortOptions>> {
await delay(100);
return {
success: true,
data: sortOptions
};
}
};

257
src/services/mockData.ts Normal file
View File

@@ -0,0 +1,257 @@
// Mock数据定义
export interface Topic {
id: string;
title: string;
content: string;
author: {
id: string;
name: string;
avatar: string;
};
category: string;
tags: string[];
imageUrl?: string;
likes: number;
views: number;
comments: number;
shares: number;
createdAt: string;
updatedAt: string;
isLiked?: boolean;
isFavorited?: boolean;
}
export interface Comment {
id: string;
content: string;
author: {
id: string;
name: string;
avatar: string;
};
likes: number;
replies: Comment[];
createdAt: string;
isLiked?: boolean;
}
export interface User {
id: string;
name: string;
avatar: string;
email: string;
role: 'user' | 'admin';
}
// Mock数据
export const mockTopics: Topic[] = [
{
id: '1',
title: 'AutoBeeAgent使用规则详解如何正确配置和使用智能客服',
content: 'AutoBeeAgent作为AI驱动的智能客服解决方案在使用过程中需要遵循特定的规则和最佳实践。本指南详细介绍了Agent配置、对话流程设计、知识库管理、性能优化等核心使用规则帮助用户充分发挥AutoBeeAgent的智能化能力。',
author: {
id: 'user1',
name: 'AutoBee专家',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1'
},
category: '使用规则',
tags: ['AutoBeeAgent', '使用规则', '配置指南'],
imageUrl: 'https://picsum.photos/400/200?random=1',
likes: 12000,
views: 15000,
comments: 45,
shares: 120,
createdAt: '2025-09-05T10:00:00Z',
updatedAt: '2025-09-05T10:00:00Z',
isLiked: false,
isFavorited: false
},
{
id: '2',
title: 'RPA机器人流程自动化入门从零开始学习RPA技术',
content: 'RPARobotic Process Automation机器人流程自动化是当前企业数字化转型的重要技术。本文从基础概念讲起详细介绍了RPA的工作原理、应用场景、主流工具选择以及实施策略。无论你是技术新手还是有经验的开发者都能从中获得实用的RPA知识。',
author: {
id: 'user2',
name: 'RPA导师',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2'
},
category: 'RPA知识',
tags: ['RPA', '流程自动化', '入门教程'],
imageUrl: 'https://picsum.photos/400/200?random=2',
likes: 8500,
views: 12000,
comments: 23,
shares: 56,
createdAt: '2025-09-09T14:30:00Z',
updatedAt: '2025-09-09T14:30:00Z',
isLiked: false,
isFavorited: false
},
{
id: '3',
title: 'AutoBeeAgent最佳实践如何设计高效的对话流程',
content: '对话流程设计是AutoBeeAgent成功应用的关键。本指南分享了对话流程设计的最佳实践包括意图识别优化、上下文管理、异常处理、用户引导等核心技巧。通过合理的流程设计可以显著提升用户满意度和问题解决率。',
author: {
id: 'user3',
name: '对话设计师',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=3'
},
category: '使用规则',
tags: ['对话设计', '最佳实践', '流程优化'],
imageUrl: 'https://picsum.photos/400/200?random=3',
likes: 32000,
views: 45000,
comments: 156,
shares: 890,
createdAt: '2025-06-30T09:15:00Z',
updatedAt: '2025-06-30T09:15:00Z',
isLiked: false,
isFavorited: false
},
{
id: '4',
title: 'RPA实施策略企业如何成功部署RPA解决方案',
content: 'RPA项目的成功实施需要科学的策略和方法。本文深入分析了RPA项目的前期准备、流程选择、工具选型、团队建设、风险管控等关键环节。结合真实案例为企业提供了一套完整的RPA实施方法论帮助避免常见的实施陷阱。',
author: {
id: 'user4',
name: 'RPA顾问',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=4'
},
category: 'RPA知识',
tags: ['RPA实施', '项目策略', '企业应用'],
imageUrl: 'https://picsum.photos/400/200?random=4',
likes: 28000,
views: 38000,
comments: 89,
shares: 234,
createdAt: '2025-06-23T16:45:00Z',
updatedAt: '2025-06-23T16:45:00Z',
isLiked: false,
isFavorited: false
},
{
id: '5',
title: 'AutoBeeAgent知识库管理构建高质量的知识体系',
content: '知识库是AutoBeeAgent智能客服的核心组件其质量直接影响服务质量。本指南详细介绍了知识库构建、维护、优化的完整流程包括内容结构设计、知识分类、更新策略、质量评估等关键环节帮助用户建立高效的知识管理体系。',
author: {
id: 'user5',
name: '知识管理专家',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=5'
},
category: '使用规则',
tags: ['知识库', '内容管理', '质量优化'],
imageUrl: 'https://picsum.photos/400/200?random=5',
likes: 15000,
views: 22000,
comments: 67,
shares: 145,
createdAt: '2025-08-15T11:20:00Z',
updatedAt: '2025-08-15T11:20:00Z',
isLiked: false,
isFavorited: false
},
{
id: '6',
title: 'RPA工具对比分析UiPath vs Automation Anywhere vs Blue Prism',
content: '选择合适的RPA工具是项目成功的关键因素。本文对三大主流RPA平台进行了全面对比分析包括功能特性、技术架构、易用性、成本效益、生态系统等多个维度。通过详细的对比表格和实际案例帮助读者做出明智的工具选择决策。',
author: {
id: 'user6',
name: 'RPA分析师',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=6'
},
category: 'RPA知识',
tags: ['RPA工具', '对比分析', '选型指南'],
imageUrl: 'https://picsum.photos/400/200?random=6',
likes: 18000,
views: 25000,
comments: 78,
shares: 167,
createdAt: '2025-07-10T13:30:00Z',
updatedAt: '2025-07-10T13:30:00Z',
isLiked: false,
isFavorited: false
}
];
export const mockComments: Comment[] = [
{
id: 'comment1',
content: 'AutoBeeAgent的配置规则讲解得很详细特别是对话流程设计这部分对我们团队帮助很大',
author: {
id: 'user7',
name: '技术爱好者',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=7'
},
likes: 15,
replies: [
{
id: 'reply1',
content: '是的,知识库管理的最佳实践也很实用,我们正在按照这个指南优化我们的知识体系',
author: {
id: 'user8',
name: '产品经理',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=8'
},
likes: 8,
replies: [],
createdAt: '2025-09-05T12:30:00Z',
isLiked: false
}
],
createdAt: '2025-09-05T11:45:00Z',
isLiked: false
},
{
id: 'comment2',
content: 'RPA入门教程写得很全面我们公司正准备引入RPA技术这篇文章给了我们很好的指导。',
author: {
id: 'user9',
name: '企业IT负责人',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=9'
},
likes: 12,
replies: [],
createdAt: '2025-09-09T15:20:00Z',
isLiked: false
},
{
id: 'comment3',
content: 'RPA工具对比分析很有价值帮助我们快速了解了各平台的优劣势选型决策更有依据了。',
author: {
id: 'user10',
name: 'RPA新手',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=10'
},
likes: 25,
replies: [],
createdAt: '2025-06-30T10:30:00Z',
isLiked: false
}
];
export const mockUser: User = {
id: 'current_user',
name: 'Itcat666666',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=current',
email: 'user@example.com',
role: 'user'
};
// 分类选项
export const categories = [
{ label: '全部', value: 'all' },
{ label: '使用规则', value: '使用规则' },
{ label: 'RPA知识', value: 'RPA知识' },
{ label: '最佳实践', value: '最佳实践' },
{ label: '技术教程', value: '技术教程' },
{ label: '案例分析', value: '案例分析' },
{ label: '工具推荐', value: '工具推荐' }
];
// 排序选项
export const sortOptions = [
{ label: '最热', value: 'hot' },
{ label: '最新', value: 'latest' },
{ label: '最多评论', value: 'comments' },
{ label: '最多点赞', value: 'likes' }
];

3
tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./src/.umi/tsconfig.json"
}

1
typings.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'umi/typings';