Skip to content

Commit 678c16c

Browse files
committed
refactor: 优化挑战贡献页面布局,将URL和标签移至基本信息区块
1 parent f35a982 commit 678c16c

26 files changed

+1202
-344
lines changed

docs/challenges/Cloudflare5秒盾.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ challenges:
3131
# 挑战难度评级(整数类型,必填)
3232
# 取值范围: 1-5,1表示最简单,5表示最难
3333
# 前端展示时会转换为星级显示
34-
difficulty-level: 3
34+
difficulty-level: 4
3535

3636
# Markdown格式详细描述(必选)
3737
# 当需要复杂排版时使用

docs/challenges/国家标准全文公开系统.yml

Lines changed: 7 additions & 2 deletions
Large diffs are not rendered by default.

docs/challenges/网易易盾.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ challenges:
3030
# 挑战难度评级(整数类型,必填)
3131
# 取值范围: 1-5,1表示最简单,5表示最难
3232
# 前端展示时会转换为星级显示
33-
difficulty-level: 4
33+
difficulty-level: 3
3434

3535
# Markdown格式详细描述(必选)
3636
# 当需要复杂排版时使用

src/components/ChallengeContributePage/components/Base64UrlInput.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { useState, useEffect, useCallback, useMemo } from 'react';
3-
import { Form, Input, message, Tooltip } from 'antd';
4-
import { InfoCircleOutlined, LinkOutlined } from '@ant-design/icons';
3+
import { Form, Input, message, Tooltip, Button } from 'antd';
4+
import { InfoCircleOutlined, LinkOutlined, ExportOutlined } from '@ant-design/icons';
55
import { FormInstance } from 'antd';
66
import { useBase64UrlEncoder } from '../hooks';
77
import { createUrlValidators } from '../utils/validators';
@@ -98,14 +98,38 @@ const useBase64UrlState = (form: FormInstance, onChange?: (value: string) => voi
9898
}
9999
}, [plaintextUrl, form, ensureBase64Format, onChange]);
100100

101+
// 打开URL
102+
const openUrl = useCallback(() => {
103+
const base64Value = form.getFieldValue('base64Url');
104+
if (!base64Value) {
105+
message.warning('请先输入URL');
106+
return;
107+
}
108+
109+
try {
110+
const url = decodeUrl(base64Value);
111+
// 确保URL有效
112+
if (!url.startsWith('http')) {
113+
throw new Error('无效的URL');
114+
}
115+
116+
// 在新标签页中打开URL
117+
window.open(url, '_blank', 'noopener,noreferrer');
118+
} catch (error) {
119+
console.error('打开URL失败:', error);
120+
message.error('无法打开URL,请检查URL格式');
121+
}
122+
}, [form, decodeUrl]);
123+
101124
return {
102125
plaintextUrl,
103126
isFocused,
104127
initializeFromForm,
105128
handleInputChange,
106129
handleFocus,
107130
handleBlur,
108-
decodeUrl
131+
decodeUrl,
132+
openUrl
109133
};
110134
};
111135

@@ -160,7 +184,8 @@ const Base64UrlInput: React.FC<Base64UrlInputProps> = ({ form, onChange }) => {
160184
handleInputChange,
161185
handleFocus,
162186
handleBlur,
163-
decodeUrl
187+
decodeUrl,
188+
openUrl
164189
} = useBase64UrlState(form, onChange);
165190

166191
// 处理YAML导入事件
@@ -176,12 +201,22 @@ const Base64UrlInput: React.FC<Base64UrlInputProps> = ({ form, onChange }) => {
176201
createUrlValidators(decodeUrl),
177202
[decodeUrl]);
178203

