1+ import * as React from 'react' ;
2+ import { useState , useEffect } from 'react' ;
3+ import { Form , Input , Button , Divider , Space , AutoComplete , message } from 'antd' ;
4+ import { PlusOutlined , EditOutlined , DeleteOutlined } from '@ant-design/icons' ;
5+ import { Solution , SectionProps } from '../types' ;
6+
7+ // URL校验正则表达式
8+ const URL_REGEX = / ^ ( h t t p s ? : \/ \/ ) ? ( [ \d a - z . - ] + ) \. ( [ a - z . ] { 2 , 6 } ) ( [ / \w . - ] * ) * \/ ? $ / ;
9+
10+ // 定义常见来源的域名映射
11+ const SOURCE_MAPPING : Record < string , string > = {
12+ 'github.com' : 'GitHub' ,
13+ 'mp.weixin.qq.com' : '微信公众号' ,
14+ 'juejin.cn' : '掘金' ,
15+ 'csdn.net' : 'CSDN' ,
16+ 'zhihu.com' : '知乎' ,
17+ 'segmentfault.com' : 'SegmentFault' ,
18+ 'jianshu.com' : '简书' ,
19+ 'leetcode.cn' : 'LeetCode' ,
20+ 'leetcode.com' : 'LeetCode' ,
21+ 'bilibili.com' : 'B站' ,
22+ 'youtube.com' : 'YouTube' ,
23+ 'blog.csdn.net' : 'CSDN' ,
24+ 'medium.com' : 'Medium' ,
25+ 'dev.to' : 'Dev.to' ,
26+ 'stackoverflow.com' : 'Stack Overflow'
27+ } ;
28+
29+ /**
30+ * 验证URL是否合法
31+ */
32+ const isValidUrl = ( url : string ) : boolean => {
33+ try {
34+ new URL ( url ) ;
35+ return true ;
36+ } catch {
37+ return false ;
38+ }
39+ } ;
40+
41+ /**
42+ * 参考资料部分组件
43+ */
44+ const SolutionsSection : React . FC < SectionProps > = ( { form } ) => {
45+ const [ solutions , setSolutions ] = useState < Solution [ ] > ( [ ] ) ;
46+ const [ editingSolutionIndex , setEditingSolutionIndex ] = useState < number | null > ( null ) ;
47+ const [ authors , setAuthors ] = useState < string [ ] > ( [ ] ) ;
48+ const [ urlError , setUrlError ] = useState < string > ( '' ) ;
49+ const [ newSolution , setNewSolution ] = useState < Solution > ( {
50+ title : '' ,
51+ url : '' ,
52+ source : '' ,
53+ author : ''
54+ } ) ;
55+
56+ // 从所有参考资料中提取作者列表
57+ useEffect ( ( ) => {
58+ const uniqueAuthors = Array . from ( new Set (
59+ solutions
60+ . map ( s => s . author )
61+ . filter ( ( author ) : author is string => author !== undefined && author . trim ( ) !== '' )
62+ ) ) ;
63+ setAuthors ( uniqueAuthors ) ;
64+ } , [ solutions ] ) ;
65+
66+ // 根据URL自动判断来源
67+ const getSourceFromUrl = ( url : string ) : string => {
68+ if ( ! isValidUrl ( url ) ) return '' ;
69+
70+ try {
71+ const urlObj = new URL ( url ) ;
72+ const domain = urlObj . hostname . toLowerCase ( ) ;
73+
74+ // 遍历SOURCE_MAPPING查找匹配的来源
75+ for ( const [ key , value ] of Object . entries ( SOURCE_MAPPING ) ) {
76+ if ( domain . includes ( key ) ) {
77+ return value ;
78+ }
79+ }
80+
81+ // 如果没有匹配的预定义来源,返回域名的首字母大写形式
82+ return domain . split ( '.' ) [ 0 ] . charAt ( 0 ) . toUpperCase ( ) + domain . split ( '.' ) [ 0 ] . slice ( 1 ) ;
83+ } catch {
84+ return '' ;
85+ }
86+ } ;
87+
88+ const handleSolutionChange = ( field : keyof Solution , value : string ) => {
89+ setNewSolution ( prev => {
90+ const updated = { ...prev , [ field ] : value } ;
91+
92+ // 如果更新的是URL字段,验证URL并自动填充source
93+ if ( field === 'url' ) {
94+ if ( value && ! isValidUrl ( value ) ) {
95+ setUrlError ( '请输入有效的URL地址' ) ;
96+ } else {
97+ setUrlError ( '' ) ;
98+ const source = getSourceFromUrl ( value ) ;
99+ if ( source ) {
100+ updated . source = source ;
101+ }
102+ }
103+ }
104+
105+ return updated ;
106+ } ) ;
107+ } ;
108+
109+ const handleAddSolution = ( ) => {
110+ // 验证必填字段
111+ if ( ! newSolution . title ) {
112+ message . error ( '请输入参考资料标题' ) ;
113+ return ;
114+ }
115+
116+ if ( ! newSolution . url ) {
117+ message . error ( '请输入参考资料链接' ) ;
118+ return ;
119+ }
120+
121+ // 验证URL合法性
122+ if ( ! isValidUrl ( newSolution . url ) ) {
123+ message . error ( '请输入有效的URL地址' ) ;
124+ return ;
125+ }
126+
127+ if ( editingSolutionIndex !== null ) {
128+ // 编辑现有参考资料
129+ const updatedSolutions = [ ...solutions ] ;
130+ updatedSolutions [ editingSolutionIndex ] = newSolution ;
131+ setSolutions ( updatedSolutions ) ;
132+ setEditingSolutionIndex ( null ) ;
133+ form . setFieldsValue ( { solutions : updatedSolutions } ) ;
134+ } else {
135+ // 添加新参考资料
136+ const updatedSolutions = [ ...solutions , newSolution ] ;
137+ setSolutions ( updatedSolutions ) ;
138+ form . setFieldsValue ( { solutions : updatedSolutions } ) ;
139+ }
140+
141+ // 重置表单
142+ setNewSolution ( {
143+ title : '' ,
144+ url : '' ,
145+ source : '' ,
146+ author : ''
147+ } ) ;
148+ setUrlError ( '' ) ;
149+ } ;
150+
151+ const handleEditSolution = ( index : number ) => {
152+ setNewSolution ( solutions [ index ] ) ;
153+ setEditingSolutionIndex ( index ) ;
154+ setUrlError ( '' ) ;
155+ } ;
156+
157+ const handleRemoveSolution = ( index : number ) => {
158+ const updatedSolutions = [ ...solutions ] ;
159+ updatedSolutions . splice ( index , 1 ) ;
160+ setSolutions ( updatedSolutions ) ;
161+ form . setFieldsValue ( { solutions : updatedSolutions } ) ;
162+ } ;
163+
164+ const solutionContainerStyle = {
165+ marginBottom : '16px' ,
166+ padding : '8px' ,
167+ border : '1px solid #f0f0f0' ,
168+ borderRadius : '4px' ,
169+ } ;
170+
171+ return (
172+ < >
173+ < Divider orientation = "left" > 参考资料</ Divider >
174+ { solutions . map ( ( solution , index ) => (
175+ < div key = { index } style = { solutionContainerStyle } >
176+ < Space style = { { marginBottom : 8 , width : '100%' , justifyContent : 'space-between' } } >
177+ < div >
178+ < strong > 标题:</ strong > { solution . title }
179+ { solution . source && < > | < strong > 来源:</ strong > { solution . source } </ > }
180+ { solution . author && < > | < strong > 作者:</ strong > { solution . author } </ > }
181+ </ div >
182+ < Space >
183+ < Button icon = { < EditOutlined /> } size = "small" onClick = { ( ) => handleEditSolution ( index ) } >
184+ 编辑
185+ </ Button >
186+ < Button icon = { < DeleteOutlined /> } size = "small" danger onClick = { ( ) => handleRemoveSolution ( index ) } >
187+ 删除
188+ </ Button >
189+ </ Space >
190+ </ Space >
191+ < div >
192+ < a href = { solution . url } target = "_blank" rel = "noopener noreferrer" > { solution . url } </ a >
193+ </ div >
194+ </ div >
195+ ) ) }
196+
197+ < Form layout = "vertical" >
198+ < Form . Item
199+ label = "标题"
200+ required
201+ help = "请输入参考资料的标题"
202+ >
203+ < Input
204+ value = { newSolution . title }
205+ onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => handleSolutionChange ( 'title' , e . target . value ) }
206+ placeholder = "如: 使用正则表达式解析URL"
207+ />
208+ </ Form . Item >
209+
210+ < Form . Item
211+ label = "URL"
212+ required
213+ help = { urlError || "请输入参考资料的链接地址" }
214+ validateStatus = { urlError ? "error" : undefined }
215+ >
216+ < Input
217+ value = { newSolution . url }
218+ onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => handleSolutionChange ( 'url' , e . target . value ) }
219+ placeholder = "如: https://github.com/your-repo"
220+ status = { urlError ? "error" : undefined }
221+ />
222+ </ Form . Item >
223+
224+ < Form . Item
225+ label = "来源"
226+ help = "可选,会根据URL自动填充"
227+ >
228+ < Input
229+ value = { newSolution . source }
230+ onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => handleSolutionChange ( 'source' , e . target . value ) }
231+ placeholder = "如: GitHub"
232+ />
233+ </ Form . Item >
234+
235+ < Form . Item
236+ label = "作者"
237+ help = "可选,支持自动补全已有作者"
238+ >
239+ < AutoComplete
240+ value = { newSolution . author }
241+ onChange = { ( value ) => handleSolutionChange ( 'author' , value ) }
242+ options = { authors . map ( author => ( { value : author } ) ) }
243+ placeholder = "请输入作者名称"
244+ filterOption = { ( inputValue , option ) =>
245+ option ! . value . toUpperCase ( ) . indexOf ( inputValue . toUpperCase ( ) ) !== - 1
246+ }
247+ />
248+ </ Form . Item >
249+
250+ < Button
251+ type = "dashed"
252+ onClick = { handleAddSolution }
253+ icon = { < PlusOutlined /> }
254+ disabled = { ! newSolution . title || ! newSolution . url || ! ! urlError }
255+ >
256+ { editingSolutionIndex !== null ? '更新参考资料' : '添加参考资料' }
257+ </ Button >
258+ { editingSolutionIndex !== null && (
259+ < Button
260+ style = { { marginLeft : 8 } }
261+ onClick = { ( ) => {
262+ setEditingSolutionIndex ( null ) ;
263+ setNewSolution ( {
264+ title : '' ,
265+ url : '' ,
266+ source : '' ,
267+ author : ''
268+ } ) ;
269+ setUrlError ( '' ) ;
270+ } }
271+ >
272+ 取消编辑
273+ </ Button >
274+ ) }
275+ </ Form >
276+
277+ { /* 隐藏的表单字段,用于提交参考资料数据 */ }
278+ < Form . Item name = "solutions" hidden >
279+ < input type = "hidden" />
280+ </ Form . Item >
281+ </ >
282+ ) ;
283+ } ;
284+
285+ export default SolutionsSection ;
0 commit comments