c77fb700创建于 2025年1月16日历史提交
/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { common } from '@kit.AbilityKit';
import { router } from '@kit.ArkUI';
import { UserModel } from '@ohos/mine';
import { LoadingFailedView, LoadingMore, NoMore } from '@ohos/uicomponents';
import {
  BreakpointType,
  BreakpointTypeEnum,
  CommonConstants,
  ContinueModel,
  EventTypeEnum,
  LazyDataSource,
  LearningResource,
  LoadingStatus,
  Logger,
  ObservedArray,
  RouterNameEnum
} from '@ohos/utils';
import { DiscoverModel } from '../model/DiscoverModel';
import { ArticleCardView } from '../components/ArticleCardView';
import { DiscoverSkeletonView } from './DiscoverSkeletonView';
import { FeedFlowItem } from '../components/FeedFlowItem';

const FEEDS_VISIBLE_LENGTH = 6;
const FEED_CARD_WIDTH_SM = '43.4%';
const FEED_CARD_WIDTH_MD = '21.6%';
const FEED_CARD_WIDTH_LG = '15%';
const SWIPER_ASPECT_RATIO = 2.4;
const SWIPER_MARGIN_MD = 220;
const SWIPER_MARGIN_LG = 100;
const COUNT_TWO = 2;
const COUNT_THREE = 3;

const TAG = '[DiscoverView]';
let continueModel = ContinueModel.getInstance();

@Entry({ routeName: 'DiscoverView' })
@Component
export struct DiscoverView {
  @State userModel: UserModel = UserModel.getInstance();
  @State discoverModel: DiscoverModel = DiscoverModel.getInstance();
  @State hotFeedList: ObservedArray<LearningResource> = this.discoverModel.feedArticleDataSource.dataArray;
  @State isListReachEnd: boolean = false;
  @StorageLink('getHomeResource') @Watch('handleGetHomeResourceChanged') getHomeResource: boolean = false;
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  @State loadingStatus: LoadingStatus = LoadingStatus.OFF;
  @State techArticleDataSource: LazyDataSource<LearningResource> = this.discoverModel.techArticleDataSource;

  dynamicLoading(): void {
    try {
      import('./FeedWaterFlowView');
      import('./SearchView');
      import('@ohos/mine/src/main/ets/views/DiscoverArticleDetailView');
    } catch (err) {
      Logger.error(TAG, 'dynamicLoading error:' + err);
    }
  }

  aboutToAppear() {
    this.dynamicLoading();
    this.userModel.getUserData();
    this.loadResources();
    this.discoverModel.getHotList();

    let routerName = continueModel.geRouterName();
    Logger.info(TAG, 'aboutToAppear routerName is:' + routerName);

    if (continueModel.isContinue && routerName === RouterNameEnum.DISCOVER_ARTICLE_VIEW) {
      let learningResource: LearningResource = continueModel.data?.itemData as LearningResource;
      if (learningResource.from) {
        this.jumpList();
      }
      this.jumpDetail(learningResource);
      continueModel.resetContinue();
    }

    if (continueModel.isContinue && routerName === RouterNameEnum.DISCOVER_FEED_WATER_FLOW) {
      this.jumpList();
      continueModel.resetContinue();
    }
  }

  loadResources(): void {
    this.loadingStatus = LoadingStatus.LOADING;
    this.discoverModel.getHomeResources().then(() => {
      this.loadingStatus = LoadingStatus.SUCCESS;
      AppStorage.setOrCreate('getHomeResource', false);
    }).catch(() => {
      this.loadingStatus = LoadingStatus.FAILED;
    });
  }

  handleGetHomeResourceChanged(): void {
    if (!this.getHomeResource) {
      return;
    }
    this.loadResources();
  }

  jumpDetail(item: LearningResource): void {
    router.pushNamedRoute({
      name: 'DiscoverArticleDetailView',
      params: new Object({
        articleDetail: item,
      })
    });
  }

  jumpList(): void {
    router.pushNamedRoute({
      name: "FeedWaterFlowView"
    });
  }

