学堂
精培
企业培训
CTO训练营
鸿蒙社区
收藏本站
公众号矩阵
移动端

HarmonyOS JS卡片之“彩票开奖查询”及避坑指北 原创 精华

发布于 2021-9-2 11:22
浏览
5收藏

前言

接触鸿蒙开发已经有3个来月了,最近开始在看鸿蒙卡片开发。因为之前的开发大都是基于Java UI,但按官方的说法,JS卡片相比Java卡片有更大的优势,故决定写个JS卡片的demo来练练手。碰巧,前几天和媳妇儿在散步时捡到1元钱,没能交给警察叔叔,媳妇儿就提议“我们把它昧了吧,买张彩票。”由于不是老CM,没有关注开奖的习惯,想着要是能把开奖结果放在手机桌面显示就好了,这样就不会错过我一夜暴富的机会了。有了需求就开撸,然后就有了这篇文章!

项目简介

  • 本项目基于API 6开发,demo运行在API 6以下的手机上会出现部分功能不能使用的现象。

  • 卡片功能上实现了双色球、大乐透、福彩3D的最近一期开奖结果查询,点击卡片“刷新”按钮,调用接口更新最新数据;点击“查看更多”按钮,跳转至应用主界面。

  • 应用主界面上实现了双色球、大乐透、福彩3D近50期开奖结果查看

  • 以上数据均使用了 聚合数据的https://www.juhe.cn/docs/api/id/300免费接口(需申请key,一个key一天可免费调用100次,如遇key使用次数过多导致接口请求失败情况时,开发者可自行申请key并替换Constants.java文件下的JH_KEY常量值),数据可能会有延迟

  • 卡片开发部分使用了卡片的JS UI框架,但由于系统PA与FA相互调用的限制问题,卡片的业务逻辑部分仍然采用Java代码编写(PS:最开始的想法是尽量可能的少依赖Java代码,故尝试了JS FA与Java PA相互调用的方式,但当应用进程被干掉时,Java端无法再调用到JS端方法。这样就导致JS只能写UI部分,业务逻辑还得Java层实现。有知道解决办法的也麻烦告知一声)

  • 卡片业务层使用Java开发,采用了简单的MVP架构,网络请求和数据处理部分使用了rxjava3+retrofit框架

  • 应用主界面使用了JS-UI框架实现

实现效果

请忽略我粗陋的UI设计和GIF的渣渣像素。

卡片效果 详情效果
HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区 HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

项目代码结构分析

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

  • base

    IBasePresenter:MVP架构中presenter基类接口

    IBaseView:MVP架构中view基类接口

  • network

    • bean

      LotteryBean:彩票详情接口返回对应model

    • CachedLotteryDetailUtil :彩票详情接口请求工具类,主要作用是防止重复调用详情接口

    • LogInterceptor:OKhttp日志拦截工具类

    • LotteryAPI:接口请求类,通过retrofit注解,将接口返回数据转化为实体类

    • Services:配置Retrofit并提供

  • presenter

    • IMainContract:MVP中 view与presenter的桥梁
    • MainPresenter:提供彩票详情接口的请求,并处理接口返回数据为卡片需要的ZSONObject对象
  • utils

    LogUtil:日志打印工具类

  • widget

    • controller
      • FormController:创建卡片时自动生成,卡片管理器的抽象基类
      • FormControllerManager:创建卡片时自动生成,管理各个FormController 的工具类
    • dltwidget.DltWidgetImpl :大乐透卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
    • fcsdwidget.FcsdWidgetImpl:福彩3D卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
    • ssqwidget.SsqWidgetImpl:双色球卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
  • Constants:常量工具类

  • MainAbility:HAP的入口ability,由DevEco Studio自动生成。同时也是各个卡片对应的Ability,用来项各个FormController 分发事件

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

  • default

    • common

      component/lottery:应用首页列表item组件

      images :资源图片

    • pages

      home:应用首页

  • dlt_widget

    • common:资源图片存放目录
    • pages/index
      • index.css :大乐透卡片css样式
      • index.hml:大乐透卡片布局文件
      • index.json:包含页面默认值
  • fcsd_widget:目录结构同 dlt_widget

  • ssq_widget:目录结构同 dlt_widget

