评论

收藏

[HarmonyOS] 51CTO鸿蒙社区服务卡片应用设计

移动开发 移动开发 发布于:2021-07-20 15:51 | 阅读数:600 | 评论:0

前言
上个月华为发布了鸿蒙,我的使用感受就是两个字,舒服。特别是服务卡片,便捷的信息展示,服务高效直达。这么形容不够直观,用个项目给大家展示下。
假如51鸿蒙社区出了鸿蒙版,本项目中包含的内容和图片仅供学习和技术交流。
::: hljs-center
DSC0000.png

:::
我平时经常在PC端逛51cto的鸿蒙社区,好多知识都是在社区学习的。但是有时候不方便开电脑,用手机就需要打开微信,点公众号搜索技术社区,然后点击逛社区,有点麻烦。如果此时有鸿蒙的服务卡片话,操作就简单了,只需要解锁,点击卡片,巴适啊。每次用手机进社区刚巧能节省大约10s,不要小看这几秒钟,以鸿蒙的体量,每人每天这么几次节省的时间就是天文数字。
::: hljs-center
DSC0001.gif

:::
接下来就具体分析下服务卡片究竟可以干哪些事情?

一、文章推荐服务卡片
社区访问最多的就是首页的“推荐内容”,而服务卡片正是为了提供用户容易使用且一目了然的信息内容,既然如此那就将“推荐内容”制作成服务卡片。
::: hljs-center
DSC0002.png

:::
1.界面设计
服务卡片有4种尺寸,分别是1×2微卡片、2×2小卡片、2×4中卡片、4×4大卡片。显然1×2微卡片无法满足制作推荐内容卡片的需求。所以选择其他三种尺寸的服务卡片。
DSC0003.png

从上图可以看出,服务卡片可以实现在不同终端设备上的展示和自适应,但其实设计这种多终端多尺寸的服务卡片,代码却并不复杂。下面是我使用js开发的页面内容相关的代码。
<div class="div_title" ><!--第一行标题-->
<span class="div_title_top" if="{{$item.is_top}}">置顶</span>
<span class="div_title_good" if="{{$item.is_good}}">精</span>
<span class="div_title_title">{{$item.title}}</span>
</div>
<div class="div_tags"><!--第二行标签-->
<text class="div_tags_text"  style="display-index: 5;" if="{{$item.tags[0]}}">{{$item.tags[1]}}</text>
<text class="div_tags_text"  style="display-index: 4;" if="{{$item.tags[2]}}">{{$item.tags[3]}}</text>
<text class="div_tags_text"  style="display-index: 3;" if="{{$item.tags[4]}}">{{$item.tags[5]}}</text>
<text class="div_tags_text"  style="display-index: 2;" if="{{$item.tags[6]}}">{{$item.tags[7]}}</text>
<text class="div_tags_text"  style="display-index: 1;" if="{{$item.tags[8]}}">{{$item.tags[9]}}</text>
</div>
<div class="div_user"><!--第三行作者-->
<image class="div_user_image" src="common/image_1.png" style="display-index: 3;"></image>
<text class="div_user_username" style="display-index: 3;">{{$item.username}}</text>
<text class="div_user_username" style="display-index: 2;">{{$item.reply_time}}</text>
<text class="div_user_username" style="display-index: 1;">最后一次回复:</text>
<text class="div_user_username" style="display-index: 1;">{{$item.reply_username}}</text>
</div>
<divider class="divider"></divider><!--分割线-->
可以使用少量的代码,实现在手机和平板2个终端6个尺寸的服务卡片,使用鸿蒙的原子布局能力。点击查看原子布局官方文档
主要就是通过样式display-index值从小到大的顺序进行隐藏。
这里说下我遇到的坑,第一行标题中的置顶和精,使用的是<span>组件,但是<span>的样式目前还不支持背景颜色设置,所以要想实现图中展示的效果,还得将代码稍微改动下,用<stack>曲线救国。
<div class="div_title" ><!--第一行标题-->
<stack>
    &lt;text&gt;
      &lt;span class="div_title_top" if="{{$item.is_top}}"&gt;置顶置顶&lt;/span&gt;
      &lt;span class="div_title_good" if="{{$item.is_good}}"&gt;精精&lt;/span&gt;
      &lt;span class="div_title_title"&gt;{{$item.title}}&lt;/span&gt;
    &lt;/text&gt;
    &lt;div&gt;&lt;!--利用stack堆叠一层text,在text上设置背景色--&gt;
      &lt;text class="div_title_top" if="{{$item.is_top}}"&gt;置顶&lt;/text&gt;
      &lt;text class="div_title_good" if="{{$item.is_good}}"&gt;精&lt;/text&gt;
    &lt;/div&gt;
  &lt;/stack&gt;