  @Builder
  feedTitleBuilder() {
    Row() {
      Text($r('app.string.hot_feeds'))
        .padding({
          left: this.currentBreakpoint === BreakpointTypeEnum.SM ? $r('app.float.md_padding_margin') : 0,
        })
        .fontColor($r('app.color.theme_font_color'))
        .fontSize($r('app.float.lg_font_size'))
        .fontWeight(FontWeight.Medium)
        .fontFamily(CommonConstants.HARMONY_HEITI_MEDIUM_FONT_FAMILY)
      Row() {
        Text($r('app.string.more'))
          .fontSize($r('app.float.md_font_size'))
          .fontColor($r('sys.color.ohos_id_color_foreground'))
          .opacity(CommonConstants.SECOND_LEVEL_OPACITY)

        Image($r('app.media.symbol_chevron_right'))
          .width('24fp')
          .height('24fp')
          .margin({ right: 5 })
      }
      .onClick(() => this.jumpList())
    }
    .alignItems(VerticalAlign.Center)
    .padding({
      right: new BreakpointType({
        sm: $r('app.float.md_padding_margin'),
        md: $r('app.float.xxl_padding_margin'),
        lg: $r('app.float.xxl_padding_margin')
      }).getValue(this.currentBreakpoint),
      left: new BreakpointType({
        sm: $r('app.float.md_padding_margin'),
        md: $r('app.float.xxl_padding_margin'),
        lg: $r('app.float.xxl_padding_margin')
      }).getValue(this.currentBreakpoint),
      top: $r('app.float.sm_padding_margin'),
      bottom: $r('app.float.sm_padding_margin')
    })
    .width(CommonConstants.FULL_PERCENT)
    .justifyContent(FlexAlign.SpaceBetween)
  }

  @Builder
  articleTitleBuilder() {
    Text($r('app.string.technical_articles'))
      .margin({
        left: this.currentBreakpoint === BreakpointTypeEnum.SM ? $r('app.float.md_padding_margin') : 0,
      })
      .fontSize($r('app.float.lg_font_size'))
      .width(CommonConstants.FULL_PERCENT)
      .fontFamily(CommonConstants.HARMONY_HEITI_MEDIUM_FONT_FAMILY)
      .fontWeight(FontWeight.Medium)
      .padding({
        right: new BreakpointType({
          sm: $r('app.float.md_padding_margin'),
          md: $r('app.float.xxl_padding_margin'),
          lg: $r('app.float.xxl_padding_margin')
        }).getValue(this.currentBreakpoint),
        left: new BreakpointType({
          sm: $r('app.float.md_padding_margin'),
          md: $r('app.float.xxl_padding_margin'),
          lg: $r('app.float.xxl_padding_margin')
        }).getValue(this.currentBreakpoint),
        top: $r('app.float.sm_padding_margin'),
        bottom: $r('app.float.sm_padding_margin')
      })
  }

  build() {
    Navigation() {
      Column() {
        Row() {
          Text($r('app.string.discover'))
            .fontSize($r('app.float.header_font_size'))
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Start)
            .fontFamily(CommonConstants.HARMONY_HEITI_BOLD_FONT_FAMILY)
            .margin({ right: $r('app.float.sm_padding_margin') })

          Search({ placeholder: 'Search' })
            .focusable(false)
            .textFont({ size: $r('app.float.large_text_size') })
            .width($r('app.float.search_width'))
            .height($r('app.float.search_height'))
            .searchIcon({
              src: $r('app.media.seach')
            })
            .cancelButton({
              style: CancelButtonStyle.CONSTANT,
              icon: {
                src: $r('app.media.cancel')
              }
            })
            .onClick(() => {
              router.pushNamedRoute({ name: 'SearchView' });
            })
        }
        .padding({ left: $r('app.float.xxl_padding_margin'), right: $r('app.float.lg_padding_margin') })
        .justifyContent(FlexAlign.SpaceBetween)
        .width(CommonConstants.FULL_PERCENT)
        .height($r('app.float.top_navigation_height'))

        if (this.loadingStatus === LoadingStatus.LOADING) {
          DiscoverSkeletonView()
        }
        if (this.loadingStatus === LoadingStatus.FAILED) {
          LoadingFailedView(() => this.loadResources())
        }
        if (this.loadingStatus === LoadingStatus.SUCCESS) {
          List({ space: CommonConstants.SPACE_16 }) {
            ListItem() {
              Banner({
                swiperData: this.discoverModel.swiperData,
                handleClick: (item: LearningResource) => this.jumpDetail(item)
              })
            }

            ListItemGroup({ header: this.feedTitleBuilder() }) {
              HotFeeds({
                hotFeedList: this.hotFeedList,
                handleClick: (item: LearningResource) => this.jumpDetail(item)
              })
            }

            ListItemGroup({ header: this.articleTitleBuilder() }) {
              TechArticles({
                articlesDataSource: this.techArticleDataSource,
                handleClick: (item: LearningResource) => this.jumpDetail(item),
                discoverModel: this.discoverModel
              })
            }
          }
          .scrollBar(BarState.Off)
          .layoutWeight(1)
          .height(CommonConstants.FULL_PERCENT)
          .width(CommonConstants.FULL_PERCENT)
        }
      }
      .padding({
        top: AppStorage.get<number>('statusBarHeight')
      })
      .height(CommonConstants.FULL_PERCENT)
      .width(CommonConstants.FULL_PERCENT)
    }
    .hideTitleBar(true)
    .mode(NavigationMode.Stack)
  }
}