204+
// 自定义后缀图标
205+
const suffixIcon = (
206+
<Tooltip title="在新标签页中打开URL">
207+
<ExportOutlined
208+
onClick={openUrl}
209+
style={{ cursor: 'pointer', color: '#1890ff' }}
210+
/>
211+
</Tooltip>
212+
);
213+
179214
return (
180215
<Form.Item
181216
name="base64Url"
182217
label="目标网站URL"
183218
tooltip={{
184-
title: '输入普通URL,系统会自动进行Base64编码。输入框获得焦点时会显示明文,失去焦点时会显示Base64编码值。',
219+
title: '输入普通URL,系统会自动进行Base64编码。输入框获得焦点时会显示明文,失去焦点时会显示Base64编码值。点击右侧图标可以在新标签页中打开URL。',
185220
icon: <InfoCircleOutlined />
186221
}}
187222
rules={validationRules}
@@ -193,6 +228,7 @@ const Base64UrlInput: React.FC<Base64UrlInputProps> = ({ form, onChange }) => {
193228
onFocus={handleFocus}
194229
onBlur={handleBlur}
195230
prefix={<LinkOutlined />}
231+
suffix={suffixIcon}
196232
/>
197233
</Form.Item>
198234
);

src/components/ChallengeContributePage/components/DifficultySelector.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const DifficultySelector: React.FC<DifficultySelectorProps> = ({ form, value, on
3535
}
3636
};
3737

38+
// 空函数,只用于让星星显示为可点击状态
39+
const handleStarClick = () => {};
40+
3841
return (
3942
<Form.Item
4043
name="difficultyLevel"
@@ -52,31 +55,51 @@ const DifficultySelector: React.FC<DifficultySelectorProps> = ({ form, value, on
5255
<Option value={1}>
5356
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
5457
<span>1 (初级)</span>
55-
<StarRating difficulty={1} />
58+
<StarRating
59+
difficulty={1}
60+
onClick={handleStarClick}
61+
style={{ cursor: 'pointer' }}
62+
/>
5663
</div>
5764
</Option>
5865
<Option value={2}>
5966
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
6067
<span>2 (简单)</span>
61-
<StarRating difficulty={2} />
68+
<StarRating
69+
difficulty={2}
70+
onClick={handleStarClick}
71+
style={{ cursor: 'pointer' }}
72+
/>
6273
</div>
6374
</Option>
6475
<Option value={3}>
6576
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
6677
<span>3 (中等)</span>
67-
<StarRating difficulty={3} />
78+
<StarRating
79+
difficulty={3}
80+
onClick={handleStarClick}
81+
style={{ cursor: 'pointer' }}
82+
/>
6883
</div>
6984
</Option>
7085
<Option value={4}>
7186
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
7287
<span>4 (困难)</span>
73-
<StarRating difficulty={4} />
88+
<StarRating
89+
difficulty={4}
90+
onClick={handleStarClick}
91+
style={{ cursor: 'pointer' }}
92+
/>
7493
</div>
7594
</Option>
7695
<Option value={5}>
7796
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
7897
<span>5 (专家)</span>
79-
<StarRating difficulty={5} />
98+
<StarRating
99+
difficulty={5}
100+
onClick={handleStarClick}
101+
style={{ cursor: 'pointer' }}
102+
/>
80103
</div>
81104
</Option>
82105
</Select>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
import { Button, Tooltip } from 'antd';
3+
import { VerticalAlignTopOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
4+
5+
/**
6+
* 滚动控制按钮组件,用于快速跳转到页面顶部或底部
7+
*/
8+
const ScrollButtons: React.FC = () => {
9+
// 滚动到页面顶部
10+
const scrollToTop = () => {
11+
window.scrollTo({
12+
top: 0,
13+
behavior: 'smooth'
14+
});
15+
};
16+
17+
// 滚动到页面底部
18+
const scrollToBottom = () => {
19+
window.scrollTo({
20+
top: document.documentElement.scrollHeight,
21+
behavior: 'smooth'
22+
});
23+
};
24+
25+
// 按钮样式
26+
const buttonStyle = {
27+
width: '40px',
28+
height: '40px',
29+
borderRadius: '50%',
30+
display: 'flex',
31+
justifyContent: 'center',
32+
alignItems: 'center',
33+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
34+
border: 'none'
35+
};
36+
37+
return (
38+
<div style={{
39+
position: 'fixed',
40+
right: '24px',
41+
bottom: '80px',
42+
zIndex: 9999,
43+
display: 'flex',
44+
flexDirection: 'column',
45+
gap: '12px'
46+
}}>
47+
<Tooltip title="回到顶部" placement="left">
48+
<Button
49+
icon={<VerticalAlignTopOutlined />}
50+
onClick={scrollToTop}
51+
style={buttonStyle}
52+
type="primary"
53+
shape="circle"
54+
/>
55+
</Tooltip>
56+
<Tooltip title="前往底部" placement="left">
57+
<Button
58+
icon={<VerticalAlignBottomOutlined />}
59+
onClick={scrollToBottom}
60+
style={buttonStyle}
61+
type="primary"
62+
shape="circle"
63+
/>
64+
</Tooltip>
65+
</div>
66+
);
67+
};
68+
69+
export default ScrollButtons;

src/components/ChallengeContributePage/components/SolutionsSection.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ const SolutionsSection: React.FC<SolutionsSectionProps> = memo(({ form, onChange
215215
rules={[solutionsValidator]}
216216
validateTrigger={['onChange', 'onBlur']}
217217
style={{ marginBottom: 0 }}
218+
extra="参考资料为可选项,无需强制填写"
219+
required={false}
218220
>
219221
<div style={{ display: 'none' }}></div>
220222
</Form.Item>

src/components/ChallengeContributePage/components/TagsSelector.tsx

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,12 @@ import { PlusOutlined, CloseOutlined } from '@ant-design/icons';
55
import { SectionProps } from '../types';
66
import { useTagsSelector } from '../hooks';
77
import { tagsValidators } from '../utils/validators';
8+
import { TagFrequency } from '../hooks/useAllTags';
89

910
// AutoComplete的选项类型
1011
type OptionType = {
1112
value: string;
1213
label: string | React.ReactNode;
13-
} | {
14-
label: React.ReactNode;
15-
options: Array<{
16-
value: string;
17-
label: string | React.ReactNode;
18-
}>;
1914
};
2015

2116
// 定义颜色映射
@@ -86,12 +81,18 @@ const TagItem = memo(({
8681

8782
interface TagsSelectorProps extends SectionProps {
8883
existingTags?: string[];
84+
tagsFrequency?: TagFrequency[];
8985
}
9086

9187
/**
9288
* 标签选择组件
9389
*/
94-
const TagsSelector: React.FC<TagsSelectorProps> = ({ form, existingTags = [], onChange }) => {
90+
const TagsSelector: React.FC<TagsSelectorProps> = ({
91+
form,
92+
existingTags = [],
93+
tagsFrequency = [],
94+
onChange
95+
}) => {
9596
// 使用标签选择器钩子
9697
const {
9798
tags,
@@ -107,51 +108,28 @@ const TagsSelector: React.FC<TagsSelectorProps> = ({ form, existingTags = [], on
107108
handleInputBlur,
108109
handleTagSelect,
109110
handleTagInputKeyPress
110-
} = useTagsSelector({ form, existingTags, onChange });
111+
} = useTagsSelector({
112+
form,
113+
existingTags,
114+
tagsFrequency,
115+
onChange
116+
});
111117

112-
// 渲染分类标签选项
118+
// 渲染标签选项,仅添加样式,不修改排序
113119
const renderTagOptions = useMemo(() => {
114-
if (!isInputFocused || (newTag && tagOptions.length === 0)) {
120+
if (!isInputFocused || tagOptions.length === 0) {
115121
return undefined;
116122
}
117123

118-
// 如果有搜索词,则只显示过滤后的选项
119-
if (newTag) {
120-
return tagOptions.map(opt => ({
121-
value: opt.value,
122-
label: opt.value
123-
}));
124-
}
124+
console.log('渲染标签选项,数量:', tagOptions.length);
125+
console.log('前5个标签:', tagOptions.slice(0, 5).map(opt => opt.value));
125126

126-
// 没有搜索词时显示分类标签
127-
const options: OptionType[] = [];
128-
129-
// 最近使用的标签
130-
if (recentlyUsedTags.length > 0) {
131-
options.push({
132-
label: <Divider orientation="left" style={{ margin: '4px 0' }}>最近使用</Divider>,
133-
options: recentlyUsedTags.map((tag: string) => ({
134-
value: tag,
135-
label: <span><Tag color={getTagColor(tag)} style={{ margin: 0 }}>{tag}</Tag></span>
136-
}))
137-
});
138-
}
139-
140-
// 分类标签
141-
Object.entries(categorizedTags).forEach(([category, categoryTags]) => {
142-
if (categoryTags.length > 0) {
143-
options.push({
144-
label: <Divider orientation="left" style={{ margin: '4px 0' }}>{category}</Divider>,
145-
options: (categoryTags as string[]).map((tag: string) => ({
146-
value: tag,
147-
label: <span><Tag color={getTagColor(tag)} style={{ margin: 0 }}>{tag}</Tag></span>
148-
}))
149-
});
150-
}
151-
});
152-
153-
return options;
154-
}, [isInputFocused, newTag, tagOptions, recentlyUsedTags, categorizedTags]);
127+
// 映射标签,只添加样式,保持顺序不变
128+
return tagOptions.map(opt => ({
129+
value: opt.value,
130+
label: <span><Tag color={getTagColor(opt.value)} style={{ margin: 0 }}>{opt.value}</Tag></span>
131+
}));
132+
}, [isInputFocused, tagOptions]);
155133

156134
// 渲染已选标签列表
157135
const renderedTags = useMemo(() => (
@@ -194,7 +172,7 @@ const TagsSelector: React.FC<TagsSelectorProps> = ({ form, existingTags = [], on
194172
style={{ width: 300 }}
195173
onKeyDown={handleTagInputKeyPress}
196174
disabled={tags.length >= 100}
197-
open={isInputFocused && (tagOptions.length > 0 || recentlyUsedTags.length > 0)}
175+
open={isInputFocused && tagOptions.length > 0}
198176
notFoundContent={newTag ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配的标签" /> : null}
199177
dropdownStyle={{ maxHeight: '400px', overflow: 'auto' }}
200178
dropdownMatchSelectWidth={true}

0 commit comments

Comments
 (0)