</div>
.div_title_top {
text-align:center;
width: 32px;
height: 16px;
font-size: 12px;
font-weight: 400;
margin: 3px;
border-radius: 3px;
color: #FFFFFF;
background-color: #f40d04;
}
.div_title_good {
text-align:center;
width: 22px;
height: 16px;
font-size: 12px;
font-weight: 400;
margin: 3px;
border-radius: 3px;
color: #FFFFFF;
background-color: #F7748F;
}
这样就可以完整显示一条文章内容信息了,接下只需要放入list列表组件,就可以实现整个推荐文章的列表页面了。
<list class="list_root"  for="list">
<list-item class="list_item">
    ... ...
  &lt;/list-item&gt;
</list>::: hljs-center
DSC0004.gif

:::
2.卡片更新
接下来使用服务卡片自带的卡片管理服务,实现卡片周期性刷新等。参考官方文档
只需要在config.json中开启服务卡片的周期性更新,在onUpdateForm(long formId)方法下执行数据获取更新。
config.json文件“abilities”的forms模块配置细节如下
"forms": [
 {
    "jsComponentName": "widget",
    "isDefault": true,
    "scheduledUpdateTime": "10:30",//定点刷新的时刻,采用24小时制,精确到分钟。"updateDuration": 0时,才会生效。
    "defaultDimension": "4*4",
    "name": "widget",
    "description": "This is a service widget",
    "colorMode": "auto",
    "type": "JS",
    "supportDimensions": [
      "2*2",
      "2*4",
      "4*4"
    ],
    "updateEnabled": true,  //表示卡片是否支持周期性刷新
    "updateDuration": 1   //卡片定时刷新的更新周期,1为30分钟,2为60分钟,N为30*N分钟
  },
  ... ...
]可以在配置文件中设置定时或者定点更新卡片,当更新触发时会调用MainAbility下的onUpdateForm(long formId)方法
public class MainAbility extends Ability {
... ...
protected ProviderFormInfo onCreateForm(Intent intent) {...}//在服务卡片上右击>>服务卡片(或上滑)时,通知接口
protected void onUpdateForm(long formId) {...}//在服务卡片请求更新,定时更新时,通知接口
protected void onDeleteForm(long formId) {..}//在服务卡片被删除时,通知接口
protected void onTriggerFormEvent(long formId, String message) {...}//JS服务卡片click时,通知接口
}
3.POST请求
而上面的方法最终调用了卡片控制器WidgetImpl的方法updateFormData()。所以最终需要卡片控制器的updateFormData()中,添加如下更新代码:
@Override
public void updateFormData(long formId, Object... vars) {
HiLog.info(TAG, "update form data timing, default 30 minutes");
//获取文章索引
String url = "https://api-harmonyos.51cto.com/";
Map<String,String> map = new HashMap<>();
map.put("method", "articles.index");
map.put("page", "1");
map.put("page_size", "50");
map.put("sort", "time");
map.put("is_file", "0");
map.put("search_type", "recommend");
map.put("platform_type", "1");
map.put("sign", getSign());
map.put("timestamp", timestamp());
map.put("token", getToken());
ZZRHttp.post(url, map, new ZZRCallBack.CallBackString() {
    @Override
    public void onFailure(int i, String s) {HiLog.info(TAG,"post请求失败");}
@Override
    public void onResponse(String s) {
      HiLog.info(TAG,"post请求成功"+s);
try{
        //解析返回的json字符串
        ArticlesIndex articlesIndex = JSON.parseObject(s,ArticlesIndex.class);
        ArticlesIndex.Data data = articlesIndex.getData();
//获取解析结果中的list列表
        List&lt;ArticlesIndex.Data.list&gt; lists = data.getList();
        ArticlesIndex.Data.list list = lists.get(0);
HiLog.info(TAG,"解析成功");
        //这部分用来更新卡片信息
        ZSONObject zsonObject = new ZSONObject(); //1.将要刷新的数据存放在一个ZSONObject实例中
        zsonObject.put("list",lists); //2.更新数据,对于list控件,可以直接赋值list
        FormBindingData formBindingData = new FormBindingData(zsonObject); //3.将其封装在一个FormBindingData的实例中
        try {
          ((MainAbility)context).updateForm(formId,formBindingData); //4.调用MainAbility的方法updateForm(),并将formBindingData作为第二个实参
        } catch (FormException e) {
          e.printStackTrace();
          HiLog.info(TAG, "更新卡片失败");
        }
      }catch (Exception e){
        HiLog.info(TAG, "解析失败");
      }      
    }
  });
}在上述的代码中,使用了两个包需要导入,同时需要开启程序的联网权限
4.添加权限和依赖包
要在config.json配置文件的module中添加:"reqPermissions": [{"name":"ohos.permission.INTERNET"}],
{
... ...
"module": {
... ...
"reqPermissions": [{"name":"ohos.permission.INTERNET"}]
 }
}
添加依赖包:找到entry/build.gradle文件,在dependencies下添加
dependencies {
<p>implementation fileTree(dir: 'libs', include: ['<em>.jar', '</em>.har'])</p>
<p>testImplementation 'junit:junit:4.13'</p>
<p>ohosTestImplementation 'com.huawei.ohos.testkit:runner:1.0.0.100'</p>
<p>// ZZRHttp 可以单独一个进程进行http请求</p>
<p>implementation 'com.zzrv5.zzrhttp:ZZRHttp:1.0.1'</p>
<p>// fastjson 可以解析JSON格式</p>
<p>implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.75'</p>
}
POST请求最终会得到一段JSON格式的字符串,内容如下图,
::: hljs-center
DSC0005.png