@Component
struct Banner {
  @Prop swiperData: LearningResource[];
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  @State showPrevMargin: boolean = false;
  @State imgHeight: Length = 0;
  private swiperController: SwiperController = new SwiperController();
  handleClick: (item: LearningResource) => void = () => {
  };

  build() {
    Stack() {
      Swiper(this.swiperController) {
        ForEach(this.swiperData, (item: LearningResource, index: number) => {
          Row() {
            Image(item.bannerSrc)
              .width(CommonConstants.FULL_PERCENT)
              .height(CommonConstants.FULL_PERCENT)
              .borderRadius($r('app.float.lg_border_radius'))
              .onClick(() => this.handleClick(item))
          }
          .aspectRatio(SWIPER_ASPECT_RATIO)
          .onAreaChange((oldValue: Area, newValue: Area) => {
            if (index === 0 && !this.showPrevMargin) {
              this.imgHeight = newValue.height;
            }
          })
          .onAppear(() => {
            if (index === 0 && this.currentBreakpoint !== BreakpointTypeEnum.SM) {
              setTimeout(() => {
                this.showPrevMargin = true;
              }, 3000)
            }
          })
          .padding({
            left: $r('app.float.md_padding_margin'),
          })
        }, (item: LearningResource) => item.id)
      }
      .width(CommonConstants.FULL_PERCENT)
      .displayCount(new BreakpointType({ sm: 1, md: 1, lg: COUNT_TWO }).getValue(this.currentBreakpoint))
      .nextMargin(new BreakpointType<Length>({
        sm: $r('app.float.md_padding_margin'),
        md: SWIPER_MARGIN_MD,
        lg: SWIPER_MARGIN_LG
      }).getValue(this.currentBreakpoint))
      .prevMargin(this.currentBreakpoint === BreakpointTypeEnum.SM ? 0 : $r('app.float.md_padding_margin'))
      .width(CommonConstants.FULL_PERCENT)
      .indicator(this.currentBreakpoint === BreakpointTypeEnum.SM ? Indicator.dot()
        .color($r('app.color.swiper_indicator_color'))
        .selectedColor($r('app.color.theme_blue_color')) : false)
      .autoPlay(true)
      .loop(true)

      Column()
        .height(this.imgHeight)
        .width($r('app.float.md_padding_margin'))
        .backgroundColor($r('app.color.common_background_color'))
        .visibility((this.showPrevMargin || this.currentBreakpoint === BreakpointTypeEnum.SM) ? Visibility.Hidden :
        Visibility.Visible)
    }
    .alignContent(Alignment.Start)
  }
}

@Component
struct HotFeeds {
  @ObjectLink hotFeedList: ObservedArray<LearningResource>;
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  handleClick: (item: LearningResource) => void = () => {
  };

