HarmonyOS JS卡片之“彩票开奖查询”及避坑指北 原创 精华
前言
接触鸿蒙开发已经有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的渣渣像素。
卡片效果 | 详情效果 |
---|---|
项目代码结构分析
-
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:双色球卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
- controller
-
Constants:常量工具类
-
MainAbility:HAP的入口ability,由DevEco Studio自动生成。同时也是各个卡片对应的Ability,用来项各个FormController 分发事件
-
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,如下图所示:
在模板选择界面,选择基本的模板Grid Pattern,点击按钮Next,进入到卡片配置界面
首先配置卡片的名称和描述;然后配置卡片关联的Page Ability;然后配置卡片的编程语言类型是JS;接下来配置卡片的JS组件名称;最后配置卡片支持的规格,勾选支持2x4、4x4规格
重复上述步骤,创建出大乐透和双色球卡片。运行项目,长按图标打开卡片管理界面,我们能看到刚创建的3类卡片,且每类卡片对应3种不同样式,如下图所示:
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;
}
编写完成后,重新运行,不出意外你应该能看到如下效果:
这里提下卡片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层,更新卡片
这里提下MainAbility
的onCreateForm
方法,它在进入卡片管理界面时被调用,内部通过FormControllerManager
获取了卡片对应的FormController
,并调用了其bindFormData()
方法;由于我们卡片展示的数据来源于网络请求,在对应的FormController
实现类中不太好获得formId
,所以我们稍微改造一下FormController
的bindFormData
方法,把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样式中,由于中奖信息列表较长,引入了list
和list-item
组件,让其可在卡片内上下滚动,具体实现此处不再赘述,有兴趣可阅读源码。
8.历史开奖列表的实现
这个界面也相对简单,使用了swiper
组件,左右滑动或点击头部标签栏,完成双色球、大乐透、福彩3D标签页的切换,每个标签页展示对应的近50期开奖结果,使用list
和list-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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
老CM表示很赞。
彩票两元一注,剩下一元是私房钱吗?
我没有媳妇,如果捡到一元钱的话是不是就要就给警察叔叔了
和媳妇散步是捡钱的前提条件*-*,所有你需要先去找个媳妇。。。
这都被你发现了
ennnn... 没买过彩票的我,突然间有个想法 哈哈哈哈
巨佬,有代码地址嘛?想在手机上跑起来看看
文章末尾有代码地址哈。