:::
5.解析JSON
但是返回是JSON格式需要进行解析,用的就是前面导入的依赖包fastjson,选择fastjson而不是jackson,是为了java类中只写要解析的数据,其他不需要的可以不写,参考下面的代码。如果不想自己写,也可以百度搜 ”JSON生成Java实体类“,可直接生成。
package com.liangzili.demos.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ArticlesIndex {
public static class Data{
    private String list_type;
    public String getList_type() {
      return list_type;
    }
    public void setList_type(String list_type) {
      this.list_type = list_type;
    }
public static class list{
      public static class Answers_users{
        private String nick_name;
        public String getNick_name() {return nick_name;}
        public void setNick_name(String nick_name) {this.nick_name = nick_name;}
      }
      private List&lt;Answers_users&gt; answers_users;
      public List&lt;Answers_users&gt; getAnswers_users() {return answers_users;}
      public void setAnswers_users(List&lt;Answers_users&gt; answers_users) {this.answers_users = answers_users;}
private List&lt;String&gt; tags;
      public List&lt;String&gt; getTags() {return tags;}
      public void setTags(List&lt;String&gt; tags) {
        List&lt;String&gt; strList = new ArrayList&lt;&gt;();
        for (String str : tags) {
          strList.add("true");
          strList.add(str);
        }
        this.tags = strList;
      }
private String title;
      public String getTitle() {return title;}
      public void setTitle(String title) {this.title = title;}
private Boolean is_good;
      public Boolean getIs_good() {return is_good;}
      public void setIs_good(Boolean is_good) {this.is_good = is_good;}
private Boolean is_top;
      public Boolean getIs_top() {return is_top;}
      public void setIs_top(Boolean is_top) {this.is_top = is_top;}
    };
    private List&lt;list&gt; list;
    public List&lt;Data.list&gt; getList() {return list;}
    public void setList(List&lt;Data.list&gt; list) {this.list = list;}
  };
  private Data data;
  public Data getData() {return data;}
  public void setData(Data data) {this.data = data;}
}在更新数据前,需要设置卡片的index.json内容如下,这个文件的内容和上面我们进行post请求数据时的返回内容格式一致,这样就可以在更新卡片内容时,直接更新list的内容。
{
"data": {
"list": [
 {
    "articles_id":"",
    "articles_type":0,
    "title":"",
    "tags":[
      ""
    ],
    "create_time":"",
    "create_time_all":"",
    "create_time_wap":"",
    "avatar":"",
    "user_id":0,
    "username":"",
    "is_reply":0,
    "views":0,
    "is_good":true,
    "is_file":0,
    "is_question":0,
    "is_video":0,
    "image":"",
    "downloads":0,
    "play_num":0,
    "supports":0,
    "comments":0,
    "duration":0,
    "is_top":true,
    "reply_user_id":0,
    "reply_avatar":"",
    "reply_username":"",
    "reply_time":""
    }
  ]
}
}二、问答服务卡片
接下来是问答模块的服务卡片,效果如图
::: hljs-center
DSC0006.png

