Skip to content

Commit 3e0f9a1

Browse files
authored
Merge pull request #16 from juwenzhang/feat/comment-system-upgrade
Feat/comment system upgrade
2 parents 7db0816 + 04e20a2 commit 3e0f9a1

File tree

31 files changed

+1216
-135
lines changed

31 files changed

+1216
-135
lines changed

.dev-registry.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
{ "version": 1, "apps": {}, "updatedAt": 0 }
1+
{
2+
"version": 1,
3+
"apps": {
4+
"feed": {
5+
"name": "feed",
6+
"url": "http://localhost:5174",
7+
"preferredPort": 5174,
8+
"resolvedPort": 5174,
9+
"startedAt": 1775478613170,
10+
"pid": 30090
11+
},
12+
"user-profile": {
13+
"name": "user-profile",
14+
"url": "http://localhost:5175",
15+
"preferredPort": 5175,
16+
"resolvedPort": 5175,
17+
"startedAt": 1775478613306,
18+
"pid": 30112
19+
}
20+
},
21+
"updatedAt": 1775478613306
22+
}

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ dev-frontend-main: ## 只启动主应用 (main)
164164
dev-frontend-feed: ## 只启动 feed 子应用
165165
pnpm dev:feed
166166

167-
build-frontend: ## 构建前端所有包和应用
168-
pnpm -r build
167+
build-frontend: ## 构建前端所有包和应用 (排除 demo)
168+
pnpm -r --filter '!./demo/**' build
169169

170170
build-preview: ## 构建前端并组装 preview 目录 (子应用 → main/dist/apps/)
171171
@bash scripts/build-preview.sh
@@ -244,10 +244,10 @@ clean: ## 清理构建产物 (保留 node_modules)
244244
@echo "✅ Clean complete"
245245

246246
clean-all: clean ## 深度清理 (含 node_modules + 杀端口进程, 需要重新 install)
247-
@echo "🧹 Killing processes on project ports (8000,50051,50052,50053,5173,5174,4173)..."
248-
@-lsof -ti:8000,50051,50052,50053,5173,5174,4173 2>/dev/null | xargs kill 2>/dev/null; true
247+
@echo "🧹 Killing processes on project ports (8000,50051-50053,5173-5175,4173)..."
248+
@-lsof -ti:8000,50051,50052,50053,5173,5174,5175,4173 2>/dev/null | xargs kill 2>/dev/null; true
249249
@echo "🧹 Deep cleaning..."
250-
rm -rf node_modules apps/*/node_modules packages/*/node_modules
250+
rm -rf node_modules apps/*/node_modules packages/*/node_modules demo/*/node_modules
251251
@echo "✅ Deep clean complete. Run 'make install' to reinstall."
252252

253253
# ------------------------------------------------------------

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
))}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Skeleton } from 'antd';
2+
3+
interface CommentSkeletonProps {
4+
count?: number;
5+
}
6+
7+
export default function CommentSkeleton({ count = 3 }: CommentSkeletonProps) {
8+
return (
9+
<>
10+
{Array.from({ length: count }, (_, i) => (
11+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton 顺序固定
12+
<div key={i} style={{ display: 'flex', gap: 12, padding: '14px 0' }}>
13+
<Skeleton.Avatar active size={36} />
14+
<div style={{ flex: 1 }}>
15+
<Skeleton
16+
active
17+
title={{ width: '30%' }}
18+
paragraph={{ rows: 2, width: ['80%', '50%'] }}
19+
/>
20+
</div>
21+
</div>
22+
))}
23+
</>
24+
);
25+
}

apps/main/src/components/CommentSection/commentSection.module.less

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,43 @@
55
padding-top: 24px;
66
border-top: 1px solid var(--color-border);
77

8-
.sectionTitle {
9-
font-size: 18px;
10-
font-weight: 600;
11-
color: var(--color-text-1);
8+
.sectionHeader {
9+
@apply flex items-center justify-between;
1210
margin-bottom: 20px;
11+
12+
.sectionTitle {
13+
font-size: 18px;
14+
font-weight: 600;
15+
color: var(--color-text-1);
16+
margin: 0;
17+
}
18+
19+
.sortTabs {
20+
@apply flex items-center gap-1;
21+
22+
.sortTab {
23+
@apply flex items-center gap-1;
24+
padding: 4px 12px;
25+
border: none;
26+
border-radius: var(--radius-sm);
27+
background: none;
28+
font-size: 13px;
29+
color: var(--color-text-3);
30+
cursor: pointer;
31+
transition: all 0.2s;
32+
33+
&:hover {
34+
color: var(--color-text-1);
35+
background: var(--color-bg-hover);
36+
}
37+
38+
&.active {
39+
color: var(--color-primary);
40+
background: var(--color-primary-light, rgba(30, 128, 255, 0.08));
41+
font-weight: 500;
42+
}
43+
}
44+
}
1345
}
1446
}
1547

@@ -164,6 +196,13 @@
164196

165197
// 评论列表
166198
.commentList {
199+
transition: opacity 0.2s ease;
200+
201+
&.refreshing {
202+
opacity: 0.5;
203+
pointer-events: none;
204+
}
205+
167206
.commentThread {
168207
&:not(:last-child) {
169208
margin-bottom: 4px;
@@ -319,6 +358,30 @@
319358
margin-left: 48px;
320359
padding-left: 16px;
321360
border-left: 2px solid var(--color-border);
361+
362+
.repliesInner {
363+
overflow: hidden;
364+
transition: max-height 0.3s ease, opacity 0.3s ease;
365+
366+
&.expanded {
367+
max-height: none;
368+
}
369+
}
370+
371+
.toggleRepliesBtn {
372+
@apply flex items-center gap-1;
373+
border: none;
374+
background: none;
375+
font-size: 12px;
376+
color: var(--color-primary);
377+
cursor: pointer;
378+
padding: 8px 0 4px;
379+
transition: color 0.2s;
380+
381+
&:hover {
382+
color: var(--color-primary-hover, #4d9fff);
383+
}
384+
}
322385
}
323386

324387
.loading,
@@ -328,3 +391,14 @@
328391
color: var(--color-text-4);
329392
font-size: 14px;
330393
}
394+
395+
.sentinel {
396+
min-height: 1px;
397+
}
398+
399+
.loadingMore {
400+
@apply flex items-center justify-center gap-2;
401+
padding: 16px 0;
402+
color: var(--color-text-4);
403+
font-size: 13px;
404+
}

0 commit comments

Comments
 (0)