Skip to content

Commit ea1db72

Browse files
committed
feat(main): add favorite count, share button, refreshing transition
- ArticleCard: show favoriteCount, commentCount, share button - ArticleCard: Web Share API fallback to clipboard copy - ArticleActions: add share button with antdMessage feedback - ArticleList: refreshing opacity transition on sort switch - detail page: show commentCount and favoriteCount in meta
1 parent d1c3029 commit ea1db72

File tree

6 files changed

+119
-6
lines changed

6 files changed

+119
-6
lines changed

apps/main/src/components/ArticleActions/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import {
33
EditOutlined,
44
HeartFilled,
55
HeartOutlined,
6+
ShareAltOutlined,
67
StarFilled,
78
StarOutlined,
89
} from '@ant-design/icons';
910
import { Tooltip } from 'antd';
1011
import { useEffect } from 'react';
1112
import { useNavigate } from 'react-router-dom';
13+
import { antdMessage } from '@/lib/antdStatic';
1214
import { useAuthStore } from '@/stores/useAuthStore';
1315
import { useSocialStore } from '@/stores/useSocialStore';
1416
import styles from './articleActions.module.less';
@@ -89,6 +91,25 @@ export default function ArticleActions({ articleId, isAuthor }: ArticleActionsPr
8991
</Tooltip>
9092
)}
9193

94+
<Tooltip title="分享" placement="right">
95+
<button
96+
type="button"
97+
className={styles.actionBtn}
98+
onClick={() => {
99+
const url = `${window.location.origin}/post/${articleId}`;
100+
if (navigator.share) {
101+
navigator.share({ title: document.title, url });
102+
} else {
103+
navigator.clipboard.writeText(url).then(() => {
104+
antdMessage.success('链接已复制到剪贴板');
105+
});
106+
}
107+
}}
108+
>
109+
<ShareAltOutlined />
110+
</button>
111+
</Tooltip>
112+
92113
<div className={styles.divider} />
93114

94115
<Tooltip title="返回首页" placement="right">

apps/main/src/components/ArticleCard/articleCard.module.less

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,23 @@
9999
.date,
100100
.views,
101101
.likes,
102-
.favorites {
102+
.favorites,
103+
.comments {
103104
display: flex;
104105
align-items: center;
105106
gap: 4px;
106107
}
108+
109+
.share {
110+
display: flex;
111+
align-items: center;
112+
cursor: pointer;
113+
transition: color 0.2s;
114+
115+
&:hover {
116+
color: var(--color-primary);
117+
}
118+
}
107119
}
108120
}
109121

apps/main/src/components/ArticleCard/index.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {
22
ClockCircleOutlined,
3+
CommentOutlined,
34
EyeOutlined,
45
HeartOutlined,
6+
ShareAltOutlined,
7+
StarOutlined,
58
TagOutlined,
69
UserOutlined,
710
} from '@ant-design/icons';
@@ -10,6 +13,7 @@ import { ArticleStatus } from '@luhanxin/shared-types';
1013
import { Avatar } from 'antd';
1114
import { useNavigate } from 'react-router-dom';
1215

16+
import { antdMessage } from '@/lib/antdStatic';
1317
import styles from './articleCard.module.less';
1418

1519
/** 去除 Markdown 标记,返回纯文本(用于摘要预览) */
@@ -108,6 +112,48 @@ export default function ArticleCard({ article }: ArticleCardProps) {
108112
{article.likeCount}
109113
</span>
110114
)}
115+
116+
{article.commentCount > 0 && (
117+
<span className={styles.comments}>
118+
<CommentOutlined />
119+
{article.commentCount}
120+
</span>
121+
)}
122+
123+
{article.favoriteCount > 0 && (
124+
<span className={styles.favorites}>
125+
<StarOutlined />
126+
{article.favoriteCount}
127+
</span>
128+
)}
129+
130+
<span
131+
className={styles.share}
132+
onClick={(e) => {
133+
e.stopPropagation();
134+
const url = `${window.location.origin}/post/${article.id}`;
135+
if (navigator.share) {
136+
navigator.share({ title: article.title, url });
137+
} else {
138+
navigator.clipboard.writeText(url).then(() => {
139+
antdMessage.success('链接已复制到剪贴板');
140+
});
141+
}
142+
}}
143+
role="button"
144+
tabIndex={0}
145+
onKeyDown={(e) => {
146+
if (e.key === 'Enter') {
147+
e.stopPropagation();
148+
const url = `${window.location.origin}/post/${article.id}`;
149+
navigator.clipboard.writeText(url).then(() => {
150+
antdMessage.success('链接已复制到剪贴板');
151+
});
152+
}
153+
}}
154+
>
155+
<ShareAltOutlined />
156+
</span>
111157
</div>
112158
</div>
113159