:::
这个服务卡片和前面的文章卡片类似,区别就在于POST的请求方法,和JSON的返回值格式不太一样,掌握了方法,稍微修改一下即可,贴一下POST的内容吧
//获取问答模块
String url = "https://api-harmonyos.51cto.com/";
Map<String,String> map = new HashMap<>();
map.put("method", "ask.qList");
map.put("tag_type", "3");
map.put("page", "1");
map.put("page_size", "30");
map.put("q", "");
map.put("platform_type", "1");
map.put("sign", getSign());
map.put("timestamp", timestamp());
map.put("token", getToken());
服务卡片除了信息展示,还有一个重要的功能,通过轻量交互行为实现服务直达、减少层级跳转的。前面的文章推荐卡片没有说跳转,是因为我在list列表的跳转事件上遇到一个坑。
卡片支持click通用事件,事件类型:跳转事件(router)和消息事件(message)。详细说明参考官方文档
消息事件(message)

  • 在index.hml中给要触发的控件上添加onclick,比如:
  • 在index.json中,添加对应的actions
    {
    "data": {
    },
    "actions": {
    "sendMessageEvent": {
    "action": "message",
    "params": {
    "p1": "v1",
    "p2": "v2"
     }
     }
     }
    }
  • 如果是消息事件(message)当点击带有onclick的控件时,会触发MainAbility下的这个函数
    @Override
    protected void onTriggerFormEvent(long formId, String message) {
    HiLog.info(TAG, "onTriggerFormEvent: " + message); //params的内容就通过message传递过来
    super.onTriggerFormEvent(formId, message);
    FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
    FormController formController = formControllerManager.getController(formId);//通过formId得到卡片控制器
    formController.onTriggerFormEvent(formId, message);//接着再调用,控制器 Widget1Impl
    }
  • 最后调用卡片控制器 Widget1Impl 中的onTriggerFormEvent()
    public void onTriggerFormEvent(long formId, String message) {
    HiLog.info(TAG, "onTriggerFormEvent."+message);
    ZSONObject data = ZSONObject.stringToZSON(message);
    String p1 = data.getString("p1");
    String p2 = data.getString("p2");
    HiLog.info(TAG,"p1:"+p1+",p2:"+p2);
    }
跳转事件(router)

  • 在index.hml中给要触发的控件上添加onclick,比如:
  • 在index.json中,添加对应的actions,跳转事件要多加一个参数"abilityName",指定要跳转的页面
    {
    "data": {
    },
    "actions": {
    "sendRouteEvent": {
    "action": "router",
    "abilityName": "com.liangzili.servicewidget.RoutePageAbility",
    "params": {
    "p1": "v1",
    "p2": "v2"
     }
     }
     }
    }
  • 如下图所示添加一个Page Ability,比如:RoutePageAbility
DSC0007.png

IDE会自动在config.json中增加这个页面,没有这个配置信息是无法调用的。
"abilities": [
... ...
 {
    "orientation": "unspecified",
    "name": "com.liangzili.demos.slice.MainAbilityWeb",
    "icon": "$media:icon",
    "description": "$string:mainabilityweb_description",
    "label": "$string:entry_MainAbilityWeb",
    "type": "page",
    "launchType": "standard"
    }</code></pre>

  • 新建完成之后会增加RoutePageAbility 和 slice/RoutePageAbilitySlice 两个文件,可以在下面的代码中添加参数验证
    public class RoutePageAbilitySlice extends AbilitySlice {
    private static final HiLogLabel TAG = new HiLogLabel(HiLog.LOG_APP,0x01818,"卡片跳转");
    @Override
    public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_bilibili_page);
    //添加参数验证
    String param = intent.getStringParam("params");//从intent中获取 跳转事件定义的params字段的值
    if(param !=null){
           HiLog.info(TAG,"param:"+param);
           ZSONObject data = ZSONObject.stringToZSON(param);
           String p1 = data.getString("p1");
           String p2 = data.getString("p2");
           HiLog.info(TAG,"p1:"+p1+",p2:"+p2);
         }
    }
    }
