Skip to content

Commit 70d34a8

Browse files
authored
Merge pull request #9 from JSREP/dev
feat(challenge-contribute): 恢复和优化挑战贡献页面组件
2 parents c9897e1 + e1a49c4 commit 70d34a8

File tree

8 files changed

+386
-151
lines changed

8 files changed

+386
-151
lines changed

README.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
# LeetCode 爬虫挑战
22

33
[![部署GitHub Pages](https://github.com/JSREP/crawler-leetcode/actions/workflows/deploy-github-pages.yml/badge.svg)](https://github.com/JSREP/crawler-leetcode/actions/workflows/deploy-github-pages.yml)
4-
[![部署Vercel](https://vercelbadge.vercel.app/api/jsrep/crawler-leetcode)](https://crawler-leetcode.vercel.app/)
54

6-
这个仓库收集了各种网站的爬虫挑战案例,展示了不同类型的反爬虫技术和解决方案。项目使用React+TypeScript开发,同时部署在GitHub Pages和Vercel平台上
5+
这个仓库收集了各种网站的爬虫挑战案例,展示了不同类型的反爬虫技术和解决方案。项目使用React+TypeScript开发,通过GitHub Pages进行部署
76

8-
**在线访问**:
9-
- GitHub Pages (需要VPN): [https://jsrep.github.io/crawler-leetcode/](https://jsrep.github.io/crawler-leetcode/)
10-
- Vercel (国内直接访问): [https://crawler-leetcode.vercel.app/](https://crawler-leetcode.vercel.app/)
7+
**在线访问**: [https://jsrep.github.io/crawler-leetcode/](https://jsrep.github.io/crawler-leetcode/) (需要VPN访问)
118

129
![image-20250413185708120](./README.assets/image-20250413185708120.png)
1310

src/components/ChallengeDetailPage/ChallengeDescription.tsx

+7-24
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const MarkdownImage = (props: any) => {
2525
// 使用传入的src或回退到默认图片
2626
const imageSrc = src || FALLBACK_IMAGE;
2727

28-
// 使用Ant Design的Image组件,支持点击预览
28+
// 使用Ant Design的Image组件,支持点击预览但无蒙版和提示文本
2929
return (
3030
<Image
3131
src={imageSrc}
@@ -37,14 +37,8 @@ const MarkdownImage = (props: any) => {
3737
display: 'block',
3838
}}
3939
preview={{
40-
mask: <div className="image-preview-mask">点击查看大图</div>,
41-
maskClassName: "image-preview-mask",
42-
rootClassName: "custom-image-preview",
43-
toolbarRender: () => (
44-
<div className="image-preview-tip">
45-
点击图片外区域关闭 | 滚轮缩放 | 左键拖动
46-
</div>
47-
)
40+
mask: false,
41+
rootClassName: "custom-image-preview"
4842
}}
4943
fallback={FALLBACK_IMAGE}
5044
/>
@@ -246,13 +240,8 @@ const ChallengeDescription: React.FC<ChallengeDescriptionProps> = ({ challenge,
246240
display: 'block'
247241
}}
248242
preview={{
249-
mask: '点击查看大图',
250-
maskClassName: 'image-preview-mask',
251-
toolbarRender: () => (
252-
<div className="image-preview-tip">
253-
点击图片外区域关闭 | 滚轮缩放 | 左键拖动
254-
</div>
255-
),
243+
mask: false,
244+
rootClassName: "custom-image-preview"
256245
}}
257246
fallback={FALLBACK_IMAGE}
258247
/>
@@ -289,14 +278,8 @@ const ChallengeDescription: React.FC<ChallengeDescriptionProps> = ({ challenge,
289278
display: 'block'
290279
}}
291280
preview={{
292-
mask: <div className="image-preview-mask">点击查看大图</div>,
293-
maskClassName: "image-preview-mask",
294-
rootClassName: "custom-image-preview",
295-
toolbarRender: () => (
296-
<div className="image-preview-tip">
297-
点击图片外区域关闭 | 滚轮缩放 | 左键拖动
298-
</div>
299-
)
281+
mask: false,
282+
rootClassName: "custom-image-preview"
300283
}}
301284
fallback={FALLBACK_IMAGE}
302285
/>

src/components/ChallengeListPage/ChallengeControls.tsx

+121-89
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Select, Divider, Space, Dropdown, Button, Menu, Checkbox, Input } from 'antd';
22
import { SortAscendingOutlined, SortDescendingOutlined, TagOutlined, FilterOutlined, DownOutlined, SearchOutlined } from '@ant-design/icons';
33
import { useTranslation } from 'react-i18next';
4-
import { useState, ChangeEvent } from 'react';
4+
import { useState, ChangeEvent, useRef, useEffect } from 'react';
55
import StarRating from '../StarRating';
66
import { useMediaQuery } from 'react-responsive';
77

@@ -90,10 +90,20 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
9090
}) => {
9191
const { t } = useTranslation();
9292
const isMobile = useMediaQuery({ maxWidth: 768 });
93+
const containerRef = useRef<HTMLDivElement>(null);
9394

9495
// 在组件的开头部分添加状态
9596
const [tagSearchText, setTagSearchText] = useState('');
9697

98+
// 确保containerRef挂载后才渲染Dropdown
99+
const [isContainerMounted, setIsContainerMounted] = useState(false);
100+
101+
useEffect(() => {
102+
if (containerRef.current) {
103+
setIsContainerMounted(true);
104+
}
105+
}, []);
106+
97107
// 排序功能菜单
98108
const sortMenu = (
99109
<Menu
@@ -154,13 +164,14 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
154164
maxHeight: isMobile ? '300px' : '400px',
155165
overflowY: 'auto',
156166
minWidth: isMobile ? '250px' : '300px',
157-
maxWidth: isMobile ? '80vw' : 'none',
167+
maxWidth: isMobile ? '80vw' : '400px',
158168
backgroundColor: '#fff',
159169
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
160170
borderRadius: '4px',
161171
border: '1px solid #f0f0f0',
162-
position: 'relative',
163-
zIndex: 1050
172+
zIndex: 1050,
173+
transform: 'translateZ(0)', // 开启硬件加速
174+
willChange: 'transform, opacity' // 提示浏览器优化渲染
164175
}}>
165176
{/* 添加标签搜索框 */}
166177
<Input
@@ -171,6 +182,7 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
171182
setTagSearchText(e.target.value);
172183
}}
173184
allowClear
185+
autoFocus={false} // 防止自动聚焦导致的布局计算问题
174186
/>
175187

176188
{/* 标签列表,使用Grid布局优化显示 */}
@@ -179,9 +191,10 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
179191
value={selectedTags}
180192
onChange={tags => onTagsChange(tags as string[])}
181193
style={{
182-
display: 'flex',
183-
flexWrap: 'wrap',
184-
width: '100%'
194+
display: 'grid',
195+
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
196+
width: '100%',
197+
gap: '6px'
185198
}}
186199
>
187200
{allTags
@@ -191,8 +204,8 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
191204
key={tag}
192205
value={tag}
193206
style={{
194-
marginRight: '12px',
195-
marginBottom: '6px',
207+
marginRight: 0,
208+
marginBottom: 0,
196209
width: 'auto',
197210
display: 'inline-flex',
198211
alignItems: 'center',
@@ -209,90 +222,109 @@ const ChallengeControls: React.FC<ChallengeControlsProps> = ({
209222
);
210223

211224
return (
212-
<Space
213-
split={isMobile ? null : <Divider type="vertical" />}
214-
style={{ marginBottom: isMobile ? 12 : 20 }}
215-
direction={isMobile ? "vertical" : "horizontal"}
216-
size={isMobile ? 8 : "middle"}
217-
wrap={!isMobile}
218-
>
219-
{/* 标签过滤 */}
220-
<Dropdown
221-
overlay={tagMenu}
222-
trigger={['click']}
223-
placement={isMobile ? "bottomCenter" : "bottomLeft"}
224-
overlayStyle={{
225-
position: 'fixed',
226-
marginTop: '8px',
227-
zIndex: 1050
228-
}}
225+
<div ref={containerRef} className="challenge-controls-container" style={{ position: 'relative' }}>
226+
<Space
227+
split={isMobile ? null : <Divider type="vertical" />}
228+
style={{ marginBottom: isMobile ? 12 : 20 }}
229+
direction={isMobile ? "vertical" : "horizontal"}
230+
size={isMobile ? 8 : "middle"}
231+
wrap={!isMobile}
229232
>
230-
<Button
231-
icon={<TagOutlined />}
232-
size={isMobile ? "middle" : "default"}
233-
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
234-
>
235-
{t('challenges.filters.tags')} {selectedTags.length > 0 && `(${selectedTags.length})`} <DownOutlined />
236-
</Button>
237-
</Dropdown>
238-
239-
{/* 难度过滤 */}
240-
<Dropdown
241-
overlay={difficultyMenu}
242-
trigger={['click']}
243-
placement={isMobile ? "bottomCenter" : "bottomLeft"}
244-
>
245-
<Button
246-
icon={<FilterOutlined />}
247-
size={isMobile ? "middle" : "default"}
248-
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
249-
>
250-
{t('challenges.filters.difficulty')} <DownOutlined />
251-
</Button>
252-
</Dropdown>
253-
254-
{/* 平台过滤 */}
255-
<Dropdown
256-
overlay={platformMenu}
257-
trigger={['click']}
258-
placement={isMobile ? "bottomCenter" : "bottomLeft"}
259-
>
260-
<Button
261-
icon={<FilterOutlined />}
262-
size={isMobile ? "middle" : "default"}
263-
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
264-
>
265-
{t('challenges.controls.platform')} <DownOutlined />
266-
</Button>
267-
</Dropdown>
268-
269-
{/* 排序控制 - 移到最后 */}
270-
<Space style={{ width: isMobile ? '100%' : 'auto' }}>
271-
<Dropdown
272-
overlay={sortMenu}
273-
trigger={['click']}
274-
placement={isMobile ? "bottomCenter" : "bottomLeft"}
275-
>
276-
<Button
277-
size={isMobile ? "middle" : "default"}
278-
style={{
279-
width: isMobile ? 'calc(100% - 32px)' : 'auto',
280-
justifyContent: 'space-between',
281-
display: 'flex',
282-
alignItems: 'center'
233+
{/* 标签过滤 */}
234+
{isContainerMounted && (
235+
<Dropdown
236+
overlay={tagMenu}
237+
trigger={['click']}
238+
placement={isMobile ? "bottomCenter" : "bottomLeft"}
239+
overlayStyle={{
240+
marginTop: '8px',
241+
zIndex: 1050
283242
}}
243+
getPopupContainer={() => containerRef.current || document.body}
244+
destroyPopupOnHide={true}
245+
mouseEnterDelay={0.1}
246+
mouseLeaveDelay={0.1}
247+
>
248+
<Button
249+
icon={<TagOutlined />}
250+
size={isMobile ? "middle" : "default"}
251+
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
252+
>
253+
{t('challenges.filters.tags')} {selectedTags.length > 0 && `(${selectedTags.length})`} <DownOutlined />
254+
</Button>
255+
</Dropdown>
256+
)}
257+
258+
{/* 难度过滤 */}
259+
{isContainerMounted && (
260+
<Dropdown
261+
overlay={difficultyMenu}
262+
trigger={['click']}
263+
placement={isMobile ? "bottomCenter" : "bottomLeft"}
264+
getPopupContainer={() => containerRef.current || document.body}
265+
destroyPopupOnHide={true}
266+
>
267+
<Button
268+
icon={<FilterOutlined />}
269+
size={isMobile ? "middle" : "default"}
270+
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
271+
>
272+
{t('challenges.filters.difficulty')} <DownOutlined />
273+
</Button>
274+
</Dropdown>
275+
)}
276+
277+
{/* 平台过滤 */}
278+
{isContainerMounted && (
279+
<Dropdown
280+
overlay={platformMenu}
281+
trigger={['click']}
282+
placement={isMobile ? "bottomCenter" : "bottomLeft"}
283+
getPopupContainer={() => containerRef.current || document.body}
284+
destroyPopupOnHide={true}
284285
>
285-
{isMobile ? t(`challenges.sort.${sortBy}`) : `${t('challenges.controls.sortBy')}: ${t(`challenges.sort.${sortBy}`)}`} <DownOutlined />
286-
</Button>
287-
</Dropdown>
288-
<Button
289-
icon={sortOrder === 'asc' ? <SortAscendingOutlined /> : <SortDescendingOutlined />}
290-
onClick={onSortOrderChange}
291-
title={sortOrder === 'asc' ? t('challenges.controls.ascending') : t('challenges.controls.descending')}
292-
size={isMobile ? "middle" : "default"}
293-
/>
286+
<Button
287+
icon={<FilterOutlined />}
288+
size={isMobile ? "middle" : "default"}
289+
style={{ width: isMobile ? '100%' : 'auto', justifyContent: 'space-between', display: 'flex', alignItems: 'center' }}
290+
>
291+
{t('challenges.controls.platform')} <DownOutlined />
292+
</Button>
293+
</Dropdown>
294+
)}
295+
296+
{/* 排序控制 - 移到最后 */}
297+
<Space style={{ width: isMobile ? '100%' : 'auto' }}>
298+
{isContainerMounted && (
299+
<Dropdown
300+
overlay={sortMenu}
301+
trigger={['click']}
302+
placement={isMobile ? "bottomCenter" : "bottomLeft"}
303+
getPopupContainer={() => containerRef.current || document.body}
304+
destroyPopupOnHide={true}
305+
>
306+
<Button
307+
size={isMobile ? "middle" : "default"}
308+
style={{
309+
width: isMobile ? 'calc(100% - 32px)' : 'auto',
310+
justifyContent: 'space-between',
311+
display: 'flex',
312+
alignItems: 'center'
313+
}}
314+
>
315+
{isMobile ? t(`challenges.sort.${sortBy}`) : `${t('challenges.controls.sortBy')}: ${t(`challenges.sort.${sortBy}`)}`} <DownOutlined />
316+
</Button>
317+
</Dropdown>
318+
)}
319+
<Button
320+
icon={sortOrder === 'asc' ? <SortAscendingOutlined /> : <SortDescendingOutlined />}
321+
onClick={onSortOrderChange}
322+
title={sortOrder === 'asc' ? t('challenges.controls.ascending') : t('challenges.controls.descending')}
323+
size={isMobile ? "middle" : "default"}
324+
/>
325+
</Space>
294326
</Space>
295-
</Space>
327+
</div>
296328
);
297329
};
298330

0 commit comments

Comments
 (0)