1+ <!doctype html>
2+ < html lang ="zh-CN ">
3+ < head >
4+ < meta charset ="utf-8 ">
5+ < meta name ="viewport " content ="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no ">
6+ < title > 腾讯视频 WebHome</ title >
7+ < style >
8+ * {box-sizing : border-box}body {margin : 0 ;color : # fff ;background : transparent;font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, 'Noto Sans SC' , sans-serif}.app {padding : 18px 18px 30px ;min-height : 100vh ;background : linear-gradient (135deg , rgba (5 , 12 , 25 , .82 ), rgba (9 , 42 , 42 , .72 ))}.top-title {display : flex;align-items : center;justify-content : space-between;margin-bottom : 14px }.brand {display : flex;align-items : center;gap : 10px ;font-size : 22px ;font-weight : 800 }.logo {width : 36px ;height : 36px ;border-radius : 12px ;background : linear-gradient (135deg , # 00f5a0, # 00d9f5 );display : grid;place-items : center;color : # 001 ;font-weight : 900 }.icon-btn , .tab , .card , .ep , .line-tab , .cfg-btn {border : 1px solid rgba (255 , 255 , 255 , .14 );background : rgba (255 , 255 , 255 , .09 );color : # fff ;border-radius : 14px }.icon-btn , .cfg-btn {height : 38px ;padding : 0 14px }.search-row {display : grid;grid-template-columns : auto 1fr auto;gap : 10px ;margin-bottom : 12px }input , textarea {width : 100% ;border : 1px solid rgba (255 , 255 , 255 , .16 );background : rgba (0 , 0 , 0 , .24 );color : # fff ;border-radius : 14px ;padding : 11px 12px ;outline : none}textarea {min-height : 112px ;resize : vertical;font-family : ui-monospace, Consolas, monospace;font-size : 12px }.tabs , .line-tabs {display : flex;gap : 8px ;overflow : auto;padding : 4px 0 12px }.tab , .line-tab {white-space : nowrap;padding : 9px 13px }.tab .active , .line-tab .active {background : linear-gradient (135deg , # 00d9f5, # 00f5a0 );color : # 001 ;font-weight : 800 }.section-head {display : flex;justify-content : space-between;align-items : center;margin : 8px 0 12px }h2 {font-size : 18px ;margin : 0 }.hint {font-size : 12px ;color : rgba (255 , 255 , 255 , .62 )}# content {display : grid;grid-template-columns : repeat (auto-fill, minmax (116px , 1fr ));gap : 12px }.card {overflow : hidden;text-align : left;padding : 0 }.poster {width : 100% ;aspect-ratio : 2 / 3 ;background : rgba (255 , 255 , 255 , .08 );object-fit : cover;display : block}.card-info {padding : 8px }.name {font-size : 14px ;font-weight : 700 ;line-height : 1.28 ;display : -webkit-box;-webkit-line-clamp : 2 ;-webkit-box-orient : vertical;overflow : hidden}.remark {margin-top : 5px ;font-size : 12px ;color : rgba (255 , 255 , 255 , .66 );white-space : nowrap;overflow : hidden;text-overflow : ellipsis}.empty {grid-column : 1 / -1 ;text-align : center;color : rgba (255 , 255 , 255 , .62 );padding : 40px 0 }.sheet , .config-mask {position : fixed;inset : 0 ;background : rgba (0 , 0 , 0 , .48 );display : none;z-index : 20 ;align-items : flex-end}.sheet .show , .config-mask .show {display : flex}.detail-panel , .config-panel {width : 100% ;max-height : 86vh ;overflow : auto;background : linear-gradient (180deg , rgba (18 , 28 , 40 , .96 ), rgba (8 , 14 , 22 , .98 ));backdrop-filter : blur (16px );border-radius : 24px 24px 0 0 ;border : 1px solid rgba (255 , 255 , 255 , .12 );padding : 18px }.detail-top {display : grid;grid-template-columns : 110px 1fr ;gap : 14px }.detail-pic {width : 110px ;border-radius : 14px ;aspect-ratio : 2 / 3 ;object-fit : cover;background : # 17202c }.detail-title {font-size : 20px ;font-weight : 900 ;margin-bottom : 8px }.meta {font-size : 13px ;color : rgba (255 , 255 , 255 , .72 );line-height : 1.55 }.desc {margin : 12px 0 ;font-size : 13px ;color : rgba (255 , 255 , 255 , .75 );line-height : 1.6 }.eps {display : grid;grid-template-columns : repeat (auto-fill, minmax (82px , 1fr ));gap : 9px }.ep {padding : 10px 8px ;overflow : hidden;text-overflow : ellipsis;white-space : nowrap}.row {display : grid;grid-template-columns : 88px 1fr ;gap : 8px ;align-items : center;margin : 9px 0 }.cfg-title {font-size : 18px ;font-weight : 900 ;margin-bottom : 10px }.parser-list {display : grid;gap : 8px }.parser-item {display : grid;grid-template-columns : 22px 90px 1fr 42px ;gap : 8px ;align-items : center}.small {font-size : 12px ;color : rgba (255 , 255 , 255 , .62 );line-height : 1.5 }.pager {display : flex;gap : 10px ;justify-content : center;align-items : center;margin : 18px 0 4px }.pager button {border : 1px solid rgba (255 , 255 , 255 , .14 );background : rgba (255 , 255 , 255 , .09 );color : # fff ;border-radius : 14px ;padding : 10px 18px }.pager button : disabled {opacity : .38 }.toast {position : fixed;left : 50% ;bottom : 28px ;transform : translateX (-50% );background : rgba (0 , 0 , 0 , .72 );padding : 10px 14px ;border-radius : 999px ;display : none;z-index : 30 }
9+ </ style >
10+ </ head >
11+ < body >
12+ < main class ="app ">
13+ < div class ="top-title "> < div class ="brand "> < div class ="logo "> TX</ div > < div > 腾讯视频</ div > </ div > < button class ="icon-btn " id ="configBtn "> 解析设置</ button > </ div >
14+ < div class ="search-row "> < button class ="cfg-btn " id ="homeBtn "> 首页</ button > < form id ="searchForm "> < input id ="kw " placeholder ="搜索电影 / 电视剧 / 动漫 / 综艺 "> </ form > < button class ="cfg-btn " id ="searchBtn "> 搜索</ button > </ div >
15+ < div class ="tabs " id ="tabs "> </ div >
16+ < div class ="section-head "> < h2 id ="sectionTitle "> 精选</ h2 > < div class ="hint " id ="status "> 就绪</ div > </ div >
17+ < section id ="content "> </ section > < div class ="pager " id ="pager "> < button id ="prevPage "> 上一页</ button > < span id ="pageText "> 第 1 页</ span > < button id ="nextPage "> 下一页</ button > </ div >
18+ </ main >
19+ < div class ="sheet " id ="detailSheet "> < div class ="detail-panel " id ="detailPanel "> </ div > </ div >
20+ < div class ="config-mask " id ="configMask "> < div class ="config-panel "> < div class ="cfg-title "> 自定义解析设置</ div > < div class ="small "> 解析地址支持 < b > {url}</ b > 占位符;不写占位符时会自动拼接 encodeURIComponent(播放页URL)。启用后详情页会生成对应线路,可随时增删改。</ div > < div id ="parserList " class ="parser-list "> </ div > < div class ="row "> < span > 名称</ span > < input id ="pName " placeholder ="例如 虾米解析 "> </ div > < div class ="row "> < span > 地址</ span > < input id ="pUrl " placeholder ="https://xxx/?url={url} "> </ div > < div style ="display:flex;gap:8px;margin-top:12px "> < button class ="cfg-btn " id ="addParser "> 添加</ button > < button class ="cfg-btn " id ="saveParser "> 保存</ button > < button class ="cfg-btn " id ="resetParser "> 重置</ button > < button class ="cfg-btn " id ="closeCfg "> 关闭</ button > </ div > </ div > </ div >
21+ < div class ="toast " id ="toast "> </ div >
22+ < script >
23+ const BASE = 'https://v.qq.com' , API = 'https://node.video.qq.com' , CATS = [ [ 'choice' , '精选' ] , [ 'movie' , '电影' ] , [ 'tv' , '电视剧' ] , [ 'variety' , '综艺' ] , [ 'cartoon' , '动漫' ] , [ 'child' , '少儿' ] , [ 'doco' , '纪录片' ] ] , UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36' ;
24+ let activeCat = 'choice' , page = 1 , lastList = [ ] , currentDetail = null , activeLine = '' ;
25+ let parsers = loadParsers ( ) ;
26+ const $ = s => document . querySelector ( s ) , esc = s => String ( s || '' ) . replace ( / [ & < > " ] / g, m => ( { '&' :'&' , '<' :'<' , '>' :'>' , '"' :'"' } [ m ] ) ) , clean = s => String ( s || '' ) . trim ( ) , abs = u => ! u ?'' :/ ^ h t t p s ? : / i. test ( u ) ?u :u . startsWith ( '//' ) ?'https:' + u :new URL ( u , BASE ) . href ;
27+ function toast ( s ) { $ ( '#toast' ) . textContent = s ; $ ( '#toast' ) . style . display = 'block' ; setTimeout ( ( ) => $ ( '#toast' ) . style . display = 'none' , 1600 ) }
28+ function setStatus ( s ) { $ ( '#status' ) . textContent = s }
29+ function loadParsers ( ) { try { return JSON . parse ( localStorage . tx_parsers || '' ) } catch ( e ) { } return [ ] }
30+ function saveParsers ( ) { localStorage . tx_parsers = JSON . stringify ( parsers ) }
31+ async function req ( url , opt = { } ) { let o = Object . assign ( { headers :{ 'User-Agent' :UA , 'Referer' :BASE + '/' } } , opt ) ; if ( window . fm && fm . req ) { let r = await fm . req ( url , o ) ; return typeof r === 'string' ?r :( r && r . body ) || JSON . stringify ( r || { } ) } let r = await fetch ( url , o ) ; return await r . text ( ) }
32+ async function json ( url , opt = { } ) { let t = await req ( url , opt ) ; return JSON . parse ( t . replace ( / ^ Q Z O u t p u t J s o n = / , '' ) . replace ( / ; \s * $ / , '' ) ) }
33+ function renderTabs ( ) { tabs . innerHTML = CATS . map ( x => `<button class="tab ${ activeCat === x [ 0 ] ?'active' :'' } " data-id="${ x [ 0 ] } ">${ x [ 1 ] } </button>` ) . join ( '' ) ; tabs . querySelectorAll ( '.tab' ) . forEach ( b => b . onclick = ( ) => { activeCat = b . dataset . id ; page = 1 ; renderTabs ( ) ; loadCategory ( ) } ) }
34+ function parseCards ( html , cat ) { let arr = [ ] , ms = String ( html || '' ) . match ( / < d i v [ ^ > ] * c l a s s = [ " ' ] ? l i s t _ i t e m [ " ' ] ? [ ^ > ] * > [ \s \S ] * ?< \/ d i v > / gi) || [ ] ; ms . forEach ( it => { let n = ( it . match ( / < i m g [ ^ > ] * a l t = [ " ' ] ? ( [ ^ " ' ] * ) / i) || [ ] ) [ 1 ] || '' , p = ( it . match ( / < i m g [ ^ > ] * s r c = [ " ' ] ? ( [ ^ " ' \s > ] + ) / i) || [ ] ) [ 1 ] || '' , u = ( it . match ( / < a [ ^ > ] * d a t a - f l o a t = [ " ' ] ? ( [ ^ " ' \s > ] + ) / i) || [ ] ) [ 1 ] || ( it . match ( / h r e f = [ " ' ] ? ( [ ^ " ' \s > ] + ) / i) || [ ] ) [ 1 ] || '' , d = ( it . match ( / < a [ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / i) || [ ] ) [ 1 ] || '' ; if ( n && p ) arr . push ( { vod_id :( cat ?cat + '$' :'' ) + u , vod_name :n , vod_pic :abs ( p ) , vod_remarks :d . replace ( / < [ ^ > ] + > / g, '' ) . trim ( ) } ) } ) ; return arr }
35+ async function loadCategory ( ) { setStatus ( '加载中' ) ; sectionTitle . textContent = ( CATS . find ( x => x [ 0 ] === activeCat ) || CATS [ 0 ] ) [ 1 ] ; content . innerHTML = '<div class="empty">加载中...</div>' ; let ch = activeCat === 'choice' ?'cartoon' :activeCat , off = ( page - 1 ) * 21 , u = `${ BASE } /x/bu/pagesheet/list?_all=1&append=1&channel=${ encodeURIComponent ( ch ) } &listpage=${ page } &offset=${ off } &pagesize=21&iarea=-1&sort=18` ; try { let h = await req ( u ) ; lastList = parseCards ( h , ch ) ; renderCards ( lastList ) ; updatePager ( lastList . length >= 21 ) ; setStatus ( '第 ' + page + ' 页' ) } catch ( e ) { content . innerHTML = '<div class="empty">加载失败</div>' ; updatePager ( false ) ; setStatus ( '失败' ) } }
36+ function renderCards ( arr ) { content . innerHTML = arr . length ?arr . map ( ( v , i ) => `<button class="card" data-i="${ i } "><img class="poster" src="${ esc ( v . vod_pic ) } "><div class="card-info"><div class="name">${ esc ( v . vod_name ) } </div><div class="remark">${ esc ( v . vod_remarks ) } </div></div></button>` ) . join ( '' ) :'<div class="empty">暂无内容</div>' ; content . querySelectorAll ( '.card' ) . forEach ( b => b . onclick = ( ) => openDetail ( lastList [ + b . dataset . i ] ) ) }
37+ function updatePager ( hasNext ) { if ( ! window . pager ) return ; pager . style . display = kw . value . trim ( ) ?'none' :'flex' ; pageText . textContent = '第 ' + page + ' 页' ; prevPage . disabled = page <= 1 ; nextPage . disabled = ! hasNext }
38+ async function batchInfo ( vids ) { let out = [ ] ; for ( let i = 0 ; i < vids . length ; i += 30 ) { let b = vids . slice ( i , i + 30 ) ; try { let j = await json ( `https://union.video.qq.com/fcgi-bin/data?otype=json&tid=1804&appid=20001238&appkey=6c03bbe9658448a4&union_platform=1&idlist=${ b . join ( ',' ) } ` ) ; ( j . results || [ ] ) . forEach ( x => { let f = x . fields || { } ; out . push ( { vid :f . vid , title :f . title , type :f . category_map && f . category_map . length > 1 ?f . category_map [ 1 ] :'' } ) } ) } catch ( e ) { } } return out }
39+ async function openDetail ( v ) { detailSheet . classList . add ( 'show' ) ; detailPanel . innerHTML = '<div class="empty">详情加载中...</div>' ; let raw = String ( v . vod_id || '' ) , sp = raw . split ( '$' ) , cid = sp [ 1 ] || sp [ 0 ] ; try { let j = await json ( `${ API } /x/api/float_vinfo2?cid=${ encodeURIComponent ( cid ) } ` ) , c = j . c || { } , vids = c . video_ids || [ ] , infos = await batchInfo ( vids ) , ordered = vids . map ( x => infos . find ( y => y . vid === x ) || { vid :x , title :'' , type :'' } ) , main = ordered . filter ( x => ! x . type || x . type === '正片' ) , extra = ordered . filter ( x => x . type && x . type !== '正片' ) , mk = x => x . map ( ( it , i ) => ( { name :( / ^ \d + $ / . test ( it . title ) ?'第' + it . title + '集' :it . title ) || ( '第' + ( i + 1 ) + '集' ) , url :`${ BASE } /x/cover/${ cid } /${ it . vid } .html` } ) ) ; let lines = [ ] ; if ( main . length ) { lines . push ( { name :'智能线路' , eps :mk ( main ) } ) ; parsers . filter ( p => p . enabled && clean ( p . url ) ) . forEach ( p => lines . push ( { name :p . name , parser :p , eps :mk ( main ) } ) ) } if ( extra . length ) lines . push ( { name :'花絮/预告' , eps :mk ( extra ) } ) ; currentDetail = { vod :{ vod_id :raw , vod_name :c . title || v . vod_name || '' , vod_pic :abs ( c . pic || v . vod_pic || '' ) , vod_remarks :j . rec || v . vod_remarks || '' , vod_year :c . year || '' , type_name :( j . typ || [ ] ) . join ( ',' ) , vod_actor :( j . nam || [ ] ) . join ( ',' ) , vod_content :c . description || '' } , lines} ; renderDetail ( ) } catch ( e ) { detailPanel . innerHTML = '<div class="empty">详情加载失败</div>' } }
40+ function renderDetail ( ) { let d = currentDetail . vod , lines = currentDetail . lines ; activeLine = lines [ 0 ] && lines [ 0 ] . name || '' ; detailPanel . innerHTML = `<div class="detail-top"><img class="detail-pic" src="${ esc ( d . vod_pic ) } "><div><div class="detail-title">${ esc ( d . vod_name ) } </div><div class="meta">${ esc ( [ d . type_name , d . vod_year , d . vod_actor , d . vod_remarks ] . filter ( Boolean ) . join ( ' · ' ) ) } </div></div></div><div class="desc">${ esc ( d . vod_content || '暂无简介' ) } </div><div class="line-tabs" id="lineTabs"></div><div class="eps" id="eps"></div>` ; renderLines ( ) }
41+ function renderLines ( ) { lineTabs . innerHTML = currentDetail . lines . map ( x => `<button class="line-tab ${ activeLine === x . name ?'active' :'' } " data-line="${ esc ( x . name ) } ">${ esc ( x . name ) } </button>` ) . join ( '' ) ; lineTabs . querySelectorAll ( '.line-tab' ) . forEach ( b => b . onclick = ( ) => { activeLine = b . dataset . line ; renderLines ( ) } ) ; let ln = currentDetail . lines . find ( x => x . name === activeLine ) || currentDetail . lines [ 0 ] , eps = ln && ln . eps || [ ] ; document . querySelector ( '#eps' ) . innerHTML = eps . length ?eps . map ( ( e , i ) => `<button class="ep" data-i="${ i } ">${ esc ( e . name ) } </button>` ) . join ( '' ) :'<div class="empty">暂无分集</div>' ; document . querySelectorAll ( '.ep' ) . forEach ( b => b . onclick = ( ) => playEpisode ( ln , + b . dataset . i ) ) }
42+ function parseUrl ( p , u ) { u = String ( u || '' ) ; if ( ! p || ! clean ( p . url ) ) return u ; let x = clean ( p . url ) ; return x . includes ( '{url}' ) ?x . replaceAll ( '{url}' , encodeURIComponent ( u ) ) :x + encodeURIComponent ( u ) }
43+ function useNativePlay ( ) { return window . fm && typeof fm . vodInline === 'function' }
44+ function isDirect ( u ) { return / \. m 3 u 8 | \. m p 4 | t c \. q q \. c o m | d e f a u l t t s | o m t s | v 3 - w e b | b y t e / i. test ( String ( u || '' ) ) }
45+ async function resolvePlay ( u ) { let x = String ( u || '' ) ; if ( ! x || isDirect ( x ) ) return x ; try { let t = await req ( x , { headers :{ 'User-Agent' :UA , 'Referer' :BASE + '/' } } ) , j = null ; try { j = JSON . parse ( t ) } catch ( e ) { } ; if ( j && j . url ) x = j . url || x ; else { let m = String ( t ) . match ( / h t t p s ? : [ ^ \" ' < > ] + (?: m 3 u 8 | m p 4 ) [ ^ \" ' < > ] * / i) ; if ( m ) x = m [ 0 ] . replace ( / \\ \/ / g, '/' ) } } catch ( e ) { } return x }
46+ async function resolveEpisode ( line , e , i , total ) { if ( e . real && isDirect ( e . real ) ) return e . real ; toast ( '解析 ' + ( i + 1 ) + '/' + total ) ; let u = await resolvePlay ( parseUrl ( line . parser , e . url ) ) ; if ( isDirect ( u ) ) e . real = u ; return e . real || u }
47+ async function playEpisode ( line , index ) { let d = currentDetail . vod , cur = line . eps [ index ] ; if ( ! cur ) return ; if ( ! line . parser ) { let p = parsers . find ( x => x . enabled && clean ( x . url ) ) ; if ( p ) line = { name :p . name , parser :p , eps :line . eps } ; else return toast ( '请先在解析设置里填写可用解析地址' ) } ; let total = line . eps . length , eps = [ ] ; for ( let i = 0 ; i < total ; i ++ ) { let e = line . eps [ i ] , u = await resolveEpisode ( line , e , i , total ) ; eps . push ( { name :e . name , url :u } ) } let real = eps [ index ] && eps [ index ] . url ; if ( ! isDirect ( real ) ) return toast ( '解析失败,未得到m3u8直链' ) ; let from = [ line . name ] , urls = [ eps . map ( e => `${ e . name } $${ e . url } ` ) . join ( '#' ) ] , vod = { vod_id :d . vod_id , vod_name :d . vod_name , vod_pic :d . vod_pic , vod_remarks :d . vod_remarks , vod_content :d . vod_content , vod_play_from :from . join ( '$$$' ) , vod_play_url :urls . join ( '$$$' ) , play_from :line . name , mark :cur . name , episodes :eps , url :real , parse :0 , jx :0 , header :{ 'User-Agent' :UA , 'Referer' :BASE + '/' } } ; toast ( '开始播放' ) ; if ( useNativePlay ( ) ) { await fm . vodInline ( vod ) ; return } if ( window . fm && fm . play ) { await fm . play ( real , d . vod_name + ' ' + cur . name ) ; return } location . href = real }
48+ async function search ( ) { let wd = clean ( kw . value ) ; if ( ! wd ) return ; setStatus ( '搜索中' ) ; sectionTitle . textContent = '搜索:' + wd ; content . innerHTML = '<div class="empty">搜索中...</div>' ; try { let payload = { version :'25042201' , clientType :1 , query :wd , pagenum :0 , pagesize :30 , extraInfo :{ isNewMarkLabel :'1' , multi_terminal_pc :'1' , themeType :'1' } } , t = await req ( 'https://pbaccess.video.qq.com/trpc.videosearch.mobile_search.MultiTerminalSearch/MbSearch?vplatform=2' , { method :'POST' , headers :{ 'User-Agent' :UA , 'Content-Type' :'application/json' , 'Origin' :BASE , 'Referer' :BASE + '/' } , body :JSON . stringify ( payload ) } ) , j = JSON . parse ( t ) , arr = [ ] , push = it => { if ( it && it . doc && it . doc . id && it . videoInfo && it . doc . id . length > 11 ) arr . push ( { vod_id :it . doc . id , vod_name :( it . videoInfo . title || '' ) . replace ( / < \/ ? e m > / g, '' ) , vod_pic :it . videoInfo . imgUrl || '' , vod_remarks :it . videoInfo . firstLine || it . videoInfo . secondLine || '' } ) } ; ( ( ( j . data || { } ) . normalList || { } ) . itemList || [ ] ) . forEach ( push ) ; ( ( j . data || { } ) . areaBoxList || [ ] ) . forEach ( a => ( a . itemList || [ ] ) . forEach ( push ) ) ; lastList = arr ; renderCards ( arr ) ; pager . style . display = 'none' ; setStatus ( arr . length + ' 条' ) } catch ( e ) { content . innerHTML = '<div class="empty">搜索失败</div>' ; setStatus ( '失败' ) } }
49+ function renderParserCfg ( ) { parserList . innerHTML = parsers . map ( ( p , i ) => `<div class="parser-item"><input type="checkbox" data-i="${ i } " ${ p . enabled ?'checked' :'' } ><input data-n="${ i } " value="${ esc ( p . name ) } "><input data-u="${ i } " value="${ esc ( p . url ) } " placeholder="https://.../?url={url}"><button class="cfg-btn" data-d="${ i } ">删</button></div>` ) . join ( '' ) ; parserList . querySelectorAll ( '[data-i]' ) . forEach ( x => x . onchange = ( ) => parsers [ x . dataset . i ] . enabled = x . checked ) ; parserList . querySelectorAll ( '[data-n]' ) . forEach ( x => x . oninput = ( ) => parsers [ x . dataset . n ] . name = x . value ) ; parserList . querySelectorAll ( '[data-u]' ) . forEach ( x => x . oninput = ( ) => parsers [ x . dataset . u ] . url = x . value ) ; parserList . querySelectorAll ( '[data-d]' ) . forEach ( x => x . onclick = ( ) => { parsers . splice ( + x . dataset . d , 1 ) ; renderParserCfg ( ) } ) }
50+ configBtn . onclick = ( ) => { renderParserCfg ( ) ; configMask . classList . add ( 'show' ) } ; closeCfg . onclick = ( ) => configMask . classList . remove ( 'show' ) ; addParser . onclick = ( ) => { let n = clean ( pName . value ) , u = clean ( pUrl . value ) ; if ( ! n || ! u ) return toast ( '请填写名称和地址' ) ; parsers . push ( { name :n , url :u , enabled :true } ) ; pName . value = pUrl . value = '' ; renderParserCfg ( ) } ; saveParser . onclick = ( ) => { saveParsers ( ) ; toast ( '已保存' ) } ; resetParser . onclick = ( ) => { parsers = [ ] ; localStorage . removeItem ( 'tx_parsers' ) ; renderParserCfg ( ) ; toast ( '已清空' ) } ; detailSheet . onclick = e => { if ( e . target === detailSheet ) detailSheet . classList . remove ( 'show' ) } ; configMask . onclick = e => { if ( e . target === configMask ) configMask . classList . remove ( 'show' ) } ; searchForm . onsubmit = e => { e . preventDefault ( ) ; search ( ) } ; searchBtn . onclick = search ; prevPage . onclick = ( ) => { if ( page > 1 ) { page -- ; loadCategory ( ) ; scrollTo ( 0 , 0 ) } } ; nextPage . onclick = ( ) => { page ++ ; loadCategory ( ) ; scrollTo ( 0 , 0 ) } ; homeBtn . onclick = ( ) => { kw . value = '' ; page = 1 ; loadCategory ( ) } ; renderTabs ( ) ; loadCategory ( ) ;
51+ </ script >
52+ </ body >
53+ </ html >
0 commit comments