list跳转事件
list组件只能添加一个onclick,所以就有个问题,在点击的同时还需要获取点击的是list列表中的哪一项。
<list class="list_root"  for="list">
<list-item class="list_item">
    &lt;div class="div_title" &gt;&lt;!--第一行标题--&gt;
      &lt;text class="div_title_title"&gt;{{$item.title}}&lt;/text&gt;
    &lt;/div&gt;
    &lt;div class="div_tags"&gt;&lt;!--第二行标签--&gt;
      &lt;text class="div_tags_text"  style="display-index: 5;" if="{{$item.tags[0]}}"&gt;{{$item.tags[1]}}&lt;/text&gt;
      &lt;text class="div_tags_text"  style="display-index: 4;" if="{{$item.tags[2]}}"&gt;{{$item.tags[3]}}&lt;/text&gt;
      &lt;text class="div_tags_text"  style="display-index: 3;" if="{{$item.tags[4]}}"&gt;{{$item.tags[5]}}&lt;/text&gt;
      &lt;text class="div_tags_text"  style="display-index: 2;" if="{{$item.tags[6]}}"&gt;{{$item.tags[7]}}&lt;/text&gt;
      &lt;text class="div_tags_text"  style="display-index: 1;" if="{{$item.tags[8]}}"&gt;{{$item.tags[9]}}&lt;/text&gt;
    &lt;/div&gt;
    &lt;div class="div_user"&gt;&lt;!--第三行作者--&gt;
      &lt;text class="div_user_username" style="display-index: 2;"&gt;{{$item.answers_user[0].nick_name}}&lt;/text&gt;
      &lt;text class="div_user_username" style="display-index: 1;"&gt;{{$item.created_at}}&lt;/text&gt;
    &lt;/div&gt;
    &lt;divider class="divider"&gt;&lt;/divider&gt;
  &lt;/list-item&gt;
</list>这个坑折磨了我好久,最终我发现在index.json中,可以使用$item,$idx获取到hml页面list的元素变量和索引。但是在官方文档并没有找到相关的内容,尝试了很久才解决这个问题。
"actions": {
"sendRouteEvent": {
    "action": "router",
    "abilityName": "com.liangzili.demos.MainAbility",
    "params": {
      "index": "{{$idx}}",
      "url": "{{$item.url}}"
    }
  }</code></pre>
三、荣誉认证卡片
这两个服务卡片的和之前的卡片略有不同,主要是因为这两个服务卡片的信息需要登录账号才能够获取到.
DSC0008.png

1.webview
在鸿蒙中webview提供在应用中集成Web页面的能力。首先在打开APP的时候显示HarmonyOS技术社区的首页,在base/layout/ability_main.xml中添加
<ohos.agp.components.webengine.WebView
ohos:id="$+id:webview"
ohos:height="match_parent"
ohos:width="match_parent">
</ohos.agp.components.webengine.WebView>
接着在com/liangzili/demos/slice/MainAbilitySlice.java的启动函数中添加如下代码
@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);
    //启动webview
    WebView webView = (WebView) findComponentById(ResourceTable.Id_webview);
    webView.getWebConfig().setJavaScriptPermit(true);   // 如果网页需要使用JavaScript,增加此行;如何使用JavaScript下文有详细介绍
// 坑:51cto的主页首次打开会有个弹窗,关闭弹窗会在Local Storage中设置"coupon=1",不开启这个将无法关闭弹窗。
    webView.getWebConfig().setWebStoragePermit(true);     // 设置是否启用HTML5 DOM存储。
    String url ="https://harmonyos-m.51cto.com";
    webView.load(url);  
  }</code></pre>
2.取消标题栏
但此时还有个小问题就是这个标题栏,强迫症的我着实觉的太难受,需要添加一个配置取消这个标题栏,配置是网上查的,啥意思我不太清楚,能用就好
DSC0009.png

