104.[HarmonyOS NEXT 实战案例:新闻阅读应用] 进阶篇 - 交互功能与状态管理

简介: 在基础篇中,我们学习了如何使用HarmonyOS NEXT的`RowSplit`组件构建新闻阅读应用的基本布局。在本篇教程中,我们将进一步探讨如何为新闻阅读应用添加交互功能和状态管理,包括新闻分类切换、新闻搜索、新闻收藏、新闻详情查看等功能,使界面更加动态和交互友好。

[HarmonyOS NEXT 实战案例:新闻阅读应用] 进阶篇 - 交互功能与状态管理

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

image.png

引言

在基础篇中,我们学习了如何使用HarmonyOS NEXT的RowSplit组件构建新闻阅读应用的基本布局。在本篇教程中,我们将进一步探讨如何为新闻阅读应用添加交互功能和状态管理,包括新闻分类切换、新闻搜索、新闻收藏、新闻详情查看等功能,使界面更加动态和交互友好。

状态管理概述

在交互式应用中,状态管理是至关重要的。HarmonyOS NEXT提供了多种状态管理机制,如@State@Prop@Link等装饰器。在本案例中,我们将主要使用@State装饰器来管理组件内部状态。

状态变量 类型 功能描述
selectedCategory string 当前选中的新闻分类
searchText string 搜索框中的文本
favoriteNews Set 收藏的新闻标题集合
selectedNews NewsItem \ null 当前选中的新闻项
isDetailMode boolean 是否处于新闻详情模式

代码实现

组件结构与状态定义

首先,我们在组件中定义状态变量:

@Component
export struct NewsReaderExample {
   
  @State selectedCategory: string = '推荐';
  @State searchText: string = '';
  @State favoriteNews: Set<string> = new Set<string>();
  @State selectedNews: NewsItem | null = null;
  @State isDetailMode: boolean = false;

  private categories: string[] = ['推荐', '科技', '体育', '财经', '娱乐', '健康'];
  @State newsData: NewsItem[] = [
    new NewsItem('HarmonyOS NEXT发布,带来全新的分布式体验', '科技日报', '10分钟前', $r('app.media.big30'), '科技'),
    new NewsItem('全国科技创新大会在北京召开', '新闻网', '30分钟前', $r('app.media.big31'), '科技'),
    new NewsItem('2023年全球智能手机市场分析报告', '科技评论', '1小时前', $r('app.media.big32'), '科技'),
    new NewsItem('国足最新一期集训名单公布', '体育新闻', '2小时前', $r('app.media.big33'), '体育'),
    new NewsItem('NBA季后赛最新战报', '体育周刊', '3小时前', $r('app.media.big34'), '体育'),
    new NewsItem('央行发布最新货币政策报告', '财经日报', '4小时前', $r('app.media.big35'), '财经'),
    new NewsItem('A股市场今日行情分析', '证券时报', '5小时前', $r('app.media.big36'), '财经'),
    new NewsItem('某流量明星最新电影票房破10亿', '娱乐周刊', '6小时前', $r('app.media.big37'), '娱乐'),
    new NewsItem('夏季养生指南:如何科学防暑', '健康时报', '7小时前', $r('app.media.big38'), '健康')
  ];

  build() {
   
    // 组件内容
  }
}

在这个组件中,我们添加了以下状态变量:

  1. searchText:用于存储搜索框中的文本
  2. favoriteNews:用于存储收藏的新闻标题集合
  3. selectedNews:用于存储当前选中的新闻项
  4. isDetailMode:用于标记是否处于新闻详情模式

外层容器

外层容器结构与基础篇相同,使用Column组件包含标题文本和主要内容区域:

Column() {
   
  Row() {
   
    Text('新闻阅读应用布局')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)

    Blank()

    if (this.isDetailMode) {
   
      Button('返回列表')
        .fontSize(14)
        .height(32)
        .backgroundColor('#007DFF')
        .onClick(() => {
   
          this.isDetailMode = false;
          this.selectedNews = null;
        })
    }
  }
  .width('100%')
  .margin({
    bottom: 10 })

  if (!this.isDetailMode) {
   
    RowSplit() {
   
      // 左侧新闻分类区域
      Column() {
   
        // 新闻分类内容
      }
      .width('25%')
      .backgroundColor('#f5f5f5')

      // 右侧新闻列表区域
      Column() {
   
        // 搜索框和新闻列表
      }
      .width('75%')
    }
    .height(600)
  } else {
   
    // 新闻详情页
    this.NewsDetailComponent(this.selectedNews!)
  }
}
.width('100%')
.padding(15)

