代码结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
├──HttpServerOfNews // Node服务端代码 ├──entry/src/main/ets // ArkTS代码区 │ ├──common │ │ ├──constant │ │ │ └──CommonConstant.ets // 公共常量类 │ │ └──utils │ │ ├──HttpUtil.ets // 网络请求方法 │ │ ├──Logger.ets // 日志打印工具 │ │ ├──PullDownRefresh.ets // 下拉刷新方法 │ │ └──PullUpLoadMore.ets // 上拉加载更多方法 │ ├──entryability │ │ └──EntryAbility.ets // 程序入口类 │ ├──pages │ │ └──Index.ets // 入口文件 │ ├──view │ │ ├──CustomRefreshLoadLayout.ets // 下拉刷新、上拉加载布局文件 │ │ ├──LoadMoreLayout.ets // 上拉加载布局封装 │ │ ├──NewsItem.ets // 新闻数据 │ │ ├──NewsList.ets // 新闻列表 │ │ ├──NoMoreLayout.ets // 没有更多数据封装 │ │ ├──RefreshLayout.ets // 下拉刷新布局封装 │ │ └──TabBar.ets // 新闻类型页签 │ └──viewmodel │ ├──NewsData.ets // 新闻数据实体类 │ ├──NewsModel.ets // 新闻数据模块信息 │ ├──NewsTypeModel.ets // 新闻类型实体类 │ ├──NewsViewModel.ets // 新闻数据获取模块 │ └──ResponseResult.ets // 请求结果实体类 └──entry/src/main/resources // 资源文件目录 |
启动服务
1 2 3 |
$ cd HttpServerOfNews $ npm install $ npm start |
代码
common
CommonConstant.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
import NewsTypeModel from '../../viewmodel/NewsTypeModel' export class CommonConstant { static readonly SERVER: string = 'http://**.**.**.**:9588' static readonly GET_NEWS_TYPE: string = 'news/getNewsType' static readonly GET_NEWS_LIST: string = 'news/getNewsList' static readonly SERVER_CODE_SUCCESS: string = 'success' static readonly Y_OFF_SET_COEFFICIENT: number = 0.1 static readonly PAGE_SIZE: number = 4 static readonly CUSTOM_LAYOUT_HEIGHT: number = 70 static readonly HTTP_CODE_200: number = 200 static readonly DELAY_ANIMATION_DURATION: number = 300 static readonly DELAY_TIME: number = 1000 static readonly ANIMATION_DURATION: number = 2000 static readonly HTTP_READ_TIMEOUT: number = 10000 static readonly FULL_WIDTH: string = '100%' static readonly FULL_HEIGHT: string = '100%' /** * The TabBars constants. */ static readonly TabBars_UN_SELECT_TEXT_FONT_SIZE: number = 18 static readonly TabBars_SELECT_TEXT_FONT_SIZE: number = 24 static readonly TabBars_UN_SELECT_TEXT_FONT_WEIGHT: number = 400 static readonly TabBars_SELECT_TEXT_FONT_WEIGHT: number = 700 static readonly TabBars_BAR_HEIGHT: string = '7.2%' static readonly TabBars_HORIZONTAL_PADDING: string = '2.2%' static readonly TabBars_BAR_WIDTH: string = '100%' static readonly TabBars_DEFAULT_NEWS_TYPES: Array<NewsTypeModel> = [ { id: 0, name: '全国' }, { id: 1, name: '国内' }, { id: 2, name: '国际' }, { id: 3, name: '娱乐' }, { id: 4, name: '军事' }, { id: 5, name: '体育' }, { id: 6, name: '科技' }, { id: 7, name: '财经' } ] /** * The NewsListConstant constants. */ static readonly NewsListConstant_LIST_DIVIDER_STROKE_WIDTH: number = 0.5 static readonly NewsListConstant_GET_TAB_DATA_TYPE_ONE: number = 1 static readonly NewsListConstant_ITEM_BORDER_RADIUS: number = 16 static readonly NewsListConstant_NONE_IMAGE_SIZE: number = 120 static readonly NewsListConstant_NONE_TEXT_OPACITY: number = 0.6 static readonly NewsListConstant_NONE_TEXT_SIZE: number = 16 static readonly NewsListConstant_NONE_TEXT_MARGIN: number = 12 static readonly NewsListConstant_ITEM_MARGIN_TOP: string = '1.5%' static readonly NewsListConstant_LIST_MARGIN_LEFT: string = '3.3%' static readonly NewsListConstant_LIST_MARGIN_RIGHT: string = '3.3%' static readonly NewsListConstant_ITEM_HEIGHT: string = '32%' static readonly NewsListConstant_LIST_WIDTH: string = '93.3%' /** * The NewsTitle constants. */ static readonly NewsTitle_TEXT_MAX_LINES: number = 3 static readonly NewsTitle_TEXT_FONT_SIZE: number = 20 static readonly NewsTitle_TEXT_FONT_WEIGHT: number = 500 static readonly NewsTitle_TEXT_MARGIN_LEFT: string = '2.4%' static readonly NewsTitle_TEXT_MARGIN_TOP: string = '7.2%' static readonly NewsTitle_TEXT_HEIGHT: string = '9.6%' static readonly NewsTitle_TEXT_WIDTH: string = '78.6%' static readonly NewsTitle_IMAGE_MARGIN_LEFT: string = '3.5%' static readonly NewsTitle_IMAGE_MARGIN_TOP: string = '7.9%' static readonly NewsTitle_IMAGE_HEIGHT: string = '8.9%' static readonly NewsTitle_IMAGE_WIDTH: string = '11.9%' /** * The NewsContent constants. */ static readonly NewsContent_WIDTH: string = '93%' static readonly NewsContent_HEIGHT: string = '16.8%' static readonly NewsContent_MARGIN_LEFT: string = '3.5%' static readonly NewsContent_MARGIN_TOP: string = '3.4%' static readonly NewsContent_MAX_LINES: number = 2 static readonly NewsContent_FONT_SIZE: number = 15 /** * The NewsSource constants. */ static readonly NewsSource_MAX_LINES: number = 1 static readonly NewsSource_FONT_SIZE: number = 12 static readonly NewsSource_MARGIN_LEFT: string = '3.5%' static readonly NewsSource_MARGIN_TOP: string = '3.4%' static readonly NewsSource_HEIGHT: string = '7.2%' static readonly NewsSource_WIDTH: string = '93%' /** * The NewsGrid constants. */ static readonly NewsGrid_MARGIN_LEFT: string = '3.5%' static readonly NewsGrid_MARGIN_RIGHT: string = '3.5%' static readonly NewsGrid_MARGIN_TOP: string = '5.1%' static readonly NewsGrid_WIDTH: string = '93%' static readonly NewsGrid_HEIGHT: string = '31.5%' static readonly NewsGrid_ASPECT_RATIO: number = 4 static readonly NewsGrid_COLUMNS_GAP: number = 5 static readonly NewsGrid_ROWS_TEMPLATE: string = '1fr' static readonly NewsGrid_IMAGE_BORDER_RADIUS: number = 8 /** * The RefreshLayout constants. */ static readonly RefreshLayout_MARGIN_LEFT: string = '40%' static readonly RefreshLayout_TEXT_MARGIN_BOTTOM: number = 1 static readonly RefreshLayout_TEXT_MARGIN_LEFT: number = 7 static readonly RefreshLayout_TEXT_FONT_SIZE: number = 17 static readonly RefreshLayout_IMAGE_WIDTH: number = 18 static readonly RefreshLayout_IMAGE_HEIGHT: number = 18 /** * The NoMoreLayout constants. */ static readonly NoMoreLayoutConstant_NORMAL_PADDING: number = 8 static readonly NoMoreLayoutConstant_TITLE_FONT: string = '16fp' /** * The RefreshConstant constants. */ static readonly RefreshConstant_DELAY_PULL_DOWN_REFRESH: number = 50 static readonly RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME: number = 150 static readonly RefreshConstant_DELAY_SHRINK_ANIMATION_TIME: number = 500 } /** * The refresh state enum. */ export const enum RefreshState { DropDown = 0, Release = 1, Refreshing = 2, Success = 3, Fail = 4 } /** * The newsList state enum. */ export const enum PageState { Loading = 0, Success = 1, Fail = 2 } /** * The request content type enum. */ export const enum ContentType { JSON = 'application/json' } |
HttpUtil.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { http } from '@kit.NetworkKit' import ResponseResult from '../../viewmodel/ResponseResult' import { CommonConstant as Const, ContentType } from '../constant/CommonConstant' import Logger from './Logger' /** * Initiates an HTTP request to a given URL. * @param url * @returns */ export function httpRequestGet(url: string): Promise<ResponseResult> { let httpRequest = http.createHttp() let responseResult = httpRequest.request(url, { method: http.RequestMethod.GET, readTimeout: Const.HTTP_READ_TIMEOUT, header: { 'Content-Type': ContentType.JSON }, connectTimeout: Const.HTTP_READ_TIMEOUT, extraData: {} }) let serverData: ResponseResult = new ResponseResult() // Processes the data and returns. return responseResult.then((value: http.HttpResponse) => { if (value.responseCode === Const.HTTP_CODE_200) { // Obtains the returned data. let result = `${value.result}` let resultJson: ResponseResult = JSON.parse(result) if (resultJson.code === Const.SERVER_CODE_SUCCESS) { serverData.data = resultJson.data } serverData.code = resultJson.code serverData.msg = resultJson.msg } else { serverData.msg = `${$r('app.string.http_error_message')}&${value.responseCode}` } // Logger.info(JSON.stringify(value)) return serverData }).catch(() => { serverData.msg = $r('app.string.http_error_message') return serverData }) } |
Logger.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import { hilog } from '@kit.PerformanceAnalysisKit' const LOGGER_PREFIX: string = 'News Release' class Logger { private domain: number private prefix: string // format Indicates the log format string. private format: string = '%{public}s, %{public}s' constructor(prefix: string = '', domain: number = 0xFF00) { this.prefix = prefix this.domain = domain } debug(...args: string[]): void { hilog.debug(this.domain, this.prefix, this.format, args) } info(...args: string[]): void { hilog.info(this.domain, this.prefix, this.format, args) } warn(...args: string[]): void { hilog.warn(this.domain, this.prefix, this.format, args) } error(...args: string[]): void { hilog.error(this.domain, this.prefix, this.format, args) } } export default new Logger(LOGGER_PREFIX) |
PullDownRefresh.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
import { promptAction } from '@kit.ArkUI'; import { touchMoveLoadMore, touchUpLoadMore } from './PullUpLoadMore'; import { CommonConstant as Const, RefreshState } from '../constant/CommonConstant'; import NewsViewModel from '../../viewmodel/NewsViewModel'; import { NewsData } from '../../viewmodel/NewsData'; import NewsModel from '../../viewmodel/NewsModel'; export function listTouchEvent(that: NewsModel, event: TouchEvent) { switch (event.type) { case TouchType.Down: that.downY = event.touches[0].y; that.lastMoveY = event.touches[0].y; break; case TouchType.Move: if ((that.isRefreshing === true) || (that.isLoading === true)) { return; } let isDownPull = event.touches[0].y - that.lastMoveY > 0; if (((isDownPull === true) || (that.isPullRefreshOperation === true)) && (that.isCanLoadMore === false)) { // Finger movement, processing pull-down refresh. touchMovePullRefresh(that, event); } else { // Finger movement, processing load more. touchMoveLoadMore(that, event); } that.lastMoveY = event.touches[0].y; break; case TouchType.Cancel: break; case TouchType.Up: if ((that.isRefreshing === true) || (that.isLoading === true)) { return; } if ((that.isPullRefreshOperation === true)) { // Lift your finger and pull down to refresh. touchUpPullRefresh(that); } else { // Fingers up, handle loading more. touchUpLoadMore(that); } break; default: break; } } export function touchMovePullRefresh(that: NewsModel, event: TouchEvent) { if (that.startIndex === 0) { that.isPullRefreshOperation = true; let height = vp2px(that.pullDownRefreshHeight); that.offsetY = event.touches[0].y - that.downY; // The sliding offset is greater than the pull-down refresh layout height, and the refresh condition is met. if (that.offsetY >= height) { pullRefreshState(that, RefreshState.Release); that.offsetY = height + that.offsetY * Const.Y_OFF_SET_COEFFICIENT; } else { pullRefreshState(that, RefreshState.DropDown); } if (that.offsetY < 0) { that.offsetY = 0; that.isPullRefreshOperation = false; } } } export function touchUpPullRefresh(that: NewsModel) { if (that.isCanRefresh === true) { that.offsetY = vp2px(that.pullDownRefreshHeight); pullRefreshState(that, RefreshState.Refreshing); that.currentPage = 1; setTimeout(() => { let self = that; NewsViewModel.getNewsList(that.currentPage, that.pageSize, Const.GET_NEWS_LIST).then((data: NewsData[]) => { if (data.length === that.pageSize) { self.hasMore = true; self.currentPage++; } else { self.hasMore = false; } self.newsData = data; closeRefresh(self, true); }).catch((err: string | Resource) => { promptAction.showToast({ message: err }); // closeRefresh.call(self, false); closeRefresh(self, false); }); }, Const.DELAY_TIME); } else { closeRefresh(that, false); } } export function pullRefreshState(that: NewsModel, state: number) { switch (state) { case RefreshState.DropDown: that.pullDownRefreshText = $r('app.string.pull_down_refresh_text'); that.pullDownRefreshImage = $r("app.media.ic_pull_down_refresh"); that.isCanRefresh = false; that.isRefreshing = false; that.isVisiblePullDown = true; break; case RefreshState.Release: that.pullDownRefreshText = $r('app.string.release_refresh_text'); that.pullDownRefreshImage = $r("app.media.ic_pull_up_refresh"); that.isCanRefresh = true; that.isRefreshing = false; break; case RefreshState.Refreshing: that.offsetY = vp2px(that.pullDownRefreshHeight); that.pullDownRefreshText = $r('app.string.refreshing_text'); that.pullDownRefreshImage = $r("app.media.ic_pull_up_load"); that.isCanRefresh = true; that.isRefreshing = true; break; case RefreshState.Success: that.pullDownRefreshText = $r('app.string.refresh_success_text'); that.pullDownRefreshImage = $r("app.media.ic_succeed_refresh"); that.isCanRefresh = true; that.isRefreshing = true; break; case RefreshState.Fail: that.pullDownRefreshText = $r('app.string.refresh_fail_text'); that.pullDownRefreshImage = $r("app.media.ic_fail_refresh"); that.isCanRefresh = true; that.isRefreshing = true; break; default: break; } } export function closeRefresh(that: NewsModel, isRefreshSuccess: boolean) { let self = that; setTimeout(() => { let delay = Const.RefreshConstant_DELAY_PULL_DOWN_REFRESH; if (self.isCanRefresh === true) { pullRefreshState(that, isRefreshSuccess ? RefreshState.Success : RefreshState.Fail); delay = Const.RefreshConstant_DELAY_SHRINK_ANIMATION_TIME; } animateTo({ duration: Const.RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME, delay: delay, onFinish: () => { pullRefreshState(that, RefreshState.DropDown); self.isVisiblePullDown = false; self.isPullRefreshOperation = false; } }, () => { self.offsetY = 0; }) }, self.isCanRefresh ? Const.DELAY_ANIMATION_DURATION : 0); } |
PullUpLoadMore.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import { promptAction } from '@kit.ArkUI'; import { CommonConstant as Const } from '../constant/CommonConstant'; import NewsViewModel from '../../viewmodel/NewsViewModel'; import { NewsData } from '../../viewmodel/NewsData'; import NewsModel from '../../viewmodel/NewsModel'; export function touchMoveLoadMore(that: NewsModel, event: TouchEvent) { if (that.endIndex === that.newsData.length - 1 || that.endIndex === that.newsData.length) { that.offsetY = event.touches[0].y - that.downY; if (Math.abs(that.offsetY) > vp2px(that.pullUpLoadHeight) / 2) { that.isCanLoadMore = true; that.isVisiblePullUpLoad = true; that.offsetY = -vp2px(that.pullUpLoadHeight) + that.offsetY * Const.Y_OFF_SET_COEFFICIENT; } } } export function touchUpLoadMore(that: NewsModel) { let self = that; animateTo({ duration: Const.ANIMATION_DURATION, }, () => { self.offsetY = 0; }) if ((self.isCanLoadMore === true) && (self.hasMore === true)) { self.isLoading = true; setTimeout(() => { closeLoadMore(that); NewsViewModel.getNewsList(self.currentPage, self.pageSize, Const.GET_NEWS_LIST).then((data: NewsData[]) => { if (data.length === self.pageSize) { self.currentPage++; self.hasMore = true; } else { self.hasMore = false; } self.newsData = self.newsData.concat(data); }).catch((err: string | Resource) => { promptAction.showToast({ message: err }); }) }, Const.DELAY_TIME); } else { closeLoadMore(self); } } export function closeLoadMore(that: NewsModel) { that.isCanLoadMore = false; that.isLoading = false; that.isVisiblePullUpLoad = false; } |
entryability
EntryAbility.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? ''); } onDestroy(): void | Promise<void> { hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); } onWindowStageCreate(windowStage: window.WindowStage): void { // Main window is created, set main page for this ability hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); windowStage.loadContent('pages/Index', (err, data) => { if (err.code) { hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.ERROR); hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); }); } onWindowStageDestroy(): void { // Main window is destroyed, release UI related resources hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); } onForeground(): void { // Ability has brought to foreground hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); } onBackground(): void { // Ability has back to background hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO); hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); } } |
pages
Index.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { CommonConstant as Const } from '../common/constant/CommonConstant' import TabBar from '../view/TabBar' /** * The Index is the entry point of the application. */ @Entry @Component struct Index { build() { Column() { TabBar() } .width(Const.FULL_WIDTH) .backgroundColor($r('app.color.listColor')) .justifyContent(FlexAlign.Center) } } |
view
CustomRefreshLoadLayout.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsData' import { CommonConstant as Const } from '../common/constant/CommonConstant' /** * Custom layout to show refresh or load. */ @Component export default struct CustomLayout { @ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass build() { Row() { Image(this.customRefreshLoadClass.imageSrc) .width(Const.RefreshLayout_IMAGE_WIDTH) .height(Const.RefreshLayout_IMAGE_HEIGHT) Text(this.customRefreshLoadClass.textValue) .fontSize(Const.RefreshLayout_TEXT_FONT_SIZE) .textAlign(TextAlign.Center) } .clip(true) .width(Const.FULL_WIDTH) .height(this.customRefreshLoadClass.heightValue) .justifyContent(FlexAlign.Center) } } |
LoadMoreLayout.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout'; import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsData'; /** * The load more layout component. */ @Component export default struct LoadMoreLayout { @ObjectLink loadMoreLayoutClass: CustomRefreshLoadLayoutClass; build() { Column() { if (this.loadMoreLayoutClass.isVisible) { CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible, this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, this.loadMoreLayoutClass.heightValue) }) } else { CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible, this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, 0) }) } } } } |
NewsItem.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
import { NewsData, NewsFile } from '../viewmodel/NewsData' import { CommonConstant as Const } from '../common/constant/CommonConstant' @Component export default struct NewsItem { private newsData: NewsData = new NewsData() build() { Column() { Row() { Image($r('app.media.news')) .width(Const.NewsTitle_IMAGE_WIDTH) .height(Const.NewsTitle_IMAGE_HEIGHT) .objectFit(ImageFit.Fill) .margin({ top: Const.NewsTitle_IMAGE_MARGIN_TOP, left: Const.NewsTitle_IMAGE_MARGIN_LEFT }) Text(this.newsData.title) .fontSize(Const.NewsTitle_TEXT_FONT_SIZE) .fontColor($r('app.color.fontColor_text')) .width(Const.NewsTitle_TEXT_WIDTH) .height(Const.NewsTitle_TEXT_HEIGHT) .maxLines(Const.NewsTitle_TEXT_MAX_LINES) .textOverflow({ overflow: TextOverflow.Ellipsis }) .fontWeight(Const.NewsTitle_TEXT_FONT_WEIGHT) .margin({ left: Const.NewsTitle_TEXT_MARGIN_LEFT, top: Const.NewsTitle_TEXT_MARGIN_TOP }) } Text(this.newsData.content) .fontSize(Const.NewsContent_FONT_SIZE) .fontColor($r('app.color.fontColor_text')) .width(Const.NewsContent_WIDTH) .height(Const.NewsContent_HEIGHT) .maxLines(Const.NewsContent_MAX_LINES) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ left: Const.NewsContent_MARGIN_LEFT, top: Const.NewsContent_MARGIN_TOP }) Grid() { ForEach(this.newsData.imagesUrl, (itemImg: NewsFile) => { GridItem() { Image(Const.SERVER + itemImg.url) .objectFit(ImageFit.Cover) .borderRadius(Const.NewsGrid_IMAGE_BORDER_RADIUS) } }, (itemImg: NewsFile, index?: number) => JSON.stringify(itemImg) + index) } .columnsTemplate('1fr '.repeat(this.newsData.imagesUrl.length)) .rowsTemplate(Const.NewsGrid_ROWS_TEMPLATE) .columnsGap(Const.NewsGrid_COLUMNS_GAP) .width(Const.NewsGrid_WIDTH) .height(Const.NewsGrid_HEIGHT) .margin({ left: Const.NewsGrid_MARGIN_LEFT, top: Const.NewsGrid_MARGIN_TOP, right: Const.NewsGrid_MARGIN_RIGHT }) Text(this.newsData.source) .width(Const.NewsSource_WIDTH) .height(Const.NewsSource_HEIGHT) .fontSize(Const.NewsSource_FONT_SIZE) .fontColor($r('app.color.fontColor_text2')) .maxLines(Const.NewsSource_MAX_LINES) .textOverflow({ overflow: TextOverflow.None }) .margin({ left: Const.NewsSource_MARGIN_LEFT, top: Const.NewsSource_MARGIN_TOP }) } .alignItems(HorizontalAlign.Start) } } |
NewsList.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
import NewsModel from '../viewmodel/NewsModel' import NewsViewModel from '../viewmodel/NewsViewModel' import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant' import { CustomRefreshLoadLayoutClass, NewsData } from '../viewmodel/NewsData' import { promptAction } from '@kit.ArkUI' import Logger from '../common/utils/Logger' import NewsItem from './NewsItem' import RefreshLayout from './RefreshLayout' import { listTouchEvent } from '../common/utils/PullDownRefresh' import CustomRefreshLoadLayout from './CustomRefreshLoadLayout' import LoadMoreLayout from './LoadMoreLayout' import NoMoreLayout from './NoMoreLayout' @Component export default struct NewsList { @State newsModel: NewsModel = new NewsModel() @Watch('changeCategory') @Link currentIndex: number changeCategory() { this.newsModel.currentPage = 1; NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST) .then((data: NewsData[]) => { this.newsModel.pageState = PageState.Success; if (data.length === this.newsModel.pageSize) { this.newsModel.currentPage++; this.newsModel.hasMore = true; } else { this.newsModel.hasMore = false; } this.newsModel.newsData = data; }) .catch((err: string | Resource) => { promptAction.showToast({ message: err, duration: Const.ANIMATION_DURATION }); this.newsModel.pageState = PageState.Fail; }); } aboutToAppear(): void { this.changeCategory() } build() { Column() { if (this.newsModel.pageState === PageState.Success) { this.ListLayout() } else if (this.newsModel.pageState === PageState.Loading) { this.LoadingLayout() } else { this.FailLayout() } } .width(Const.FULL_WIDTH) .height(Const.FULL_HEIGHT) .justifyContent(FlexAlign.Center) .onTouch((event: TouchEvent | undefined) => { if (event) { if (this.newsModel.pageState === PageState.Success) { listTouchEvent(this.newsModel, event) } } }) } @Builder LoadingLayout() { CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass( true, $r('app.media.ic_pull_up_load'), $r('app.string.pull_up_load_text'), this.newsModel.pullDownRefreshHeight ) }) } @Builder ListLayout() { List() { ListItem() { RefreshLayout({ refreshLayoutClass: new CustomRefreshLoadLayoutClass( this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage, this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight ) }) } ForEach(this.newsModel.newsData, (item: NewsData) => { ListItem() { NewsItem({ newsData: item }) } .height(Const.NewsListConstant_ITEM_HEIGHT) .backgroundColor($r('app.color.white')) .margin({ top: Const.NewsListConstant_ITEM_MARGIN_TOP }) .borderRadius(Const.NewsListConstant_ITEM_BORDER_RADIUS) }, (item: NewsData, index?: number) => JSON.stringify(item) + index) ListItem() { if (this.newsModel.hasMore) { LoadMoreLayout({ loadMoreLayoutClass: new CustomRefreshLoadLayoutClass( this.newsModel.isVisiblePullUpLoad, this.newsModel.pullUpLoadImage, this.newsModel.pullUpLoadText, this.newsModel.pullUpLoadHeight ) }) } else { NoMoreLayout() } } } .width(Const.NewsListConstant_LIST_WIDTH) .height(Const.FULL_HEIGHT) .backgroundColor($r('app.color.listColor')) .edgeEffect(EdgeEffect.None) .offset({ x: 0, y: `${this.newsModel.offsetY}px` }) .margin({ left: Const.NewsListConstant_LIST_MARGIN_LEFT, right: Const.NewsListConstant_LIST_MARGIN_RIGHT }) .divider({ color: $r('app.color.dividerColor'), strokeWidth: Const.NewsListConstant_LIST_DIVIDER_STROKE_WIDTH, endMargin: Const.NewsListConstant_LIST_MARGIN_RIGHT }) .onScrollIndex((start: number, end: number) => { this.newsModel.startIndex = start this.newsModel.endIndex = end }) } @Builder FailLayout() { Image($r('app.media.none')) .width(Const.NewsListConstant_NONE_IMAGE_SIZE) .height(Const.NewsListConstant_NONE_IMAGE_SIZE) Text($r('app.string.page_none_msg')) .opacity(Const.NewsListConstant_NONE_TEXT_OPACITY) .fontSize(Const.NewsListConstant_NONE_TEXT_SIZE) .fontColor($r('app.color.fontColor_text3')) .margin({ top: Const.NewsListConstant_NONE_TEXT_MARGIN }) } } |
NoMoreLayout.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { CommonConstant as Const } from '../common/constant/CommonConstant' /** * The No more data layout component. */ @Component export default struct NoMoreLayout { build() { Row() { Text($r('app.string.prompt_message')) .margin({ left: Const.NoMoreLayoutConstant_NORMAL_PADDING }) .fontSize(Const.NoMoreLayoutConstant_TITLE_FONT) .textAlign(TextAlign.Center) } .width(Const.FULL_WIDTH) .justifyContent(FlexAlign.Center) .height(Const.CUSTOM_LAYOUT_HEIGHT) } } |
RefreshLayout.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout' import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsData' @Component export default struct RefreshLayout { @ObjectLink refreshLayoutClass: CustomRefreshLoadLayoutClass build() { Column() { if (this.refreshLayoutClass.isVisible) { CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass( this.refreshLayoutClass.isVisible, this.refreshLayoutClass.imageSrc, this.refreshLayoutClass.textValue, this.refreshLayoutClass.heightValue ) }) } } } } |
TabBar.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import NewsTypeModel from '../viewmodel/NewsTypeModel' import NewsViewModel from '../viewmodel/NewsViewModel' import Logger from '../common/utils/Logger' import { CommonConstant as Const } from '../common/constant/CommonConstant' import NewsList from './NewsList' @Component export default struct TabBar { @State tabBarArray: NewsTypeModel[] = NewsViewModel.getDefaultTypeList() @State currentIndex: number = 0 @State currentPage: number = 1 @Builder TabBuilder(index: number) { Column() { Text(this.tabBarArray[index].name) .height(Const.FULL_HEIGHT) .padding({ left: Const.TabBars_HORIZONTAL_PADDING, right: Const.TabBars_HORIZONTAL_PADDING }) .fontSize(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_SIZE : Const.TabBars_UN_SELECT_TEXT_FONT_SIZE) .fontWeight(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_WEIGHT : Const.TabBars_UN_SELECT_TEXT_FONT_WEIGHT) .fontColor($r('app.color.fontColor_text3')) } } aboutToAppear(): void { Logger.info(JSON.stringify(this.tabBarArray)) NewsViewModel.getNewsTypeList().then((typeList: NewsTypeModel[]) => { this.tabBarArray = typeList }).catch((typeList: NewsTypeModel[]) => { this.tabBarArray = typeList }) } build() { Tabs() { ForEach(this.tabBarArray, (tabItem: NewsTypeModel) => { TabContent() { Column() { NewsList({ currentIndex: $currentIndex }) } } .tabBar(this.TabBuilder(tabItem.id)) }, (item: NewsTypeModel) => JSON.stringify(item)) } .barHeight(Const.TabBars_BAR_HEIGHT) .barMode(BarMode.Scrollable) .barWidth(Const.TabBars_BAR_WIDTH) .vertical(false) .onChange((index: number) => { this.currentIndex = index this.currentPage = 1 }) } } |
viewmodel
NewsData.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/** * News list item info. */ export class NewsData { title: string = '' content: string = '' imagesUrl: Array<NewsFile> = [new NewsFile()] source: string = '' } /** * News image list item info. */ export class NewsFile { id: number = 0 url: string = '' type: number = 0 newsId: number = 0 } /** * Custom refresh load layout data. */ @Observed export class CustomRefreshLoadLayoutClass { isVisible: boolean imageSrc: Resource textValue: Resource heightValue: number constructor(isVisible: boolean, imageSrc: Resource, textValue: Resource, heightValue: number) { this.isVisible = isVisible this.imageSrc = imageSrc this.textValue = textValue this.heightValue = heightValue } } |
NewsModel.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant' import { NewsData } from './NewsData' export default class NewsModel { newsData: NewsData[] = [] currentPage: number = 1 pageSize: number = Const.PAGE_SIZE pullDownRefreshText: Resource = $r('app.string.pull_up_load_text') pullDownRefreshImage: Resource = $r('app.media.ic_pull_down_refresh') pullDownRefreshHeight: number = Const.CUSTOM_LAYOUT_HEIGHT isVisiblePullDown: boolean = false pullUpLoadText: Resource = $r('app.string.pull_up_load_text') pullUpLoadImage: Resource = $r('app.media.ic_pull_up_load') pullUpLoadHeight: number = Const.CUSTOM_LAYOUT_HEIGHT isVisiblePullUpLoad: boolean = false offsetY: number = 0 pageState: number = PageState.Loading hasMore: boolean = true startIndex = 0 endIndex = 0 downY = 0 lastMoveY = 0 isRefreshing: boolean = false isCanRefresh: boolean = false isPullRefreshOperation: boolean = false isLoading: boolean = false isCanLoadMore: boolean = false } |
NewsTypeModel.ets
1 2 3 4 |
export default class NewsTypeModel { id: number = 0 name: ResourceStr = '' } |
NewsViewModel.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import { CommonConstant as Const } from '../common/constant/CommonConstant' import { NewsData } from './NewsData' import NewsTypeModel from './NewsTypeModel' import { httpRequestGet } from '../common/utils/HttpUtil' import Logger from '../common/utils/Logger' import ResponseResult from './ResponseResult' class NewsViewModel { getNewsTypeList(): Promise<NewsTypeModel[]> { return new Promise((resolve: Function, reject: Function) => { let url = `${Const.SERVER}/${Const.GET_NEWS_TYPE}` httpRequestGet(url).then((data: ResponseResult) => { if (data.code === Const.SERVER_CODE_SUCCESS) { resolve(data.data) } else { reject(Const.TabBars_DEFAULT_NEWS_TYPES) } }).catch(() => { reject(Const.TabBars_DEFAULT_NEWS_TYPES) }) }) } getDefaultTypeList(): NewsTypeModel[] { return Const.TabBars_DEFAULT_NEWS_TYPES } getNewsList(currentPage: number, pageSize: number, path: string): Promise<NewsData[]> { return new Promise(async (resolve: Function, reject: Function) => { let url = `${Const.SERVER}/${path}` url += '?currentPage=' + currentPage + '&pageSize=' + pageSize httpRequestGet(url).then((data: ResponseResult) => { if (data.code === Const.SERVER_CODE_SUCCESS) { resolve(data.data) } else { Logger.error('getNewsList failed', JSON.stringify(data)) reject($r('app.string.page_none_msg')) } }).catch((err: Error) => { Logger.error('getNewsList failed', JSON.stringify(err)) reject($r('app.string.http_error_message')) }) }) } } let newsViewModel = new NewsViewModel() export default newsViewModel as NewsViewModel |
ResponseResult.ets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Data returned by the network request. */ export default class ResponseResult { code: string msg: string | Resource data: string | object | ArrayBuffer constructor() { this.code = '' this.msg = '' this.data = '' } } |