DSC00010.png

在abilities下要隐藏标题栏的页面下添加下面配置,添加在哪个页面隐藏哪个。
"metaData":{
"customizeData":[
    {
      "name": "hwc-theme",
      "value": "androidhwext:style/Theme.Emui.Light.NoTitleBar",
      "extra": ""
    }
  ]
},3.保存cookie
主要用到的就是CookieStore,在文档中CookieStore有一个persist()方法,看描述应该就是保存cookie信息的意思,参考官方文档
Modifier and TypeMethodDescriptionabstract voidpersist()Saves cookies to the device's persistent storage.这里我又双叕遇到一个坑,这个persist()方法我尝试了很多次,只要清理后台,cookie就会丢失,难道是我对这个方法有什么误解,打开的方式不对。到现在也没有成功,有知道如何使用的大佬,麻烦告知一声,这里先行谢过了。
4.使用偏好型数据库
没办法只能取出cookie的内容,然后一条条保存到数据库了。首先制造一个保存指定域名Cookie的函数,使用关系型数据库
public void saveCookie(String url,String filename){
//先取出要保存的cookie
CookieStore cookieStore = CookieStore.getInstance();
String cookieStr = cookieStore.getCookie(url);
HiLog.info(TAG,"saveCookie(String url)"+url+cookieStr);
//然后将cooke转成map
Map<String,String> cookieMap = cookieToMap(cookieStr);
//最后将map写入数据库
MaptoDB(cookieMap,filename);
}
// cookieToMap
public static Map<String,String> cookieToMap(String value) {
Map<String, String> map = new HashMap<String, String>();
value = value.replace(" ", "");
if (value.contains(";")) {
    String values[] = value.split(";");
    for (String val : values) {
      String vals[] = val.split("=");
      map.put(vals[0], vals[1]);
    }
  } else {
    String values[] = value.split("=");
    map.put(values[0], values[1]);
  }
  return map;
}// 将map写入数据库
public void MaptoDB(Map<String,String> map,String filename){
// 开启数据库
context = getContext();
DatabaseHelper databaseHelper = new DatabaseHelper(context);//1.创建数据库使用数据库操作的辅助类
Preferences preferences = databaseHelper.getPreferences(filename);//2.获取到对应文件名的Preferences实例
// 遍历map
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
    preferences.putString(entry.getKey(),entry.getValue());//3.将数据写入Preferences实例,
  }
  preferences.flushSync();//4.通过flush()或者flushSync()将Preferences实例持久化。
}接着制造从数据库中读取Cookie的函数
public void readCookie(String url,String filename){
Map<String, ?> map = new HashMap<>();
//先从数据库中取出cookie
map = DBtoMap(filename);
//然后写入到cookieStore
CookieStore cookieStore = CookieStore.getInstance();//1.获取一个CookieStore的示例
for (Map.Entry<String, ?> entry : map.entrySet()) {
    System.out.println(entry.getKey()+"="+entry.getValue().toString());
    cookieStore.setCookie(url,entry.getKey()+"="+entry.getValue().toString());//2.写入数据,只能一条一条写
  }
}最后在启动时调用readCookie,结束时调用saveCookie就可以了。
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
readCookie("https://harmonyos-m.51cto.com","harmonyos-m");
readCookie("https://home.51cto.com","home");
readCookie("https://ucenter.51cto.com","ucenter");
}
@Override
protected void onStop() {}
@Override
protected void onBackground() {
saveCookie("https://harmonyos-m.51cto.com","harmonyos-m");
saveCookie("https://home.51cto.com","home");
saveCookie("https://ucenter.51cto.com","ucenter");
}
不过这样操作会触发一个新的问题,这里就不深究了,已经偏离服务卡片的初衷了。这里要感谢下社区@Whyalone,感谢指点迷津。听说51CTO官方版的服务卡片也马上上线了,期待啊!!
以上就是我制作51社区服务卡片的过程了,如果对你们有所帮助别忘了点赞支持啊,如果有问题也欢迎留言进行交流。
想了解更多关于鸿蒙的内容,请访问:
51CTO和华为官方战略合作共建的鸿蒙技术社区
https://harmonyos.51cto.com/#bkwz

关注下面的标签,发现更多相似文章