apps/main/src/components/ArticleList/articleList.module.less

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
border: 1px solid var(--color-border);
66
border-radius: var(--radius-sm);
77
overflow: hidden;
8+
transition: opacity 0.2s ease;
9+
10+
&.refreshing {
11+
opacity: 0.5;
12+
pointer-events: none;
13+
}
814
}
915

1016
.skeleton {

apps/main/src/components/ArticleList/index.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default function ArticleList({
3434
}: ArticleListProps) {
3535
const [articles, setArticles] = useState<Article[]>([]);
3636
const [loading, setLoading] = useState(true);
37+
const [refreshing, setRefreshing] = useState(false);
3738
const [loadingMore, setLoadingMore] = useState(false);
3839
const [totalCount, setTotalCount] = useState(0);
3940
const [nextPageToken, setNextPageToken] = useState('');
@@ -49,15 +50,20 @@ export default function ArticleList({
4950
paramsRef.current = { tag, authorId, query, categories, sort, pageSize };
5051

5152
// 初始加载 + 参数变化时重新加载
52-
// const categoriesKey = JSON.stringify(categories);
53+
const hasDataRef = useRef(false);
5354

5455
const fetchList = useCallback(async () => {
55-
// 取消上一次请求
5656
abortRef.current?.abort();
5757
const ctrl = new AbortController();
5858
abortRef.current = ctrl;
5959

60-
setLoading(true);
60+
// 有旧数据时用 refreshing(保留旧内容),无数据时用 loading(骨架屏)
61+
if (hasDataRef.current) {
62+
setRefreshing(true);
63+
} else {
64+
setLoading(true);
65+
}
66+
6167
try {
6268
const res = await articleClient.listArticles(
6369
{
@@ -74,16 +80,19 @@ export default function ArticleList({
7480

7581
const nextToken = res.pagination?.nextPageToken ?? '';
7682
setArticles(res.articles);
83+
hasDataRef.current = res.articles.length > 0;
7784
setTotalCount(res.pagination?.totalCount ?? res.articles.length);
7885
setNextPageToken(nextToken);
7986
setHasMore(nextToken !== '');
8087
} catch {
8188
if (!ctrl.signal.aborted) {
8289
setArticles([]);
90+
hasDataRef.current = false;
8391
}
8492
} finally {
8593
if (!ctrl.signal.aborted) {
8694
setLoading(false);
95+
setRefreshing(false);
8796
}
8897
}
8998
}, [authorId, query, tag, sort, pageSize, categories]);
@@ -165,7 +174,7 @@ export default function ArticleList({
165174
}
166175

167176
return (
168-
<div className={styles.list}>
177+
<div className={`${styles.list} ${refreshing ? styles.refreshing : ''}`}>
169178
{visible.map((article) => (
170179
<ArticleCard key={article.id} article={article} />
171180
))}

apps/main/src/pages/post/pages/detail/index.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ClockCircleOutlined, EyeOutlined, TagOutlined, UserOutlined } from '@ant-design/icons';
1+
import {
2+
ClockCircleOutlined,
3+
CommentOutlined,
4+
EyeOutlined,
5+
StarOutlined,
6+
TagOutlined,
7+
UserOutlined,
8+
} from '@ant-design/icons';
29
import { Avatar, Button, Skeleton } from 'antd';
310
import { useEffect, useRef } from 'react';
411
import { Link, useNavigate, useParams } from 'react-router-dom';
@@ -105,6 +112,18 @@ export default function ArticleDetailPage() {
105112
<EyeOutlined />
106113
{article.viewCount} 阅读
107114
</span>
115+
{article.commentCount > 0 && (
116+
<span className={styles.views}>
117+
<CommentOutlined />
118+
{article.commentCount} 评论
119+
</span>
120+
)}
121+
{article.favoriteCount > 0 && (
122+
<span className={styles.views}>
123+
<StarOutlined />
124+
{article.favoriteCount} 收藏
125+
</span>
126+
)}
108127
</div>
109128

110129
{article.tags.length > 0 && (

0 commit comments

Comments
 (0)