详细实现过程

1. 创建双色球卡片

在目录entry上点击右键,在弹出的菜单中选择New,然后在弹出的子菜单中点击Service Widget,如下图所示:

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

在模板选择界面,选择基本的模板Grid Pattern,点击按钮Next,进入到卡片配置界面

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

首先配置卡片的名称和描述;然后配置卡片关联的Page Ability;然后配置卡片的编程语言类型是JS;接下来配置卡片的JS组件名称;最后配置卡片支持的规格,勾选支持2x4、4x4规格

重复上述步骤,创建出大乐透和双色球卡片。运行项目,长按图标打开卡片管理界面,我们能看到刚创建的3类卡片,且每类卡片对应3种不同样式,如下图所示:

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

2. 绘制双色球卡片UI

我们编写双色球界面:

<!--index.hml-->
<div class="container">
 <div>
     <text class="text">双色球</text>
     <text class="text-small" style="margin-left : 15px;" if="{{ cardType2x4 || cardType4x4 }}">每周二、四、日开奖</text>
     <text class="text" style="margin-left : 75px;" if="{{ cardType2x4 || cardType4x4 }}" onclick="showMore"> 查看更多
     </text>
 </div>
 <div style="margin-top : 10px; align-content : center;">
     <text class="text-small" style="margin-right : 10px;">第{{ lotteryData.lottery_no }}期</text>
     <text class="text-small" if="{{ cardType2x4 || cardType4x4 }}"> 开奖日期:</text>
     <text class="text-small"> {{ lotteryData.lottery_date }}</text>
 </div>

 <div style="flex-wrap : wrap; margin-top : 10px;">
     <text class="ball" for="{{ lotteryRed }}" tid="id">{{ $item }}</text>
     <text class="ball" style="background-color : blue;" for="{{ lotteryBlue }}" tid="id">{{ $item }}</text>
 </div>
 <div class="amount-box" if="{{ cardType2x4 || cardType4x4 }}">
     <div style="flex-direction : column;">
         <text class="text-small">本期全国销量</text>
         <text class="text-amount">{{ lotteryData.lottery_sale_amount }}</text>
     </div>
     <text class="diver"></text>
     <div style="flex-direction : column;">
         <text class="text-small">累计奖池</text>
         <text class="text-amount">{{ lotteryData.lottery_pool_amount }}</text>
     </div>
 </div>
 <div style="flex-direction : column; padding-top : 10px; margin-bottom: 20px;" if="{{ cardType4x4 }}">
     <div style="background-color : green;">
         <text class="text-prize">奖项</text>
         <text class="text-prize">中奖条件</text>
         <text class="text-prize">中奖注数</text>
         <text class="text-prize">单注金额(元)</text>
     </div>
     <div for="{{ lottery_prize }}">
         <text class="text-prize">{{ $item.prize_name }}</text>
         <text class="text-prize">{{ $item.prize_require }}</text>
         <text class="text-prize">{{ $item.prize_num }}</text>
         <text class="text-prize">{{ $item.prize_amount }}</text>
     </div>
 </div>
 <div>
     <text class="text-small" if="{{ cardType2x4 || cardType4x4 }}">更新时间:</text>
     <text class="text-small">{{ updateTime }}</text>
     <image class="refresh" src="/common/image_1.png" onclick="updateData"></image>
 </div>
</div>
<!--index.json-->
{
"data": {
 "cardType2x2": true,
 "cardType2x4": false,
 "cardType4x4": false,
 "lotteryData": {
   "lottery_no": "21081",
   "lottery_date": "2021-07-20",
   "lottery_sale_amount":"344,437,194",
   "lottery_pool_amount":"997,378,346"
 },
 "lotteryRed":["01","03","05","18","22","23"],
 "lotteryBlue":["01"],
 "updateTime": "2021/07/07 14:20:59",
 "lottery_prize":[
   {
     "prize_name":"一等奖",
     "prize_num":"4",
     "prize_amount":"10,000,000",
     "prize_require":"6+1"
   },
   {
     "prize_name":"二等奖",
     "prize_num":"135",
     "prize_amount":"207,725",
     "prize_require":"6+0"
   },
   {
     "prize_name":"三等奖",
     "prize_num":"879",
     "prize_amount":"3,000",
     "prize_require":"5+1"
   },
   {
     "prize_name":"四等奖",
     "prize_num":"45659",
     "prize_amount":"200",
     "prize_require":"5+0,4+1"
   },
   {
     "prize_name":"五等奖",
     "prize_num":"1001881",
     "prize_amount":"10",
     "prize_require":"4+0,3+1"
   },
   {
     "prize_name":"六等奖",
     "prize_num":"6962930",
     "prize_amount":"5",
     "prize_require":"2+1,1+1,0+1"
   }
 ]
},
"actions": {
}
}
<!--index.css-->
.container {
 width: 100%;
 height: 100%;
 padding: 10px;
 flex-direction: column;
}