在这个实现中,我们添加了以下功能:

  1. 在标题行添加了一个返回按钮,当处于新闻详情模式时显示
  2. 使用条件渲染,根据isDetailMode状态显示不同的内容:
    • isDetailModefalse时,显示分类和新闻列表
    • isDetailModetrue时,显示新闻详情

左侧新闻分类区域

左侧新闻分类区域与基础篇相同,使用Column组件包含一系列分类按钮:

Column() {
   
  Button('我的收藏')
    .width('90%')
    .height(50)
    .fontSize(16)
    .margin({
    top: 10, bottom: 10 })
    .borderRadius(8)
    .backgroundColor('#ff9500')
    .fontColor('#ffffff')
    .onClick(() => {
   
      this.selectedCategory = '收藏';
    })

  ForEach(this.categories, (category: string) => {
   
    Button(category)
      .width('90%')
      .height(50)
      .fontSize(16)
      .margin({
    top: 10 })
      .borderRadius(8)
      .backgroundColor(this.selectedCategory === category ? '#007DFF' : '#ffffff')
      .fontColor(this.selectedCategory === category ? '#ffffff' : '#333333')
      .onClick(() => {
   
        this.selectedCategory = category;
      })
  })
}
.width('25%')
.backgroundColor('#f5f5f5')
.padding({
    top: 10 })

在这个实现中,我们添加了一个"我的收藏"按钮,用于显示收藏的新闻。

右侧新闻列表区域

右侧新闻列表区域添加了搜索框和收藏功能:

Column() {
   
  // 搜索框
  Row() {
   
    TextInput({
    placeholder: '搜索新闻', text: this.searchText })
      .width('80%')
      .height(40)
      .backgroundColor('#f0f0f0')
      .borderRadius(20)
      .padding({
    left: 15, right: 15 })
      .onChange((value: string) => {
   
        this.searchText = value;
      })

    Button('搜索')
      .width('18%')
      .height(40)
      .fontSize(14)
      .margin({
    left: '2%' })
      .borderRadius(20)
      .backgroundColor('#007DFF')
      .onClick(() => {
   
        // 搜索逻辑
        console.info(`搜索:${
     this.searchText}`);
      })
  }
  .width('100%')
  .padding({
    left: 10, right: 10, top: 10, bottom: 10 })

  // 新闻列表
  List() {
   
    ForEach(this.getFilteredNews(), (item: NewsItem) => {
   
      ListItem() {
   
        this.NewsItemComponent(item)
      }
      .padding(10)
      .onClick(() => {
   
        this.selectedNews = item;
        this.isDetailMode = true;
      })
    })
  }
  .width('100%')
  .height('100%')
  .divider({
    strokeWidth: 1, color: '#f0f0f0', startMargin: 10, endMargin: 10 })
}
.width('75%')

在这个实现中,我们添加了以下功能:

  1. 搜索框:使用TextInput组件实现,添加onChange事件处理器更新searchText状态
  2. 搜索按钮:添加onClick事件处理器,输出搜索文本
  3. 新闻列表:使用getFilteredNews方法过滤新闻数据,添加onClick事件处理器,在点击时更新selectedNewsisDetailMode状态

新闻项组件

新闻项组件添加了收藏功能:

