@@ -6,6 +6,205 @@ import got from '@/utils/got';
66import { parseDate , parseRelativeDate } from '@/utils/parse-date' ;
77import timezone from '@/utils/timezone' ;
88
9+ export function extractNextData < T = unknown > ( html : string , url ?: string ) : T {
10+ const scriptMatch = html . match ( / < s c r i p t i d = " _ _ N E X T _ D A T A _ _ " t y p e = " a p p l i c a t i o n \/ j s o n " > ( .* ?) < \/ s c r i p t > / ) ;
11+ if ( ! scriptMatch || ! scriptMatch [ 1 ] ) {
12+ throw new Error ( `Failed to find __NEXT_DATA__ script tag in page${ url ? `: ${ url } ` : '' } ` ) ;
13+ }
14+
15+ try {
16+ return JSON . parse ( scriptMatch [ 1 ] ) as T ;
17+ } catch ( error ) {
18+ throw new Error ( `Failed to parse __NEXT_DATA__ JSON: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
19+ }
20+ }
21+
22+ interface ThreadNextData {
23+ props : {
24+ pageProps : {
25+ threadData : {
26+ data : {
27+ moduleConfigList : {
28+ content : {
29+ moduleContent : {
30+ content : string ;
31+ } ;
32+ } ;
33+ } ;
34+ } ;
35+ } ;
36+ } ;
37+ } ;
38+ }
39+
40+ interface MatchContent {
41+ team ?: string ;
42+ url ?: string ;
43+ matchId ?: string ;
44+ type ?: string ;
45+ }
46+
47+ function hexToRgb ( hex : string ) : string {
48+ const result = / ^ # ? ( [ a - f \d ] { 2 } ) ( [ a - f \d ] { 2 } ) ( [ a - f \d ] { 2 } ) $ / i. exec ( hex ) ;
49+ if ( result ) {
50+ const r = Number . parseInt ( result [ 1 ] , 16 ) ;
51+ const g = Number . parseInt ( result [ 2 ] , 16 ) ;
52+ const b = Number . parseInt ( result [ 3 ] , 16 ) ;
53+ return `rgb(${ r } , ${ g } , ${ b } )` ;
54+ }
55+ return hex ;
56+ }
57+
58+ interface PlayerStats {
59+ playerId : string ;
60+ playerName : string ;
61+ alias : string ;
62+ photo : string ;
63+ mins : number ;
64+ pts : number ;
65+ reb : number ;
66+ asts : number ;
67+ stl : number ;
68+ blk : number ;
69+ nfg : string ;
70+ fgp : string ;
71+ threePoints : string ;
72+ tpp : string ;
73+ ft : string ;
74+ ftp : string ;
75+ to : number ;
76+ oreb : number ;
77+ dreb : number ;
78+ blkr : number ;
79+ pf : number ;
80+ foulr : number ;
81+ plusMinus : string ;
82+ }
83+
84+ interface TeamPlayerStats {
85+ teamName : string ;
86+ teamColor : string ;
87+ start : PlayerStats [ ] ;
88+ reserve : PlayerStats [ ] ;
89+ dnpPlayerList : string [ ] ;
90+ }
91+
92+ interface MatchStats {
93+ firstTeam : {
94+ teamName : string ;
95+ totalScore : number ;
96+ section : number [ ] ;
97+ } ;
98+ secondTeam : {
99+ teamName : string ;
100+ totalScore : number ;
101+ section : number [ ] ;
102+ } ;
103+ }
104+
105+ interface TeamStats {
106+ leftTeam : {
107+ teamName : string ;
108+ teamColor : string ;
109+ pts : number ;
110+ reb : number ;
111+ asts : number ;
112+ stl : number ;
113+ blk : number ;
114+ } ;
115+ rightTeam : {
116+ teamName : string ;
117+ teamColor : string ;
118+ pts : number ;
119+ reb : number ;
120+ asts : number ;
121+ stl : number ;
122+ blk : number ;
123+ } ;
124+ }
125+
126+ interface GameStatusResult {
127+ playerStats : {
128+ first : TeamPlayerStats ;
129+ second : TeamPlayerStats ;
130+ } ;
131+ matchStats : MatchStats ;
132+ teamStats : TeamStats ;
133+ playerVertical : string [ ] [ ] ;
134+ teamVertical : string [ ] [ ] ;
135+ }
136+
137+ function generatePlayerRow ( player : PlayerStats , isStart : boolean ) : string {
138+ return `<tr><td><div class="body-cell cell${ isStart ? ' start' : '' } "><span style="padding-left: 15px">${ player . alias } </span></div></td></tr>` ;
139+ }
140+
141+ function generatePlayerDataRow ( player : PlayerStats ) : string {
142+ const plusMinus = player . plusMinus . startsWith ( '-' ) ? player . plusMinus : `+${ player . plusMinus } ` ;
143+ return `<tr><td><div class="body-cell cell">${ player . mins } </div></td><td><div class="body-cell cell">${ player . pts } </div></td><td><div class="body-cell cell">${ player . reb } </div></td><td><div class="body-cell cell">${ player . asts } </div></td><td><div class="body-cell cell">${ player . stl } </div></td><td><div class="body-cell cell">${ player . blk } </div></td><td><div class="body-cell cell">${ player . nfg } </div></td><td><div class="body-cell cell">${ player . fgp } </div></td><td><div class="body-cell cell">${ player . threePoints } </div></td><td><div class="body-cell cell">${ player . tpp } </div></td><td><div class="body-cell cell">${ player . ft } </div></td><td><div class="body-cell cell">${ player . ftp } </div></td><td><div class="body-cell cell">${ player . to } </div></td><td><div class="body-cell cell">${ player . oreb } </div></td><td><div class="body-cell cell">${ player . dreb } </div></td><td><div class="body-cell cell">${ player . blkr } </div></td><td><div class="body-cell cell">${ player . pf } </div></td><td><div class="body-cell cell">${ player . foulr } </div></td><td><div class="body-cell cell">${ plusMinus } </div></td></tr>` ;
144+ }
145+
146+ function generateTeamPlayerTable ( team : TeamPlayerStats ) : string {
147+ const allPlayers = [ ...team . start , ...team . reserve ] ;
148+ const playerNameRows = allPlayers . map ( ( player , index ) => generatePlayerRow ( player , index < team . start . length ) ) . join ( '' ) ;
149+ const playerDataRows = allPlayers . map ( ( player ) => generatePlayerDataRow ( player ) ) . join ( '' ) ;
150+ const teamColor = hexToRgb ( team . teamColor ) ;
151+
152+ const headerRow = `<tr><td><div class="body-cell cell">时间</div></td><td><div class="body-cell cell">得分</div></td><td><div class="body-cell cell">篮板</div></td><td><div class="body-cell cell">助攻</div></td><td><div class="body-cell cell">抢断</div></td><td><div class="body-cell cell">盖帽</div></td><td><div class="body-cell cell">投篮</div></td><td><div class="body-cell cell">投篮%</div></td><td><div class="body-cell cell">三分</div></td><td><div class="body-cell cell">三分%</div></td><td><div class="body-cell cell">罚球</div></td><td><div class="body-cell cell">罚球%</div></td><td><div class="body-cell cell">失误</div></td><td><div class="body-cell cell">前板</div></td><td><div class="body-cell cell">后板</div></td><td><div class="body-cell cell">被盖</div></td><td><div class="body-cell cell">犯规</div></td><td><div class="body-cell cell">被犯</div></td><td><div class="body-cell cell">+/-</div></td></tr>` ;
153+
154+ return `<div class="match-player-data"><div class="table-wrap-views"><div class="table-views table-views-body"><div class="table-body-left"><table cellspacing="0" cellpadding="0"><tbody><tr><td><div class="body-cell cell team" style="border-color: ${ teamColor } "><span style="padding-left: 15px">${ team . teamName } </span></div></td></tr>${ playerNameRows } </tbody></table></div><div class="table-body-right"><table cellspacing="0" cellpadding="0"><tbody>${ headerRow } ${ playerDataRows } </tbody></table></div></div></div></div>${ team . dnpPlayerList . length > 0 ? `<div class="not-play mlr-15"><span>未出场队员:</span>${ team . dnpPlayerList . join ( '、' ) } </div>` : '' } ` ;
155+ }
156+
157+ function generateTeamCompareItem ( leftValue : number , rightValue : number , label : string , leftColor : string ) : string {
158+ const total = leftValue + rightValue ;
159+ const leftPercent = total > 0 ? ( leftValue / total ) * 100 : 50 ;
160+ const rightPercent = total > 0 ? ( rightValue / total ) * 100 : 50 ;
161+
162+ return `<div class="team-compare"><div class="team-item-data"><span class="left">${ leftValue } </span><span class="center">${ label } </span><span class="right">${ rightValue } </span></div><div class="compare-item"><div class="item left"><span class="" style="width: ${ leftPercent . toFixed ( 4 ) } %; background-color: ${ leftColor } "></span></div><div class="item right"><span class="gray" style="width: ${ rightPercent . toFixed ( 4 ) } %"></span></div></div></div>` ;
163+ }
164+
165+ export function generateGameStatusHtml ( result : GameStatusResult ) : string {
166+ const { playerStats, matchStats, teamStats } = result ;
167+
168+ // Player data section
169+ const playerDataHtml = `<div class="post-player"><div class="post-title mlr-15">球员数据<span class="tips">可横滑查看更多数据</span></div>${ generateTeamPlayerTable ( playerStats . first ) } ${ generateTeamPlayerTable ( playerStats . second ) } </div>` ;
170+
171+ // Team score table
172+ const sectionHeaders = matchStats . firstTeam . section . map ( ( _ , i ) => `<td><div class="body-cell cell">${ i + 1 } </div></td>` ) . join ( '' ) ;
173+ const firstTeamSections = matchStats . firstTeam . section . map ( ( s ) => `<td><div class="body-cell cell">${ s } </div></td>` ) . join ( '' ) ;
174+ const secondTeamSections = matchStats . secondTeam . section . map ( ( s ) => `<td><div class="body-cell cell">${ s } </div></td>` ) . join ( '' ) ;
175+
176+ const teamScoreTable = `<div class="match-team-data"><div class="table-wrap-views team"><div class="table-views table-views-body"><div class="table-body-left"><table cellspacing="0" cellpadding="0"><tbody><tr><td><div class="body-cell cell"><span style="padding-left: 15px">球队</span></div></td></tr><tr><td><div class="body-cell cell"><span style="padding-left: 15px">${ matchStats . firstTeam . teamName } </span></div></td></tr><tr><td><div class="body-cell cell"><span style="padding-left: 15px">${ matchStats . secondTeam . teamName } </span></div></td></tr></tbody></table></div><div class="table-body-right"><table cellspacing="0" cellpadding="0"><tbody><tr><td><div class="body-cell cell">总分</div></td>${ sectionHeaders } </tr><tr><td><div class="body-cell cell">${ matchStats . firstTeam . totalScore } </div></td>${ firstTeamSections } </tr><tr><td><div class="body-cell cell">${ matchStats . secondTeam . totalScore } </div></td>${ secondTeamSections } </tr></tbody></table></div></div></div>` ;
177+
178+ // Team comparison
179+ const leftTeam = teamStats . leftTeam ;
180+ const rightTeam = teamStats . rightTeam ;
181+ const leftColor = hexToRgb ( leftTeam . teamColor ) ;
182+ const rightColor = hexToRgb ( rightTeam . teamColor ) ;
183+
184+ const teamCompareHtml = `<div class="team-name"><span style="background-color: ${ leftColor } ">${ leftTeam . teamName } </span><span style="background-color: ${ rightColor } ">${ rightTeam . teamName } </span></div><div class="match-team-compare">${ generateTeamCompareItem ( leftTeam . pts , rightTeam . pts , '得分' , leftColor ) } ${ generateTeamCompareItem ( leftTeam . reb , rightTeam . reb , '篮板' , leftColor ) } ${ generateTeamCompareItem ( leftTeam . asts , rightTeam . asts , '助攻' , leftColor ) } ${ generateTeamCompareItem ( leftTeam . stl , rightTeam . stl , '抢断' , leftColor ) } ${ generateTeamCompareItem ( leftTeam . blk , rightTeam . blk , '封盖' , leftColor ) } </div>` ;
185+
186+ // Team data section
187+ const teamDataHtml = `<div class="post-team"><div class="post-title mlr-15">球队数据<span class="tips">可横滑查看更多数据</span></div>${ teamScoreTable } ${ teamCompareHtml } </div><a href="https://mobile.hupu.com" rel="nofollow"><div class="see-more">进入直播间查看更多数据、视频</div></a>` ;
188+
189+ return `<div class="match-post">${ playerDataHtml } ${ teamDataHtml } </div>` ;
190+ }
191+
192+ export async function getGameStatus ( matchID : string ) : Promise < string | null > {
193+ const res = await got ( {
194+ method : 'get' ,
195+ url : 'https://games.mobileapi.hupu.com/1/7.4.4/basketballapi/matchCompletedAutoPostContent' ,
196+ searchParams : {
197+ matchId : matchID ,
198+ } ,
199+ } ) ;
200+ const data = res . data ;
201+ if ( data ?. result && data . result . playerStats && data . result . matchStats && data . result . teamStats ) {
202+ const html = generateGameStatusHtml ( data . result as GameStatusResult ) ;
203+ return html ;
204+ }
205+ return null ;
206+ }
207+
9208export function getEntryDetails ( item : DataItem ) : Promise < DataItem > {
10209 if ( ! item . link ) {
11210 return Promise . resolve ( item ) ;
@@ -17,7 +216,21 @@ export function getEntryDetails(item: DataItem): Promise<DataItem> {
17216 url : item . link ,
18217 } ) ;
19218
20- const content = load ( detailResponse . data ) ;
219+ const html = detailResponse . data ;
220+ const content = load ( html ) ;
221+
222+ // Extract matchId from __NEXT_DATA__
223+ let matchId : string | null = null ;
224+ try {
225+ const nextData = extractNextData < ThreadNextData > ( html , item . link ) ;
226+ const contentStr = nextData . props ?. pageProps ?. threadData ?. data ?. moduleConfigList ?. content ?. moduleContent ?. content ;
227+ if ( contentStr ) {
228+ const matchContent : MatchContent = JSON . parse ( contentStr ) ;
229+ matchId = matchContent . matchId ?? null ;
230+ }
231+ } catch {
232+ // ignore
233+ }
21234
22235 const author = content ( '.bbs-user-info-name, .bbs-user-wrapper-content-name-span' ) . text ( ) ;
23236 const pubDateString = content ( '.second-line-user-info span:not([class])' ) . text ( ) ;
@@ -69,7 +282,15 @@ export function getEntryDetails(item: DataItem): Promise<DataItem> {
69282 }
70283 }
71284
72- const description = descriptionParts . length > 0 ? descriptionParts . join ( '' ) : undefined ;
285+ let description = descriptionParts . length > 0 ? descriptionParts . join ( '' ) : undefined ;
286+
287+ // 如果有 matchId,获取比赛数据
288+ if ( matchId ) {
289+ const gameStatusHtml = await getGameStatus ( matchId ) ;
290+ if ( gameStatusHtml ) {
291+ description = ( description ?? '' ) + gameStatusHtml ;
292+ }
293+ }
73294
74295 return {
75296 ...item ,
0 commit comments