.text {
 font-size: 15px;
}

.text-small {
 font-size: 11px;
}

.text-amount {
 font-size: 14px;
 font-weight: bold;
 color: darkred;
}

.text-prize {
 font-size: 11px;
 flex-weight: 25;
 min-height: 20px;
 text-align: center;
}

.ball {
 width: 20px;
 height: 20px;
 text-align: center;
 font-size: 12px;
 margin: 4px;
 color: #FFFFFF;
 border-radius: 20px;
 background-color: red;
}

.amount-box {
 flex-direction: row;
 align-items: center;
 height: 30px;
}

.diver {
 width: 1px;
 height: 20px;
 background-color: red;
 margin-left: 15px;
 margin-right: 15px;
}

.refresh {
 width: 22px;
 height: 22px;
}

编写完成后,重新运行,不出意外你应该能看到如下效果:

HarmonyOS  JS卡片之“彩票开奖查询”及避坑指北-鸿蒙HarmonyOS技术社区

这里提下卡片JS-UI框架的坑:

  • 框架提供了原子布局来控制元素在不同尺寸布局上的隐藏和展示,但怎么说呢 ,一句话概括就是:你以为的并不是你以为的。可以看到我这里放弃了display-index的使用,而采用通过JAVA端卡片的类型,来适配不同的UI
  • 不同于应用开发中的JS UI框架,这里的条件渲染 不支持表达式
  • css 不支持标签选择器
  • 部分css样式不能被继承,例如给父div元素设置了font-size,你会发现div中的text组件并没继承上述样式
  • 列表渲染的for循环的数组必须是index.json data对象的最外层
  • 不支持双层for循环
  • api6 不兼容api 5的设备

你会发现卡片JSON文件中的data对象数据结构同接口返回的数据结构有些差异,就是因为上述原因导致的

3. 获取网络数据

简单的MVP架构,采用Retrofit+RxJava作为异步网络请求框架

build.gradle 中添加RxJava和Retrofit的依赖:

// RxJava
api 'io.reactivex.rxjava3:rxjava:3.0.3'
implementation 'io.openharmony.tpc.thirdlib:Rxohos:1.0.0'

// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation "com.squareup.retrofit2:converter-gson:2.1.0"
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'

config.json文件中添加网络权限

// config.json
"deviceConfig": {
  "default": {
    "network": {
      "cleartextTraffic": true,
      "securityConfig": {
        "domainSettings": {
          "cleartextPermitted": true,
          "domains": [
            {
              "subdomains": true,
              "name": "apis.juhe.cn"
            },
            {
              "subdomains": true,
              "name": "v.juhe.cn"
            }
          ]
        }
      }
    }
  }
},
"module": {
    ...
    "reqPermissions": [
      {
        "name": "ohos.permission.GET_NETWORK_INFO"
      },
      {
        "name": "ohos.permission.SET_NETWORK_INFO"
      },
      {
        "name": "ohos.permission.INTERNET"
      }
    ],
    ...
}

创建接口请求:

// LotteryAPI.java
public interface LotteryAPI {

 @POST("/lottery/query")
 @FormUrlEncoded
 Observable<LotteryBean> queryDetail(@Field("lottery_id") String lottery_id, @Field("lottery_no") String lottery_no, @Field("key") String key);
}

P层和V层的接口比较简单,不在罗列。编写P层业务逻辑:

//MainPresenter.java