@Builder
private NewsItemComponent(item: NewsItem) {
   
  Row() {
   
    Column() {
   
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({
    bottom: 5 })
      Row() {
   
        Text(item.source)
          .fontSize(14)
          .fontColor('#666')
        Text(item.time)
          .fontSize(14)
          .fontColor('#666')
          .margin({
    left: 10 })

        Blank()

        Button(this.favoriteNews.has(item.title) ? '已收藏' : '收藏')
          .fontSize(12)
          .height(24)
          .backgroundColor(this.favoriteNews.has(item.title) ? '#ff9500' : '#f0f0f0')
          .fontColor(this.favoriteNews.has(item.title) ? '#ffffff' : '#333333')
          .borderRadius(12)
          .onClick((event: ClickEvent) => {
   
            event.stopPropagation();
            if (this.favoriteNews.has(item.title)) {
   
              this.favoriteNews.delete(item.title);
            } else {
   
              this.favoriteNews.add(item.title);
            }
            // 强制更新Set
            this.favoriteNews = new Set(this.favoriteNews);
          })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Start)

    Image(item.imageUrl)
      .width(100)
      .height(70)
      .objectFit(ImageFit.Cover)
      .borderRadius(5)
      .margin({
    left: 10 })
  }
  .width('100%')
}

在这个实现中,我们添加了一个收藏按钮,用于收藏或取消收藏新闻:

  1. 根据favoriteNews状态设置按钮的文本、背景色和文字颜色
  2. 添加onClick事件处理器,在点击时更新favoriteNews状态
  3. 使用event.stopPropagation()阻止事件冒泡,避免触发列表项的点击事件

新闻详情组件

为了显示新闻详情,我们定义了一个NewsDetailComponent方法:

@Builder
private NewsDetailComponent(item: NewsItem) {
   
  Column() {
   
    // 新闻标题
    Text(item.title)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .margin({
    bottom: 15 })

    // 新闻来源和时间
    Row() {
   
      Text(item.source)
        .fontSize(14)
        .fontColor('#666')
      Text(item.time)
        .fontSize(14)
        .fontColor('#666')
        .margin({
    left: 10 })

      Blank()

      Button(this.favoriteNews.has(item.title) ? '已收藏' : '收藏')
        .fontSize(14)
        .height(32)
        .backgroundColor(this.favoriteNews.has(item.title) ? '#ff9500' : '#f0f0f0')
        .fontColor(this.favoriteNews.has(item.title) ? '#ffffff' : '#333333')
        .borderRadius(16)
        .onClick(() => {
   
          if (this.favoriteNews.has(item.title)) {
   
            this.favoriteNews.delete(item.title);
          } else {
   
            this.favoriteNews.add(item.title);
          }
          // 强制更新Set
          this.favoriteNews = new Set(this.favoriteNews);
        })
    }
    .width('100%')
    .margin({
    bottom: 20 })

    // 新闻图片
    Image(item.imageUrl)
      .width('100%')
      .height(200)
      .objectFit(ImageFit.Cover)
      .borderRadius(8)
      .margin({
    bottom: 20 })

    // 新闻内容
    Text(this.generateNewsContent(item))
      .fontSize(16)
      .lineHeight(24)
      .margin({
    bottom: 20 })

    // 相关新闻
    Text('相关新闻')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .margin({
    bottom: 10 })

    List() {
   
      ForEach(this.getRelatedNews(item), (relatedItem: NewsItem) => {
   
        ListItem() {
   
          Row() {
   
            Text(relatedItem.title)
              .fontSize(14)
              .layoutWeight(1)

            Text(relatedItem.time)
              .fontSize(12)
              .fontColor('#666')
          }
          .width('100%')
          .padding({
    top: 8, bottom: 8 })
        }
        .onClick(() => {
   
          this.selectedNews = relatedItem;
        })
      })
    }
    .width('100%')
    .height(150)
    .divider({
    strokeWidth: 1, color: '#f0f0f0' })
  }
  .width('100%')
  .alignItems(HorizontalAlign.Start)
  .padding(15)
}

在这个方法中,我们显示了新闻的详细信息,包括标题、来源、时间、图片、内容和相关新闻。

辅助方法

为了支持上述功能,我们添加了以下辅助方法:

private getFilteredNews(): NewsItem[] {
   
  // 根据选中的分类和搜索文本过滤新闻
  let filteredNews = this.newsData;

  // 根据分类过滤
  if (this.selectedCategory !== '推荐') {
   
    if (this.selectedCategory === '收藏') {
   
      // 显示收藏的新闻
      filteredNews = filteredNews.filter(item => this.favoriteNews.has(item.title));
    } else {
   
      // 显示特定分类的新闻
      filteredNews = filteredNews.filter(item => item.category === this.selectedCategory);
    }
  }

  // 根据搜索文本过滤
  if (this.searchText.trim() !== '') {
   
    const searchLower = this.searchText.toLowerCase();
    filteredNews = filteredNews.filter(item => 
      item.title.toLowerCase().includes(searchLower) || 
      item.source.toLowerCase().includes(searchLower) ||
      item.category.toLowerCase().includes(searchLower)
    );
  }

  return filteredNews;
}

private getRelatedNews(currentNews: NewsItem): NewsItem[] {
   
  // 获取与当前新闻相关的新闻(同一分类的其他新闻)
  return this.newsData
    .filter(item => item.category === currentNews.category && item.title !== currentNews.title)
    .slice(0, 3); // 最多显示3条相关新闻
}

private generateNewsContent(item: NewsItem): string {
   
  // 生成新闻内容(实际应用中应该从后端获取)
  return `这是一篇关于${
     item.category}的新闻。${
     item.title}。这里是新闻的详细内容,包含了事件的起因、经过和结果。\n\n这是第二段落,提供了更多的背景信息和相关数据。根据最新的统计数据显示,这一领域的发展趋势非常明显。\n\n这是第三段落,包含了专家的观点和分析。多位专家认为,这一事件将对行业产生深远的影响。`;
}

这些方法用于过滤新闻数据、获取相关新闻和生成新闻内容,使代码更加模块化和可维护。

交互功能分析

分类切换

分类切换通过selectedCategory状态变量来控制:

@State selectedCategory: string = '推荐';

// 分类按钮
Button(category)
  // 按钮属性
  .backgroundColor(this.selectedCategory === category ? '#007DFF' : '#ffffff')
  .fontColor(this.selectedCategory === category ? '#ffffff' : '#333333')
  .onClick(() => {
   
    this.selectedCategory = category;
  })

当用户点击分类按钮时,selectedCategory状态会更新为选中的分类,按钮样式和新闻列表也会相应地变化。

新闻搜索

新闻搜索通过searchText状态变量来控制:

@State searchText: string = '';

// 搜索框
TextInput({
    placeholder: '搜索新闻', text: this.searchText })
  // 输入框属性
  .onChange((value: string) => {
   
    this.searchText = value;
  })

// 搜索按钮
Button('搜索')
  // 按钮属性
  .onClick(() => {
   
    // 搜索逻辑
    console.info(`搜索:${
     this.searchText}`);
  })

当用户在搜索框中输入文本时,searchText状态会更新,新闻列表也会根据搜索文本进行过滤。

新闻收藏

新闻收藏通过favoriteNews状态变量来控制:

@State favoriteNews: Set<string> = new Set<string>();

// 收藏按钮
Button(this.favoriteNews.has(item.title) ? '已收藏' : '收藏')
  // 按钮属性
  .backgroundColor(this.favoriteNews.has(item.title) ? '#ff9500' : '#f0f0f0')
  .fontColor(this.favoriteNews.has(item.title) ? '#ffffff' : '#333333')
  .onClick((event: ClickEvent) => {
   
    event.stopPropagation();
    if (this.favoriteNews.has(item.title)) {
   
      this.favoriteNews.delete(item.title);
    } else {
   
      this.favoriteNews.add(item.title);
    }
    // 强制更新Set
    this.favoriteNews = new Set(this.favoriteNews);
  })

当用户点击收藏按钮时,新闻标题会被添加到或从favoriteNews集合中移除,按钮样式也会相应地变化。

新闻详情查看

新闻详情查看通过selectedNewsisDetailMode状态变量来控制:

@State selectedNews: NewsItem | null = null;
@State isDetailMode: boolean = false;

// 新闻列表项
ListItem() {
   
  this.NewsItemComponent(item)
}
.padding(10)
.onClick(() => {
   
  this.selectedNews = item;
  this.isDetailMode = true;
})

// 返回按钮
Button('返回列表')
  // 按钮属性
  .onClick(() => {
   
    this.isDetailMode = false;
    this.selectedNews = null;
  })

// 条件渲染
if (!this.isDetailMode) {
   
  // 显示分类和新闻列表
} else {
   
  // 显示新闻详情
  this.NewsDetailComponent(this.selectedNews!)
}

当用户点击新闻列表项时,selectedNews状态会更新为选中的新闻项,isDetailMode状态会设置为true,界面会切换到新闻详情模式。当用户点击返回按钮时,isDetailMode状态会设置为falseselectedNews状态会设置为null,界面会切换回新闻列表模式。

状态管理技巧

状态变量的选择

在本案例中,我们使用@State装饰器来管理组件内部状态。@State装饰器适用于组件内部的状态管理,当状态变化时,组件会自动重新渲染。

@State selectedCategory: string = '推荐';
@State searchText: string = '';
@State favoriteNews: Set<string> = new Set<string>();
@State selectedNews: NewsItem | null = null;
@State isDetailMode: boolean = false;

条件渲染

我们使用条件渲染来根据状态显示不同的内容:

// 根据isDetailMode状态显示不同的内容
if (!this.isDetailMode) {
   
  // 显示分类和新闻列表
} else {
   
  // 显示新闻详情
  this.NewsDetailComponent(this.selectedNews!)
}

// 根据favoriteNews状态设置按钮的文本和样式
Button(this.favoriteNews.has(item.title) ? '已收藏' : '收藏')
  .backgroundColor(this.favoriteNews.has(item.title) ? '#ff9500' : '#f0f0f0')
  .fontColor(this.favoriteNews.has(item.title) ? '#ffffff' : '#333333')

状态更新

我们在事件处理器中更新状态:

// 更新selectedCategory状态
.onClick(() => {
   
  this.selectedCategory = category;
})

// 更新searchText状态
.onChange((value: string) => {
   
  this.searchText = value;
})

// 更新favoriteNews状态
.onClick((event: ClickEvent) => {
   
  event.stopPropagation();
  if (this.favoriteNews.has(item.title)) {
   
    this.favoriteNews.delete(item.title);
  } else {
   
    this.favoriteNews.add(item.title);
  }
  // 强制更新Set
  this.favoriteNews = new Set(this.favoriteNews);
})

// 更新selectedNews和isDetailMode状态
.onClick(() => {
   
  this.selectedNews = item;
  this.isDetailMode = true;
})

当状态更新时,组件会自动重新渲染,显示最新的状态。

数据过滤

我们使用辅助方法来过滤数据:

private getFilteredNews(): NewsItem[] {
   
  // 根据选中的分类和搜索文本过滤新闻
  let filteredNews = this.newsData;

  // 根据分类过滤
  if (this.selectedCategory !== '推荐') {
   
    if (this.selectedCategory === '收藏') {
   
      // 显示收藏的新闻
      filteredNews = filteredNews.filter(item => this.favoriteNews.has(item.title));
    } else {
   
      // 显示特定分类的新闻
      filteredNews = filteredNews.filter(item => item.category === this.selectedCategory);
    }
  }

  // 根据搜索文本过滤
  if (this.searchText.trim() !== '') {
   
    const searchLower = this.searchText.toLowerCase();
    filteredNews = filteredNews.filter(item => 
      item.title.toLowerCase().includes(searchLower) || 
      item.source.toLowerCase().includes(searchLower) ||
      item.category.toLowerCase().includes(searchLower)
    );
  }

  return filteredNews;
}

这个方法根据选中的分类和搜索文本过滤新闻数据,返回符合条件的新闻列表。

总结

在本教程中,我们学习了如何为新闻阅读应用添加交互功能和状态管理,包括新闻分类切换、新闻搜索、新闻收藏、新闻详情查看等功能。
通过使用HarmonyOS NEXT的状态管理机制,我们可以轻松地实现这些交互功能,使界面更加动态和交互友好。我们还学习了如何使用条件渲染来根据状态显示不同的内容,以及如何在事件处理器中更新状态。

相关文章
|
2月前
|
监控 JavaScript 编译器
从“天书”到源码:HarmonyOS NEXT 崩溃堆栈解析实战指南
本文详解如何利用 hiAppEvent 监控并获取 sourcemap、debug so 等核心产物,剖析了 hstack 工具如何将混淆的 Native 与 ArkTS 堆栈还原为源码,助力开发者掌握异常分析方法,提升应用稳定性。
432 40
|
3月前
|
开发者 容器
鸿蒙应用开发从入门到实战(十四):ArkUI组件Column&Row&线性布局
ArkUI提供了丰富的系统组件,用于制作鸿蒙原生应用APP的UI,本文主要讲解Column和Row组件的使用以及线性布局的方法。
310 12
|
3月前
|
API 数据处理
鸿蒙应用开发从入门到实战(十三):ArkUI组件Slider&Progress
ArkUI提供了丰富的系统组件,用于制作鸿蒙原生应用APP的UI,本文主要讲解滑块Slider和进度条Progress组件的使用。
192 1
|
3月前
|
JavaScript 开发者 索引
鸿蒙应用开发从入门到实战(九):ArkTS渲染控制
ArkTS拓展了TypeScript,可以结合ArkUI进行渲染控制,是的界面设计具有可编程性。本文简要描述鸿蒙应用开发中的条件渲染和循环渲染。
186 5
|
2月前
|
移动开发 前端开发 Android开发
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
304 12
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
2月前
|
移动开发 JavaScript 应用服务中间件
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
270 5
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
2月前
|
移动开发 Rust JavaScript
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
631 4
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
3月前
|
数据安全/隐私保护 开发者
鸿蒙应用开发从入门到实战(十一):ArkUI组件Text&TextInput
ArkUI提供了丰富的系统组件,用于制作鸿蒙原生应用APP的UI,本文主要讲解文本组件Text和TextInput的使用。
303 3
|
2月前
|
移动开发 Android开发
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
165 0
|
3月前
|
存储 缓存 5G
鸿蒙 HarmonyOS NEXT端云一体化开发-云存储篇
本文介绍用户登录后获取昵称、头像的方法,包括通过云端API和AppStorage两种方式,并实现上传头像至云存储及更新用户信息。同时解决图片缓存问题,添加上传进度提示,支持自动登录判断,提升用户体验。
186 1

热门文章

最新文章