  build() {
    Column({ space: CommonConstants.SPACE_8 }) {
      List() {
        ForEach(this.hotFeedList.slice(0, FEEDS_VISIBLE_LENGTH), (item: LearningResource, index: number) => {
          ListItem() {
            FeedFlowItem({ feedItem: item })
              .onClick(() => this.handleClick(item))
          }
          .margin({
            left: index === 0 ? new BreakpointType({
              sm: $r('app.float.md_padding_margin'),
              md: $r('app.float.xxl_padding_margin'),
              lg: $r('app.float.xxl_padding_margin')
            }).getValue(this.currentBreakpoint) : 0,
            right: (this.currentBreakpoint !== BreakpointTypeEnum.SM && index === FEEDS_VISIBLE_LENGTH - 1)
              ? $r('app.float.xxl_padding_margin') : $r('app.float.md_padding_margin')
          })
          .width(new BreakpointType<ResourceStr>({
            sm: FEED_CARD_WIDTH_SM,
            md: FEED_CARD_WIDTH_MD,
            lg: FEED_CARD_WIDTH_LG
          }).getValue(this.currentBreakpoint))
        }, (item: LearningResource) => item.id)
      }
      .edgeEffect(EdgeEffect.None)
      .scrollBar(BarState.Off)
      .listDirection(Axis.Horizontal)
    }
  }
}

@Component
export struct TechArticles {
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  @State userModel: UserModel = UserModel.getInstance();
  @State @Watch('userOperationChanged') collectedIds: ObservedArray<string> = this.userModel.collectedIds;
  @State @Watch('userOperationChanged') likedIds: ObservedArray<string> = this.userModel.likedIds;
  @ObjectLink @Watch('userOperationChanged') articlesDataSource: LazyDataSource<LearningResource>;
  @State isListReachEnd: boolean = false;
  @Link discoverModel: DiscoverModel;
  private eventHub: common.EventHub = (getContext(this) as common.UIAbilityContext).eventHub;
  handleClick: (item: LearningResource) => void = () => {
  };

  aboutToAppear(): void {
    this.userOperationChanged();
  }

  // After a user likes and collects favorites, the article list data needs to be updated.
  userOperationChanged() {
    this.articlesDataSource.dataArray.forEach((item: LearningResource) => {
      item.isCollected = this.collectedIds.some((id: string) => id === item.id);
      item.isLiked = this.likedIds.some((id: string) => id === item.id);
    });
  }

  build() {
    Column() {
      List({ space: CommonConstants.SPACE_12 }) {
        LazyForEach(this.articlesDataSource, (item: LearningResource, index: number) => {
          ListItem() {
            ArticleCardView({
              articleItem: item,
              onCollected: (articleItem: LearningResource) => {
                this.eventHub.emit(EventTypeEnum.COLLECTED, {
                  resourceId: articleItem.id,
                  resourceType: articleItem.type,
                  actionValue: !articleItem.isCollected
                });
              },
              onLiked: (articleItem: LearningResource) => {
                this.eventHub.emit(EventTypeEnum.LIKED, {
                  resourceId: articleItem.id,
                  resourceType: articleItem.type,
                  actionValue: !articleItem.isLiked
                });
              }
            })
              .onClick(() => this.handleClick(item))
              .reuseId('article')
          }
          .onAppear(() => {
            if (this.discoverModel.loadingArticleStatus !== LoadingStatus.LOADING &&
            this.discoverModel.hasNextArticle && index + 3 === this.articlesDataSource.dataArray.length) {
              this.discoverModel.loadMoreArticle().then(() => {
                this.userOperationChanged();
              });
            }
          })
          .padding({ right: $r('app.float.md_padding_margin') })
        }, (item: LearningResource, index: number) => JSON.stringify(item) + index)

        if (this.discoverModel.loadingArticleStatus === LoadingStatus.LOADING) {
          ListItemGroup({ header: LoadingMore() }) {
          }
        }
        if (!this.discoverModel.hasNextArticle) {
          ListItemGroup({ header: NoMore() }) {
          }
        }
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .nestedScroll({
        scrollForward: NestedScrollMode.PARENT_FIRST,
        scrollBackward: NestedScrollMode.SELF_FIRST
      })
      .lanes(new BreakpointType({ sm: 1, md: COUNT_TWO, lg: COUNT_THREE }).getValue(this.currentBreakpoint))
    }
    .padding({
      left: this.currentBreakpoint === BreakpointTypeEnum.SM ? $r('app.float.md_padding_margin') :
      $r('app.float.xxl_padding_margin'),
      right: this.currentBreakpoint === BreakpointTypeEnum.SM ? 0 : $r('app.float.md_padding_margin')
    })
  }
}