@Override
public void loadLotteryData(long formId, String lotteryId) {
 CachedLotteryDetailUtil.getLotteryDetail(lotteryId)
         .flatMap((Function<LotteryBean, ObservableSource<ZSONObject>>) res -> {
         	if (0 != res.getError_code()) {
                 Throwable throwable = new Throwable(res.getReason());
                 return Observable.error(throwable);
             }
             ZSONObject zsonObject = buildDataByResult(res);
             return Observable.just(zsonObject);
         })
         .subscribeOn(Schedulers.io())
         .observeOn(OpenHarmonySchedulers.mainThread())
         .subscribe(new Observer<ZSONObject>() {
             @Override
             public void onSubscribe(@NonNull Disposable d) {
             }

             @Override
             public void onNext(@NonNull ZSONObject result) {
                 if (mView != null) {
                     mView.onLoadDataSuccess(formId, result);
                 }
             }

             @Override
             public void onError(@NonNull Throwable e) {
                 if (mView != null) {
                     mView.onLoadDataFailed(e);
                 }
             }

             @Override
             public void onComplete() {

             }
         });
}

/**
* 根据接口返回的数据,组装成JS卡片需要的数据
*
* @param result
* @return
*/
private ZSONObject buildDataByResult(LotteryBean result) {
 LotteryBean.DetailBean detailBean = result.getResult();
 ZSONObject data = new ZSONObject();
 ZSONObject lotteryData = new ZSONObject();
 lotteryData.put("lottery_no", detailBean.getLottery_no());
 lotteryData.put("lottery_date", detailBean.getLottery_date());
 lotteryData.put("lottery_sale_amount", detailBean.getLottery_sale_amount());
 lotteryData.put("lottery_pool_amount", detailBean.getLottery_pool_amount());
 data.put("lotteryData", lotteryData);
 String[] ballArray = detailBean.getLottery_res().split(",");
 if ("ssq".equalsIgnoreCase(detailBean.getLottery_id())){
     data.put("lotteryRed", Arrays.copyOfRange(ballArray, 0, 6));
     data.put("lotteryBlue", Arrays.copyOfRange(ballArray, 6, 7));
 } else if ("dlt".equalsIgnoreCase(detailBean.getLottery_id())){
     data.put("lotteryRed", Arrays.copyOfRange(ballArray, 0, 5));
     data.put("lotteryBlue", Arrays.copyOfRange(ballArray, 5, 7));
 } else if ("fcsd".equalsIgnoreCase(detailBean.getLottery_id())){
     data.put("lotteryRed", ballArray);
 }

 data.put("updateTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

 ZSONArray zsonArray = new ZSONArray();
 for (LotteryBean.PrizeBean prizeBean : detailBean.getLottery_prize()) {
     ZSONObject prize = new ZSONObject();
     prize.put("prize_name", prizeBean.getPrize_name());
     prize.put("prize_num", prizeBean.getPrize_num());
     prize.put("prize_amount", prizeBean.getPrize_amount());
     prize.put("prize_require", prizeBean.getPrize_require());
     zsonArray.add(prize);
 }
 data.put("lottery_prize", zsonArray);
 return data;
}

4. V层调用P层,更新卡片

这里提下MainAbilityonCreateForm方法,它在进入卡片管理界面时被调用,内部通过FormControllerManager获取了卡片对应的FormController,并调用了其bindFormData()方法;由于我们卡片展示的数据来源于网络请求,在对应的FormController实现类中不太好获得formId,所以我们稍微改造一下FormControllerbindFormData方法,把formId给传进去

public abstract ProviderFormInfo bindFormData(long formId);
//MainAbility.java

public class MainAbility extends AceAbility implements IMainContract.View {

 private IMainContract.Presenter mPresenter;

 @Override
 public void onStart(Intent intent) {
   ...
 }

 @Override
 public void onStop() {
     super.onStop();
 }

 @Override
 protected ProviderFormInfo onCreateForm(Intent intent) {
     HiLog.info(TAG, "onCreateForm");
     long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
     String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
     int dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
     HiLog.info(TAG, "onCreateForm: formId=" + formId + ",formName=" + formName);
     FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
     FormController formController = formControllerManager.getController(formId);
     formController = (formController == null) ? formControllerManager.createFormController(formId,
             formName, dimension) : formController;
     if (formController == null) {
         HiLog.error(TAG, "Get null controller. formId: " + formId + ", formName: " + formName);
         return null;
     }
     return formController.bindFormData(formId);
 }

 @Override
 protected void onUpdateForm(long formId) {
   ...
 }

 @Override
 protected void onDeleteForm(long formId) {
    ...
 }

 @Override
 protected void onTriggerFormEvent(long formId, String message) {
    ...
 }

 @Override
 public void onNewIntent(Intent intent) {
    ...
 }

 private boolean intentFromWidget(Intent intent) {
    ...
 }

 private String getRoutePageSlice(Intent intent) {
   ...
 }

 private IMainContract.Presenter getPresenter() {
     if (mPresenter == null) {
         mPresenter = new MainPresenter(this);
     }
     return mPresenter;
 }

 /**
  * 查询彩票开奖详情
  *
  * @param formId
  * @param lotteryId
  */
 public void loadData(long formId, String lotteryId) {
     getPresenter().loadLotteryData(formId, lotteryId);
 }

 @Override
 public void onLoadDataSuccess(long formId, ZSONObject data) {
     try {
         // 更新卡片
         updateForm(formId, new FormBindingData(data));
     } catch (FormException e) {
         e.printStackTrace();
     }
 }

 @Override
 public void onLoadDataFailed(Throwable exception) {
 	...
 }
}
//SsqWidgetImpl.java
public class SsqWidgetImpl extends FormController{
 private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, SsqWidgetImpl.class.getName());

 public SsqWidgetImpl(Context context, String formName, Integer dimension) {
     super(context, formName, dimension);
 }

 @Override
 public ProviderFormInfo bindFormData(long formId) {
     HiLog.info(TAG, "===== ssq bind form data");
     ZSONObject zsonObject = new ZSONObject();
     ProviderFormInfo providerFormInfo = new ProviderFormInfo();
     boolean is2x2 = dimension == DEFAULT_DIMENSION_2X2;
     boolean is2x4 = dimension == DIMENSION_2X4;
     boolean is4x4 = dimension == DIMENSION_4X4;
     zsonObject.put("cardType2x2", is2x2);
     zsonObject.put("cardType2x4", is2x4);
     zsonObject.put("cardType4x4", is4x4);
     providerFormInfo.setJsBindingData(new FormBindingData(zsonObject));
     
     // 调用接口更新卡片数据
     ((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
     return providerFormInfo;
 }

 @Override
 public void updateFormData(long formId, Object... vars) {
 }

 @Override
 public void onTriggerFormEvent(long formId, String message) {
 }

 @Override
 public Class<? extends AbilitySlice> getRoutePageSlice(Intent intent) {
     HiLog.info(TAG, "get the default page to route when you click card.");
     return null;
 }
}

5. 简单的网络优化

由于onCreateForm(Intent intent)方法会被调用多次,而每个类型的卡片请求的数据一样,聚合api一天100次免费请求的次数一会儿就用完了,故对此进行一个简单的优化:优先看内存缓存中是否有,有且为过期(缓存默认10分钟有效期)则直接返回,否则阻塞等待接口请求完成,对于并发请求,若请求队列已有相同的请求,则阻塞,否则创建新的请求。

若不关心接口调用的可略过本节。

// CachedLotteryDetailUtil.java
/**
* CachedLotteryDetailUtil
* 缓存彩票详情工具类,防止在卡片创建时,重复调用接口
*
* @author:xwg
* @since 2021-07-30
*/
public class CachedLotteryDetailUtil {
 //缓存的有效时间 默认10分钟
 private static final long CACHE_TIME = 10 * 60 * 1000;

 // 缓存的彩票结果
 private static volatile ConcurrentHashMap<String, LotteryBean> cacheResMap = new ConcurrentHashMap<>();

 //缓存的请求时间
 private static volatile ConcurrentHashMap<String, Long> reqTimeMap = new ConcurrentHashMap<>();

 //正在请求 对应的lotteryId
 private static volatile List<String> requestingLotteryId;


 /**
  * 获取彩票详情
  *
  * @param lotteryId
  * @return
  */
 public static Observable<LotteryBean> getLotteryDetail(String lotteryId) {
     if (cacheResMap.get(lotteryId) != null) {
         if (!isExpired(lotteryId)) {
             return Observable.create(emitter -> {
                 if (!emitter.isDisposed()) {
                     emitter.onNext(cacheResMap.get(lotteryId));
                 }
             });
         } else {
             cacheResMap.remove(lotteryId);
         }

     }
     if (isRequesting(lotteryId)) {
         return waitToRequestEnd(lotteryId);
     } else {
         return requestDetail(lotteryId);
     }
 }

 /**
  * 是否正在请求
  *
  * @param lotteryId
  * @return
  */
 private static boolean isRequesting(String lotteryId) {
     if (requestingLotteryId == null || lotteryId.isEmpty()) {
         return false;
     }
     return requestingLotteryId.contains(lotteryId);
 }

 /**
  * 阻塞等待请求结果
  *
  * @param lotteryId
  * @return
  */
 private static Observable<LotteryBean> waitToRequestEnd(final String lotteryId) {
     return Observable.create(emitter -> {
         while (isRequesting(lotteryId)) {
             try {
                 Thread.sleep(10);
             } catch (InterruptedException e) {
             }
         }
         if (!emitter.isDisposed()) {
             try {
                 if (cacheResMap != null && cacheResMap.get(lotteryId) != null && !isExpired(lotteryId)) {
                     LotteryBean lotteryBean = cacheResMap.get(lotteryId);
                     emitter.onNext(lotteryBean);

                     if (!emitter.isDisposed()) {
                         emitter.onComplete();
                     }
                 } else {
                     Throwable throwable = new Throwable("请求异常,请稍后再试");
                     emitter.onError(throwable);
                 }

             } catch (Exception e) {
                 emitter.onError(e);
             }
         }
     });
 }

 /**
  * 联网请求彩票详情
  *
  * @param lotteryId
  * @return
  */
 private static Observable<LotteryBean> requestDetail(final String lotteryId) {
     if (requestingLotteryId == null) {
         requestingLotteryId = new LinkedList<>();
     }
     if (!lotteryId.isEmpty() && !requestingLotteryId.contains(lotteryId)) {
         requestingLotteryId.add(lotteryId);
     }


     return Services.createAPI(LotteryAPI.class).queryDetail(lotteryId, "", Constants.JH_KEY)
             .doOnError(throwable -> {
                 requestingLotteryId.remove(lotteryId);
                 reqTimeMap.remove(lotteryId);
             })
             .doAfterNext(lotteryBean -> {
                 requestingLotteryId.remove(lotteryId);
                 cacheResMap.put(lotteryId, lotteryBean);
                 reqTimeMap.put(lotteryId, System.currentTimeMillis());
             });
 }

 /**
  * 缓存是否已经过期
  *
  * @return
  */
 private static boolean isExpired(String lotteryId) {
     if (reqTimeMap == null || reqTimeMap.get(lotteryId) == null) {
         return true;
     }
     return System.currentTimeMillis() - reqTimeMap.get(lotteryId) > CACHE_TIME;
 }

}

6. 给卡片增加刷新事件和查看更多事件

给卡片的index.json文件添加actions,定义showMore为router事件,触发这个事件会跳转到指定的abilityName对应的Ability;updateData为message事件,触发该事件,会回调Ability中的onTriggerFormEvent(long formId, String message)方法

// index.json
{
"data": {
...
},
"actions": {
 "showMore": {
   "action": "router",
   "abilityName": "com.xwg.lotteryquery.MainAbility",
   "params": {
     "message": "fcsd"
   }
 },
 "updateData": {
   "action": "message",
   "params": {
     "key": "fcsd"
   }
 }
}
}

卡片的布局文件中添加点击事件:

<!--跳转应用首页事件-->
<text class="text" style="margin-left : 120px;" if="{{ cardType2x4 || cardType4x4 }}" onclick="showMore"> 查看更多
</text>
<!--点击刷新按钮事件-->
<image class="refresh" src="/common/refresh.png" onclick="updateData"></image>
// SsqWidgetImpl.java
@Override
public void onTriggerFormEvent(long formId, String message) {
 HiLog.info(TAG, "======== ssq handle card click event.");
 ((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
}

添加定时刷新:

打开config.json,对于标签“scheduledUpdateTime”设定的时刻,当到达之后,MainAbility中卡片的回调方法onUpdateForm()就会被自动调用,updateDuration默认为1,下面配置表示:双色球卡片允许定时刷新,从10:30开始,每隔半小时刷新一次。

// config.json

"forms": [
  {
    "jsComponentName": "ssq_widget",
    "isDefault": true,
    "scheduledUpdateTime": "10:30",
    "defaultDimension": "2*2",
    "name": "SsqWidget",
    "description": "双色球",
    "colorMode": "auto",
    "type": "JS",
    "supportDimensions": [
      "2*2",
      "2*4",
      "4*4"
    ],
    "updateEnabled": true,
    "updateDuration": 1
  }
  ...
  ]

FormController直接调用MainAbility的获取数据方法

// SsqWidgetImpl.java
@Override
public void updateFormData(long formId, Object... vars) {
    HiLog.info(TAG, "======== ssq update form data timing, default 30 minutes");
    ((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
}

7. 大乐透和福彩3D卡片的实现

这两个卡片的实现过程和双色球卡片基本一致,主要是UI上有些区别。大乐透卡片4X4样式中,由于中奖信息列表较长,引入了listlist-item 组件,让其可在卡片内上下滚动,具体实现此处不再赘述,有兴趣可阅读源码。

8.历史开奖列表的实现

这个界面也相对简单,使用了swiper组件,左右滑动或点击头部标签栏,完成双色球、大乐透、福彩3D标签页的切换,每个标签页展示对应的近50期开奖结果,使用listlist-item组件渲染。由于各列表item展示UI相近,故将其抽成了组件放置于*/common/component/lottery*目录下。

lottery子组件部分

lottery.js:通过props接收外部传入的lotteryData数据

// lottery.js
export default {
    props: {
        lotteryData: {}
    }
}

lottery.hml组件布局代码:

<!--lottery.hml-->
<div class="item">
    <div class="item-header">
        <text style="font-weight : bold;">第{{ lotteryData.lottery_no }}期</text>
        <text class="remarks">开奖日期:{{ lotteryData.lottery_date }}</text>
    </div>
    <div style="margin-left : 10px;">
        <text for="{{ lotteryData.ballList }}"
              class="ball"
              style="background-color : {{ $idx >= lotteryData.redBallCount ? '#6666FF' : '#FF0033' }};"
                >{{ $item }}</text>
    </div>
    <div class="amount-box">
        <div style="flex-direction : column;">
            <text class="text-small">本期全国销量</text>
            <text class="text-amount">{{ lotteryData.lottery_sale_amount }}</text>
        </div>
        <text class="diver"></text>
        <div style="flex-direction : column;">
            <text class="text-small">累计奖池</text>
            <text class="text-amount">{{ lotteryData.lottery_pool_amount || '0' }}</text>
        </div>
    </div>
</div>

css比较简单,这里不再给出

父组件实现:
<!--home.hml-->

<element name='lottery' src='../../common/component/lottery/lottery.hml'></element>
<div class="container">
    <div class="title-container">
        <text class="title {{ currentIdx == 0 ? tabSelected : tabUnSelected }}" onclick="onTabClick(0)">双色球</text>
        <text class="title {{ currentIdx == 1 ? tabSelected : tabUnSelected }}" onclick="onTabClick(1)">大乐透</text>
        <text class="title {{ currentIdx == 2 ? tabSelected : tabUnSelected }}" onclick="onTabClick(2)">福彩3D</text>
    </div>

    <swiper class="swiper" id="swiper" index="0" indicator="false" loop="true"
            digital="false" on:change="onChange">
        <div class="swiperContent">
            <list class="">
                <list-item for="{{ ssqResList }}" class="lottery-item">
                   <lottery lottery-data = "{{$item}}"></lottery>
                </list-item>
            </list>
        </div>

        <div class="swiperContent">
            <list class="">
                <list-item for="{{ dltResList }}" class="lottery-item">
                    <lottery lottery-data = "{{$item}}"></lottery>
                </list-item>
            </list>
        </div>
        <div class="swiperContent">
            <list class="">
                <list-item for="{{ fcsdResList }}" class="lottery-item">
                    <lottery lottery-data = "{{$item}}"></lottery>
                </list-item>
            </list>
        </div>
    </swiper>
</div>
<!--home.js-->

import http from '@ohos.net.http';

const JH_URL = 'http://apis.juhe.cn'
const JH_KEY = '4931b786dd99c28e7e9990fb75c39fad'
export default {
    data: {
        currentIdx: 0,
        tabSelected: 'tabSelected',
        tabUnSelected: 'tabUnSelected',
        ssqResList: null, // 双色球近期开奖结果列表
        dltResList: null, // 大乐透近期开奖结果列表
        fcsdResList: null, // 3D球近期开奖结果列表
    },
    onInit() {
        this.querySsqHis();
        this.queryDltHis();
        this.queryFcsdHis();
    },

// 查询双色球历史开奖
    querySsqHis() {
        console.info('xwg==  querySsqHis ----------');
        let _t = this
        let httpRequest = http.createHttp();
        let url = JH_URL + '/history' + '?lottery_id=ssq&page_size=50&page=&key=' + JH_KEY
        httpRequest.request(url, (err, data) => {
            if (err == null) {
                let lotteryResList = JSON.parse(data.result).result.lotteryResList
                _t.ssqResList = lotteryResList.map(item => {
                    item.ballList = item.lottery_res.split(",")
                    item.redBallCount = 6
                    return item
                })
                console.info('xwg== ssqResList:' + JSON.stringify(_t.ssqResList));
            } else {
                console.info('xwg== error:' + JSON.stringify(err));
            }
        });
    },

// 查询大乐透历史开奖
    queryDltHis() {
        let _t = this
        let httpRequest = http.createHttp();
        let url = JH_URL + '/history' + '?lottery_id=ssq&page_size=50&page=&key=' + JH_KEY
        httpRequest.request(url, (err, data) => {
            if (err == null) {
                let dltRes = JSON.parse(data.result).result.lotteryResList
                _t.dltResList = dltRes.map(item => {
                    item.ballList = item.lottery_res.split(",")
                    item.redBallCount = 5
                    return item
                })
            } else {
                console.info('xwg== error:' + JSON.stringify(err));
            }
        });

    },

// 查询福彩3D历史开奖
    queryFcsdHis() {
        let _t = this
        let httpRequest = http.createHttp();
        let url = JH_URL + '/history' + '?lottery_id=ssq&page_size=50&page=&key=' + JH_KEY
        httpRequest.request(url, (err, data) => {
            if (err == null) {
                let fcsdRes = JSON.parse(data.result).result.lotteryResList
                _t.fcsdResList = fcsdRes.map(item => {
                    item.ballList = item.lottery_res.split(",")
                    item.redBallCount = 3
                    return item
                })
            } else {
                console.info('xwg== error:' + JSON.stringify(err));
            }
        });

    },

// swiper滑动监听
    onChange(value) {
        this.currentIdx = value.index
    },

// tab click事件
    onTabClick(idx) {
        this.currentIdx = idx
        this.$element('swiper').swipeTo({
            index: idx
        });
    },
    computed: {}
}

尚未解决的问题:这里引入了http组件进行网络请求,但在请求聚合接口时,失败率很高,但尝试请求别的网站的api时没有此现象,目前尚不知原因。

总结

由于这也是我第一次使用JS UI框架进行卡片开发的项目,水平有限,难免会对官方部分API理解不到位甚至理解有误的地方,希望大家也多多指正,共同进步。这一路上虽然磕磕巴巴,也有很多吐槽,但我们从卡片JS-UI API 5到API 6功能上逐渐靠拢应用JS-UI上也能看出来鸿蒙的努力,给它点时间,相信它功能上会变得更强大、完善;对于开发也会变得更快捷、简单。

最后附上项目地址:lottery-query

请自行下载资源,欢迎交流学习。

作者:熊文功

更多原创内容请关注:开鸿 HarmonyOS 学院

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

©著作权归作者和HarmonyOS技术社区共同所有,如需转载,请注明出处,否则将追究法律责任
已于2021-9-10 15:28:37修改
9
收藏 5
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