import Context from '../context' ; import Component from '../lib/component' ; import * as Utils from '../lib/utils' ; import * as Ui from '../lib/ui' ; import Api from '../api' ; import Comment from './comment' ; import { ListData , CommentData , NotifyData , ApiVersionData } from '~/types/artalk-data' ; import Pagination from './pagination' ; import ReadMoreBtn from './read-more-btn' ; export default class ListLite extends Component { protected $commentsWrap : HTMLElement ; protected comments : Comment [ ] = [ ] ; protected data ? : ListData ; protected isLoading : boolean = false ; public noCommentText = '无内容' ; /** 平铺模式 */ public flatMode = false ; /** 分页 */ public pageMode : 'pagination' | 'read-more' = 'pagination' ; public pageSize : number = 20 ; public scrollListenerAt ? : HTMLElement ; public repositionAt ? : HTMLElement ; protected pagination ? : Pagination ; protected readMoreBtn ? : ReadMoreBtn ; protected autoLoadScrollEvent ? : any ; public renderComment ? : ( comment : Comment ) => void ; public paramsEditor ? : ( params : any ) => void ; public onAfterLoad ? : ( data : ListData ) => void ; protected unread : NotifyData [ ] = [ ] ; public unreadHighlight ? : boolean ; constructor ( ctx ) { super ( ctx ) ; this . $el = Utils . createElement ( `<div class="atk-list-lite">
      <div class="atk-list-comments-wrap"></div>
    </div>` ) ; this . $commentsWrap = this . $el . querySelector ( '.atk-list-comments-wrap' ) ! ; window . setInterval ( ( ) => { this . $el . querySelectorAll < HTMLElement > ( '[data-atk-comment-date]' ) . forEach ( el => { const date = el . getAttribute ( 'data-atk-comment-date' ) ; el . innerText = Utils . timeAgo ( new Date ( Number ( date ) ) ) ; } ) ; } , 30 * 1000 ) ; this . ctx . on ( 'unread-update' , ( data ) => ( this . updateUnread ( data . notifies ) ) ) ; } /** 评论获取 */ public async fetchComments ( offset ) { if ( this . isLoading ) return ; const showLoading = ( ) => { this . isLoading = true ; if ( offset === 0 ) Ui . showLoading ( this . $el ) ; else if ( this . pageMode === 'read-more' ) this . readMoreBtn ! . setLoading ( true ) ; else if ( this . pageMode === 'pagination' ) this . pagination ! . setLoading ( true ) ; } ; const hideLoading = ( ) => { this . isLoading = false ; if ( offset === 0 ) Ui . hideLoading ( this . $el ) ; else if ( this . pageMode === 'read-more' ) this . readMoreBtn ! . setLoading ( false ) ; else if ( this . pageMode === 'pagination' ) this . pagination ! . setLoading ( false ) ; } ; showLoading ( ) ; this . ctx . trigger ( 'comments-load' ) ; if ( this . pageMode === 'read-more' && offset === 0 ) { this . clearAllComments ( ) ; } let listData ; try { listData = await new Api ( this . ctx ) . get ( offset , this . pageSize , this . flatMode , this . paramsEditor ) ; } catch ( e ) { this . onError ( e . msg || String ( e ) , offset ) ; throw e ; } finally { hideLoading ( ) ; } Ui . setError ( this . $el , null ) ; try { this . onLoad ( listData , offset ) ; } catch ( e ) { this . onError ( String ( e ) , offset ) ; throw e ; } finally { hideLoading ( ) ; } } protected onLoad ( data , offset ) { if ( this . pageMode === 'pagination' ) { this . clearAllComments ( ) ; } this . data = data ; if ( this . ctx . conf . versionCheck && this . versionCheck ( data . api_version ) ) return ; this . importComments ( data . comments ) ; this . refreshPagination ( offset , ( this . flatMode ? data . total : data . total_roots ) ) ; this . refreshUI ( ) ; this . ctx . trigger ( 'unread-update' , { notifies : data . unread || [ ] } ) ; this . ctx . trigger ( 'comments-loaded' ) ; if ( this . onAfterLoad ) this . onAfterLoad ( data ) ; } /** 分页模式 */ private refreshPagination ( offset , total ) { const modePagination = ( this . pageMode === 'pagination' ) ; const modeReadMoreBtn = ( this . pageMode === 'read-more' ) ; const initialized = ( modePagination ) ? ! ! this . pagination : ! ! this . readMoreBtn ; if ( ! initialized ) { this . initPagination ( ) ; } if ( modePagination ) this . pagination ! . update ( offset , total ) ; if ( modeReadMoreBtn ) this . readMoreBtn ! . update ( offset , total ) ; } private initPagination ( ) { if ( this . pageMode === 'read-more' ) { this . readMoreBtn = new ReadMoreBtn ( { pageSize : this . pageSize , onClick : async ( o ) => { await this . fetchComments ( o ) ; } , } ) ; this . $el . append ( this . readMoreBtn . $el ) ; if ( this . conf . pagination ?. autoLoad ) { const at = this . scrollListenerAt || document ; if ( this . autoLoadScrollEvent ) at . removeEventListener ( 'scroll' , this . autoLoadScrollEvent ) ; this . autoLoadScrollEvent = ( ) => { if ( this . pageMode !== 'read-more' ) return ; if ( ! this . readMoreBtn ) return ; if ( ! this . readMoreBtn . hasMore ) return ; if ( this . isLoading ) return ; const $target = this . $el . querySelector < HTMLElement > ( '.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)' ) ; if ( ! $target ) return ; if ( Ui . isVisible ( $target , this . scrollListenerAt ) ) { this . readMoreBtn . click ( ) ; } } ; at . addEventListener ( 'scroll' , this . autoLoadScrollEvent ) ; } } if ( this . pageMode === 'pagination' ) { this . pagination = new Pagination ( ( ! this . flatMode ? this . data ! . total_roots : this . data ! . total ) , { pageSize : this . pageSize , onChange : async ( o ) => { if ( this . ctx . conf . editorTravel === true ) { this . ctx . trigger ( 'editor-travel-back' ) ; } await this . fetchComments ( o ) ; if ( this . repositionAt ) { const at = this . scrollListenerAt || window ; at . scroll ( { top : this . repositionAt ? Utils . getOffset ( this . repositionAt ) . top : 0 , left : 0 , } ) ; } } } ) ; this . $el . append ( this . pagination . $el ) ; } } /** 错误处理 */ protected onError ( msg , offset ) { msg = String ( msg ) ; console . error ( msg ) ; if ( offset !== 0 && this . pageMode === 'read-more' ) { this . readMoreBtn ?. showErr ( `获取失败` ) ; return ; } const $err = Utils . createElement ( `<span>${ msg }，无法获取评论列表数据<br/></span>` ) ; const $retryBtn = Utils . createElement ( '<span style="cursor:pointer;">点击重新获取</span>' ) ; $retryBtn . onclick = ( ) => ( this . fetchComments ( 0 ) ) ; $err . appendChild ( $retryBtn ) ; const adminBtn = Utils . createElement ( '<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>' ) ; adminBtn . onclick = ( ) => ( this . ctx . trigger ( 'sidebar-show' ) ) ; if ( ! this . ctx . user . data . isAdmin ) adminBtn . classList . add ( 'atk-hide' ) ; $err . appendChild ( adminBtn ) ; Ui . setError ( this . $el , $err ) ; } /** 刷新界面 */ public refreshUI ( ) { const isNoComment = this . comments . length <= 0 ; let $noComment = this . $commentsWrap . querySelector < HTMLElement > ( '.atk-list-no-comment' ) ; if ( isNoComment ) { if ( ! $noComment ) { $noComment = Utils . createElement ( '<div class="atk-list-no-comment"></div>' ) ; $noComment . innerHTML = this . noCommentText ; this . $commentsWrap . appendChild ( $noComment ) ; } } else { $noComment ?. remove ( ) ; } this . ctx . trigger ( 'check-admin-show-el' ) ; } /** 创建新评论 */ protected createComment ( data ) { const comment = new Comment ( this . ctx , data ) ; comment . afterRender = ( ) => { if ( this . renderComment ) this . renderComment ( comment ) ; } ; comment . onDelete = ( c ) => { this . deleteComment ( c ) ; this . refreshUI ( ) ; } ; return comment ; } /** 导入评论 · 通过请求数据 */ public importComments ( srcData ) { if ( this . flatMode ) { srcData . forEach ( ( commentData ) => { this . putCommentFlatMode ( commentData , srcData , 'append' ) ; } ) ; } else { this . importCommentsNesting ( srcData ) ; } } private importCommentsNesting ( srcData ) { const loadChildren = ( parentC ) => { const children = srcData . filter ( o => o . rid === parentC . data . id ) ; children . forEach ( ( childData ) => { const childC = this . createComment ( childData ) ; childC . render ( ) ; parentC . putChild ( childC ) ; loadChildren ( childC ) ; } ) ; } ; const rootComments = srcData . filter ( ( o ) => o . rid === 0 ) ; rootComments . forEach ( ( rootData ) => { const rootC = this . createComment ( rootData ) ; rootC . render ( ) ; this . $commentsWrap . appendChild ( rootC . getEl ( ) ) ; rootC . playFadeInAnim ( ) ; this . comments . push ( rootC ) ; loadChildren ( rootC ) ; rootC . checkHeightLimit ( ) ; } ) ; } /** 导入评论 · 平铺模式 */ private putCommentFlatMode ( cData , srcData , insertMode ) { if ( cData . is_collapsed ) cData . is_allow_reply = false ; const comment = this . createComment ( cData ) ; if ( cData . rid !== 0 ) { const rComment = srcData . find ( o => o . id === cData . rid ) ; if ( rComment ) comment . replyTo = rComment ; } comment . render ( ) ; if ( insertMode === 'append' ) this . comments . push ( comment ) ; if ( insertMode === 'prepend' ) this . comments . unshift ( comment ) ; if ( cData . visible ) { if ( insertMode === 'append' ) this . $commentsWrap . append ( comment . getEl ( ) ) ; if ( insertMode === 'prepend' ) this . $commentsWrap . prepend ( comment . getEl ( ) ) ; comment . playFadeInAnim ( ) ; } comment . checkHeightLimit ( ) ; return comment ; } /** 新增评论 · 首部添加 */ public insertComment ( commentData ) { if ( ! this . flatMode ) { const comment = this . createComment ( commentData ) ; comment . render ( ) ; if ( commentData . rid === 0 ) { this . $commentsWrap . prepend ( comment . getEl ( ) ) ; this . comments . unshift ( comment ) ; } else { const parent = this . findComment ( commentData . rid ) ; if ( parent ) { parent . putChild ( comment ) ; comment . getParents ( ) . forEach ( ( p ) => { if ( p . $children ) p . heightLimitRemove ( p . $children ) ; } ) ; } } comment . checkHeightLimit ( ) ; Ui . scrollIntoView ( comment . getEl ( ) ) ; comment . playFadeInAnim ( ) ; } else { const comment = this . putCommentFlatMode ( commentData , this . comments . map ( c => c . data ) , 'prepend' ) ; Ui . scrollIntoView ( comment . getEl ( ) ) ; } if ( this . data ) this . data . total += 1 ; this . refreshUI ( ) ; this . ctx . trigger ( 'comments-loaded' ) ; } /** 遍历操作 Comment (包括子评论) */ protected eachComment ( commentList , action ) { if ( commentList . length === 0 ) return ; commentList . every ( ( item ) => { if ( action ( item , commentList ) === false ) return false ; this . eachComment ( item . getChildren ( ) , action ) ; return true ; } ) ; } /** 查找评论项 */ protected findComment ( id , src ? ) { if ( ! src ) src = this . comments ; let comment = null ; this . eachComment ( src , ( item ) => { if ( item . data . id === id ) { comment = item ; return false ; } return true ; } ) ; return comment ; } /** 删除评论 */ public deleteComment ( comment ) { let findComment ; if ( typeof comment === 'number' ) { findComment = this . findComment ( comment ) ; if ( ! findComment ) throw Error ( `未找到评论 ${ comment }` ) ; } else findComment = comment ; findComment . getEl ( ) . remove ( ) ; this . eachComment ( this . comments , ( item , levelList ) => { if ( item === findComment ) { levelList . splice ( levelList . indexOf ( item ) , 1 ) ; return false ; } return true ; } ) ; if ( this . data ) this . data . total -= 1 ; this . refreshUI ( ) ; } /** 清空所有评论 */ public clearAllComments ( ) { this . $commentsWrap . innerHTML = '' ; this . data = undefined ; this . comments = [ ] ; } /** 更新未读数据 */ public updateUnread ( notifies ) { this . unread = notifies ; if ( this . unreadHighlight === true ) { this . eachComment ( this . comments , ( comment ) => { const notify = this . unread . find ( o => o . comment_id === comment . data . id ) ; if ( notify ) { comment . setUnread ( true ) ; comment . setOpenURL ( notify . read_link ) ; comment . openEvt = ( ) => { this . unread = this . unread . filter ( o => o . comment_id !== comment . data . id ) ; this . ctx . trigger ( 'unread-update' , { notifies : this . unread } ) ; } ; } else { comment . setUnread ( false ) ; } } ) ; } } /** 前端版本检测 */ public versionCheck ( versionData ) { const needVersion = versionData ?. fe_min_version || '0.0.0' ; const needUpdate = Utils . versionCompare ( needVersion , ARTALK_VERSION ) === 1 ; if ( needUpdate ) { const errEl = Utils . createElement ( `<div>前端 Artalk 版本已过时，请更新以获得完整体验<br/>` + `若您是站点管理员，请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获取帮助` + `<br/><br/>` + `<span style="color: var(--at-color-meta);">前端版本 ${ ARTALK_VERSION }，需求版本 >= ${ needVersion }</span><br/><br/>` + `</div>` ) ; const ignoreBtn = Utils . createElement ( '<span style="cursor:pointer;">忽略</span>' ) ; ignoreBtn . onclick = ( ) => { Ui . setError ( this . ctx , null ) ; this . ctx . conf . versionCheck = false ; this . fetchComments ( 0 ) ; } ; errEl . append ( ignoreBtn ) ; Ui . setError ( this . ctx , errEl , '<span class="atk-warn-title">Artalk Warn</span>' ) ; } return needUpdate ; } }