OpenHarmony开发实例:【仿桌面应用】

 介绍

本示例实现了一个简单桌面应用,实现了以下几点功能:

1.展示了系统安装的应用,实现点击启动、应用上滑弹出卡片、卡片添加到桌面、卡片移除功能。

2.实现桌面数据持久化存储,应用支持卸载、监听应用卸载和安装并显示。

3.实现最近任务管理功能,包括任务卡片加锁、解锁、清理和清理所有任务功能。

4.通过点击应用图标或点击由长按图标弹出的菜单栏中的打开按钮的方式打开应用,是以打开最近任务方式拉起应用Ability。

效果预览

image.png

使用说明

1.安装编译的hap包,使用hdc shell aa start -b ohos.samples.launcher -a MainAbility命令启动应用,应用启动后显示系统安装的应用。

2.点击应用主界面上的应用图标可以启动应用,长按弹出菜单,点击打开可以正常启动应用。

3.图库等支持卡片的应用,长按菜单中有服务卡片,点击进入卡片预览界面,在卡片预览界面点击添加到桌面,返回到桌面并且卡片成功添加到桌面。

4.上滑图库等支持卡片的应用,可以弹出默认上滑卡片,点击上滑卡片右上角的**+**图标,可以添加卡片到桌面。

5.应用在桌面界面,使用hdc install安装一个应用,桌面可以监听到应用安装,并显示新安装的应用到桌面上。

6.应用在桌面界面,使用hdc uninstall 卸载第5步安装的应用,桌面可以监听到卸载,并移除桌面上的应用。

7.在桌面空白处上滑,可以进入最近任务管理界面,下滑任务卡片可以加锁/解锁,上滑卡片可以清理该后台任务,点击垃圾桶可以清除所有后台任务(加锁的应用不会被清理掉)。

代码解读

鸿蒙next开发文档参考了:qr23.cn/AKFP8k点击或者复制转到。

entry/src/main/ets/
|---Application
|   |---MyAbilityStage.ts
|---components
|   |---FormManagerComponent.ets               // 弹窗组件
|---MainAbility
|   |---MainAbility.ts
|---manager
|   |---WindowManager.ts                       // 数据类型
|---pages
|   |---FormPage.ets                           // 首页
|   |---Home.ets                               // 详情页面
|   |---RecentsPage.ets                        // 详情页面

具体实现

搜狗高速浏览器截图20240326151344.png

  • 获取应用功能模块

    • 使用launcherBundleManager模块接口(系统能力:SystemCapability.BundleManager.BundleFramework),获取所有应用信息和给定包名获取应用信息,实现桌面展示所有安装的应用。使用on接口监听应用的安装和卸载从而实现应用安装和卸载刷新桌面。
  • 源码链接:[LauncherAbilityManager.ts]

/*

 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */



import installer from '@ohos.bundle.installer';

import launcherBundleManager from '@ohos.bundle.launcherBundleManager';

import bundleMonitor from '@ohos.bundle.bundleMonitor';

import osAccount from '@ohos.account.osAccount'

import { AppItemInfo } from '../bean/AppItemInfo'

import { CheckEmptyUtils } from '../utils/CheckEmptyUtils'

import { CommonConstants } from '../constants/CommonConstants'

import { EventConstants } from '../constants/EventConstants'

import { ResourceManager } from './ResourceManager'

import { Logger } from '../utils/Logger'

import type { BusinessError } from '@ohos.base';



const TAG: string = 'LauncherAbilityManager'



/**

 * Wrapper class for innerBundleManager and formManager interfaces.

 */

export class LauncherAbilityManager {

  private static readonly BUNDLE_STATUS_CHANGE_KEY_REMOVE = 'remove'

  private static readonly BUNDLE_STATUS_CHANGE_KEY_ADD = 'add'

  private static launcherAbilityManager: LauncherAbilityManager = undefined

  private readonly mAppMap = new Map<string, AppItemInfo>()

  private mResourceManager: ResourceManager = undefined

  private readonly mLauncherAbilityChangeListeners: any[] = []

  private mUserId: number = 100

  private context: any = undefined



  constructor(context) {

    this.context = context

    this.mResourceManager = ResourceManager.getInstance(context)

    const osAccountManager = osAccount.getAccountManager()

    osAccountManager.getOsAccountLocalIdFromProcess((err, localId) => {

      Logger.debug(TAG, `getOsAccountLocalIdFromProcess localId ${localId}`)

      this.mUserId = localId

    })

  }



  /**

   * Get the application data model object.

   *

   * @return {object} application data model singleton

   */

  static getInstance(context): LauncherAbilityManager {

    if (this.launcherAbilityManager === null || this.launcherAbilityManager === undefined) {

      this.launcherAbilityManager = new LauncherAbilityManager(context)

    }

    return this.launcherAbilityManager

  }



  /**

   * get all app List info from BMS

   *

   * @return 应用的入口Ability信息列表

   */

  async getLauncherAbilityList(): Promise<AppItemInfo[]> {

    Logger.info(TAG, 'getLauncherAbilityList begin')

    let abilityList = await launcherBundleManager.getAllLauncherAbilityInfo(this.mUserId)

    const appItemInfoList = new Array<AppItemInfo>()

    if (CheckEmptyUtils.isEmpty(abilityList)) {

      Logger.info(TAG, 'getLauncherAbilityList Empty')

      return appItemInfoList

    }

    for (let i = 0; i < abilityList.length; i++) {

      let appItem = await this.transToAppItemInfo(abilityList[i])

      appItemInfoList.push(appItem)

    }

    return appItemInfoList

  }



  /**

   * get AppItemInfo from BMS with bundleName

   * @params bundleName

   * @return AppItemInfo

   */

  async getAppInfoByBundleName(bundleName: string): Promise<AppItemInfo | undefined> {

    let appItemInfo: AppItemInfo | undefined = undefined

    // get from cache

    if (this.mAppMap != null && this.mAppMap.has(bundleName)) {

      appItemInfo = this.mAppMap.get(bundleName)

    }

    if (appItemInfo != undefined) {

      Logger.info(TAG, `getAppInfoByBundleName from cache: ${JSON.stringify(appItemInfo)}`)

      return appItemInfo

    }

    // get from system

    let abilityInfos = await launcherBundleManager.getLauncherAbilityInfo(bundleName, this.mUserId)

    if (abilityInfos == undefined || abilityInfos.length == 0) {

      Logger.info(TAG, `${bundleName} has no launcher ability`)

      return undefined

    }

    let appInfo = abilityInfos[0]

    const data = await this.transToAppItemInfo(appInfo)

    Logger.info(TAG, `getAppInfoByBundleName from BMS: ${JSON.stringify(data)}`)

    return data

  }



  private async transToAppItemInfo(info): Promise<AppItemInfo> {

    const appItemInfo = new AppItemInfo()

    appItemInfo.appName = await this.mResourceManager.getAppNameSync(

    info.labelId, info.elementName.bundleName, info.applicationInfo.label

    )

    appItemInfo.isSystemApp = info.applicationInfo.systemApp

    appItemInfo.isUninstallAble = info.applicationInfo.removable

    appItemInfo.appIconId = info.iconId

    appItemInfo.appLabelId = info.labelId

    appItemInfo.bundleName = info.elementName.bundleName

    appItemInfo.abilityName = info.elementName.abilityName

    await this.mResourceManager.updateIconCache(appItemInfo.appIconId, appItemInfo.bundleName)

    this.mAppMap.set(appItemInfo.bundleName, appItemInfo)

    return appItemInfo

  }





  /**

   * 启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbility(paramAbilityName, paramBundleName) {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`)

    this.context.startAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success')

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`)

    })

  }



  /**

   * 通过桌面图标启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbilityFromRecent(paramAbilityName, paramBundleName): void {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`);

    this.context.startRecentAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success');

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`);

    });

  }



  /**

   * 卸载应用

   *

   * @params bundleName 应用包名

   * @params callback 卸载回调

   */

  async uninstallLauncherAbility(bundleName: string, callback): Promise<void> {

    Logger.info(TAG, `uninstallLauncherAbility bundleName: ${bundleName}`);

    const bundlerInstaller = await installer.getBundleInstaller();

    bundlerInstaller.uninstall(bundleName, {

      userId: this.mUserId,

      installFlag: 0,

      isKeepData: false

    }, (err: BusinessError) => {

      Logger.info(TAG, `uninstallLauncherAbility result => ${JSON.stringify(err)}`);

      callback(err);

    })

  }



  /**

   * 开始监听系统应用状态.

   *

   * @params listener 监听对象

   */

  registerLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback add bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_ADDED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

        })

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback remove bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_REMOVED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

          AppStorage.Set('isRefresh', true)

        })

      }

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index == CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.push(listener)

      }

    }

  }



  /**

   * 取消监听系统应用状态.

   *

   * @params listener 监听对象

   */

  unregisterLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index != CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.splice(index, 1)

      }

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD)

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE)

      }

    }

  }



  private notifyLauncherAbilityChange(event: string, bundleName: string, userId: number): void {

    for (let index = 0; index < this.mLauncherAbilityChangeListeners.length; index++) {

      this.mLauncherAbilityChangeListeners[index](event, bundleName, userId)

    }

  }

}
  • 接口参考:[@ohos.bundle.launcherBundleManager]

  • 应用卸载功能模块

  • 使用bundle模块的getBundleInstaller接口获取到BundleInstaller(系统能力:SystemCapability.BundleManager.BundleFramework),调用uninstall接口实现应用卸载功能。

  • 源码链接:[LauncherAbilityManager.ts]

/*

 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */



import installer from '@ohos.bundle.installer';

import launcherBundleManager from '@ohos.bundle.launcherBundleManager';

import bundleMonitor from '@ohos.bundle.bundleMonitor';

import osAccount from '@ohos.account.osAccount'

import { AppItemInfo } from '../bean/AppItemInfo'

import { CheckEmptyUtils } from '../utils/CheckEmptyUtils'

import { CommonConstants } from '../constants/CommonConstants'

import { EventConstants } from '../constants/EventConstants'

import { ResourceManager } from './ResourceManager'

import { Logger } from '../utils/Logger'

import type { BusinessError } from '@ohos.base';



const TAG: string = 'LauncherAbilityManager'



/**

 * Wrapper class for innerBundleManager and formManager interfaces.

 */

export class LauncherAbilityManager {

  private static readonly BUNDLE_STATUS_CHANGE_KEY_REMOVE = 'remove'

  private static readonly BUNDLE_STATUS_CHANGE_KEY_ADD = 'add'

  private static launcherAbilityManager: LauncherAbilityManager = undefined

  private readonly mAppMap = new Map<string, AppItemInfo>()

  private mResourceManager: ResourceManager = undefined

  private readonly mLauncherAbilityChangeListeners: any[] = []

  private mUserId: number = 100

  private context: any = undefined



  constructor(context) {

    this.context = context

    this.mResourceManager = ResourceManager.getInstance(context)

    const osAccountManager = osAccount.getAccountManager()

    osAccountManager.getOsAccountLocalIdFromProcess((err, localId) => {

      Logger.debug(TAG, `getOsAccountLocalIdFromProcess localId ${localId}`)

      this.mUserId = localId

    })

  }



  /**

   * Get the application data model object.

   *

   * @return {object} application data model singleton

   */

  static getInstance(context): LauncherAbilityManager {

    if (this.launcherAbilityManager === null || this.launcherAbilityManager === undefined) {

      this.launcherAbilityManager = new LauncherAbilityManager(context)

    }

    return this.launcherAbilityManager

  }



  /**

   * get all app List info from BMS

   *

   * @return 应用的入口Ability信息列表

   */

  async getLauncherAbilityList(): Promise<AppItemInfo[]> {

    Logger.info(TAG, 'getLauncherAbilityList begin')

    let abilityList = await launcherBundleManager.getAllLauncherAbilityInfo(this.mUserId)

    const appItemInfoList = new Array<AppItemInfo>()

    if (CheckEmptyUtils.isEmpty(abilityList)) {

      Logger.info(TAG, 'getLauncherAbilityList Empty')

      return appItemInfoList

    }

    for (let i = 0; i < abilityList.length; i++) {

      let appItem = await this.transToAppItemInfo(abilityList[i])

      appItemInfoList.push(appItem)

    }

    return appItemInfoList

  }



  /**

   * get AppItemInfo from BMS with bundleName

   * @params bundleName

   * @return AppItemInfo

   */

  async getAppInfoByBundleName(bundleName: string): Promise<AppItemInfo | undefined> {

    let appItemInfo: AppItemInfo | undefined = undefined

    // get from cache

    if (this.mAppMap != null && this.mAppMap.has(bundleName)) {

      appItemInfo = this.mAppMap.get(bundleName)

    }

    if (appItemInfo != undefined) {

      Logger.info(TAG, `getAppInfoByBundleName from cache: ${JSON.stringify(appItemInfo)}`)

      return appItemInfo

    }

    // get from system

    let abilityInfos = await launcherBundleManager.getLauncherAbilityInfo(bundleName, this.mUserId)

    if (abilityInfos == undefined || abilityInfos.length == 0) {

      Logger.info(TAG, `${bundleName} has no launcher ability`)

      return undefined

    }

    let appInfo = abilityInfos[0]

    const data = await this.transToAppItemInfo(appInfo)

    Logger.info(TAG, `getAppInfoByBundleName from BMS: ${JSON.stringify(data)}`)

    return data

  }



  private async transToAppItemInfo(info): Promise<AppItemInfo> {

    const appItemInfo = new AppItemInfo()

    appItemInfo.appName = await this.mResourceManager.getAppNameSync(

    info.labelId, info.elementName.bundleName, info.applicationInfo.label

    )

    appItemInfo.isSystemApp = info.applicationInfo.systemApp

    appItemInfo.isUninstallAble = info.applicationInfo.removable

    appItemInfo.appIconId = info.iconId

    appItemInfo.appLabelId = info.labelId

    appItemInfo.bundleName = info.elementName.bundleName

    appItemInfo.abilityName = info.elementName.abilityName

    await this.mResourceManager.updateIconCache(appItemInfo.appIconId, appItemInfo.bundleName)

    this.mAppMap.set(appItemInfo.bundleName, appItemInfo)

    return appItemInfo

  }





  /**

   * 启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbility(paramAbilityName, paramBundleName) {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`)

    this.context.startAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success')

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`)

    })

  }



  /**

   * 通过桌面图标启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbilityFromRecent(paramAbilityName, paramBundleName): void {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`);

    this.context.startRecentAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success');

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`);

    });

  }



  /**

   * 卸载应用

   *

   * @params bundleName 应用包名

   * @params callback 卸载回调

   */

  async uninstallLauncherAbility(bundleName: string, callback): Promise<void> {

    Logger.info(TAG, `uninstallLauncherAbility bundleName: ${bundleName}`);

    const bundlerInstaller = await installer.getBundleInstaller();

    bundlerInstaller.uninstall(bundleName, {

      userId: this.mUserId,

      installFlag: 0,

      isKeepData: false

    }, (err: BusinessError) => {

      Logger.info(TAG, `uninstallLauncherAbility result => ${JSON.stringify(err)}`);

      callback(err);

    })

  }



  /**

   * 开始监听系统应用状态.

   *

   * @params listener 监听对象

   */

  registerLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback add bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_ADDED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

        })

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback remove bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_REMOVED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

          AppStorage.Set('isRefresh', true)

        })

      }

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index == CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.push(listener)

      }

    }

  }



  /**

   * 取消监听系统应用状态.

   *

   * @params listener 监听对象

   */

  unregisterLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index != CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.splice(index, 1)

      }

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD)

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE)

      }

    }

  }



  private notifyLauncherAbilityChange(event: string, bundleName: string, userId: number): void {

    for (let index = 0; index < this.mLauncherAbilityChangeListeners.length; index++) {

      this.mLauncherAbilityChangeListeners[index](event, bundleName, userId)

    }

  }

}

  • 接口参考:[@ohos.bundle]

  • 添加卡片功能模块

  • 使用formHost接口(系统能力:SystemCapability.Ability.Form),获取应用卡片信息,使用FormComponent组件展示卡片内容,从而实现添加卡片到桌面的功能。

  • 源码链接:[FormManager.ts]

/*

 * Copyright (c) 2022 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */



import formManagerAbility from '@ohos.app.form.formHost'

import { CardItemInfo } from '../bean/CardItemInfo'

import { CommonConstants } from '../constants/CommonConstants'

import { Logger } from '../utils/Logger'



const TAG: string = 'FormManager'



/**

 * Wrapper class for formManager interfaces.

 */

class FormManagerModel {

  private readonly CARD_SIZE_1x2: number[] = [1, 2]

  private readonly CARD_SIZE_2x2: number[] = [2, 2]

  private readonly CARD_SIZE_2x4: number[] = [2, 4]

  private readonly CARD_SIZE_4x4: number[] = [4, 4]



  /**

   * get all form info

   *

   * @return Array<CardItemInfo> cardItemInfoList

   */

  public async getAllFormsInfo(): Promise<CardItemInfo[]> {

    const formList = await formManagerAbility.getAllFormsInfo()

    const cardItemInfoList = new Array<CardItemInfo>()

    for (const formItem of formList) {

      const cardItemInfo = new CardItemInfo()

      cardItemInfo.bundleName = formItem.bundleName

      cardItemInfo.abilityName = formItem.abilityName

      cardItemInfo.moduleName = formItem.moduleName

      cardItemInfo.cardName = formItem.name

      cardItemInfo.cardDimension = formItem.defaultDimension

      cardItemInfo.description = formItem.description

      cardItemInfo.formConfigAbility = formItem.formConfigAbility

      cardItemInfo.supportDimensions = formItem.supportDimensions

      cardItemInfo.area = this.getCardSize(cardItemInfo.cardDimension)

      cardItemInfoList.push(cardItemInfo)

    }

    return cardItemInfoList

  }



  /**

   * get card area by dimension

   *

   * @param dimension

   * @return number[]

   */

  public getCardSize(dimension: number): number[] {

    if (dimension == CommonConstants.CARD_DIMENSION_1x2) {

      return this.CARD_SIZE_1x2

    } else if (dimension == CommonConstants.CARD_DIMENSION_2x2) {

      return this.CARD_SIZE_2x2

    } else if (dimension == CommonConstants.CARD_DIMENSION_2x4) {

      return this.CARD_SIZE_2x4

    } else {

      return this.CARD_SIZE_4x4

    }

  }



  /**

   * get card dimension bty area

   *

   * @param dimension

   * @return number[]

   */

  public getCardDimension(area: number[]) {

    if (area.toString() === this.CARD_SIZE_1x2.toString()) {

      return CommonConstants.CARD_DIMENSION_1x2

    } else if (area.toString() === this.CARD_SIZE_2x2.toString()) {

      return CommonConstants.CARD_DIMENSION_2x2

    } else if (area.toString() == this.CARD_SIZE_2x4.toString()) {

      return CommonConstants.CARD_DIMENSION_2x4

    } else {

      return CommonConstants.CARD_DIMENSION_4x4

    }

  }



  /**

   * get form info by bundleName

   *

   * @param bundle

   * @return Array<CardItemInfo> cardItemInfoList

   */

  public async getFormsInfoByApp(bundle: string): Promise<CardItemInfo[]> {

    Logger.info(TAG, `getFormsInfoByApp bundle: ${bundle}`)

    const formList = await formManagerAbility.getFormsInfo(bundle)

    const cardItemInfoList = new Array<CardItemInfo>()

    for (const formItem of formList) {

      const cardItemInfo = new CardItemInfo()

      cardItemInfo.bundleName = formItem.bundleName

      cardItemInfo.abilityName = formItem.abilityName

      cardItemInfo.moduleName = formItem.moduleName

      cardItemInfo.cardName = formItem.name

      cardItemInfo.cardDimension = formItem.defaultDimension

      cardItemInfo.area = this.getCardSize(cardItemInfo.cardDimension)

      cardItemInfo.description = formItem.description

      cardItemInfo.formConfigAbility = formItem.formConfigAbility

      cardItemInfo.supportDimensions = formItem.supportDimensions

      cardItemInfoList.push(cardItemInfo)

    }

    return cardItemInfoList

  }

}



export let FormManager =  new FormManagerModel()
  • 接口参考:[@ohos.app.form.formHost]

  • 桌面数据持久化存储功能模块

    • 使用关系型数据库rdb接口(系统能力:SystemCapability.DistributedDataManager.RelationalStore.Core),实现桌面数据持久化存储,存储应用的位置信息,卡片信息。
  • 源码链接:[RdbManager.ts]

/*

 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */



import dataRdb from '@ohos.data.relationalStore'

import { CheckEmptyUtils } from '../utils/CheckEmptyUtils'

import { CommonConstants } from '../constants/CommonConstants'

import { GridLayoutItemInfo } from '../bean/GridLayoutItemInfo'

import { GridLayoutInfoColumns } from '../bean/GridLayoutInfoColumns'

import { Logger } from '../utils/Logger'



export const TABLE_NAME: string = 'launcher'



export const SQL_CREATE_TABLE = 'CREATE TABLE IF NOT EXISTS launcher ' +

'(id INTEGER PRIMARY KEY AUTOINCREMENT, ' +

'app_name TEXT, ' +

'appIcon_id INTEGER, ' +

'container INTEGER, ' +

'type_id INTEGER, ' +

'card_id INTEGER, ' +

'card_name TEXT, ' +

'badge_number INTEGER, ' +

'module_name TEXT, ' +

'bundle_name TEXT, ' +

'ability_name TEXT, ' +

'area TEXT, ' +

'page INTEGER, ' +

'column INTEGER, ' +

'row INTEGER)'



export const STORE_CONFIG = { name: 'launcher.db', securityLevel: dataRdb.SecurityLevel.S1 }

const TAG: string = 'RdbModel'

class RdbManagerModel {

  private mRdbStore: dataRdb.RdbStore = undefined



  constructor() {

  }



  /**

   * initRdbConfig

   *

   * @param context

   */

  async initRdbConfig(context): Promise<void> {

    Logger.info(TAG, 'initRdbConfig start')

    if (this.mRdbStore === undefined) {

      this.mRdbStore = await dataRdb.getRdbStore(context, STORE_CONFIG);

      await this.mRdbStore.executeSql(SQL_CREATE_TABLE);

      Logger.info(TAG, 'create table end');

    }

  }



  /**

   * deleteTable

   *

   * @param tableName

   */

  async deleteTable(tableName: string): Promise<void> {

    Logger.info(TAG, 'deleteTable start')

    try {

      let detelSql = `DELETE FROM ${tableName};`

      let detelSequenceSql = `UPDATE sqlite_sequence SET seq=0 WHERE name = '${tableName}';`

      await this.mRdbStore.executeSql(detelSql)

      await this.mRdbStore.executeSql(detelSequenceSql)

      Logger.debug(TAG, 'deleteTable end')

    } catch (e) {

      Logger.error(TAG, `deleteTable err: ${e}`)

    }

  }



  /**

   * insertData

   *

   * @param layoutInfo

   */

  async insertData(layoutInfo: any) {

    Logger.info(TAG, 'insertGridLayoutInfo start');

    let result: boolean = true

    if (CheckEmptyUtils.isEmpty(layoutInfo)) {

      Logger.error(TAG, 'insertGridLayoutInfo gridlayoutinfo is empty')

      result = false

      return result

    }

    try {

      // delete gridlayoutinfo table

      await this.deleteTable(TABLE_NAME)

      // insert into gridlayoutinfo

      for (let i in layoutInfo) {

        let layout = layoutInfo[i]

        for (let j in layout) {

          let element = layout[j]

          Logger.info(TAG, `insertGridLayoutInfo i= ${i}`)

          let item = {}

          if (element.typeId === CommonConstants.TYPE_APP) {

            item = {

              'app_name': element.appName,

              'bundle_name': element.bundleName,

              'module_name': element.modelName,

              'ability_name': element.abilityName,

              'appIcon_id': element.appIconId,

              'type_id': element.typeId,

              'area': element.area[0] + ',' + element.area[1],

              'page': element.page,

              'column': element.column,

              'row': element.row,

              'container': -100

            }

            let ret = await this.mRdbStore.insert(TABLE_NAME, item)

            Logger.debug(TAG, `insertGridLayoutInfo type is app ${i} ret: ${ret}`)

          } else if (element.typeId === CommonConstants.TYPE_CARD) {

            item = {

              'app_name': element.appName,

              'bundle_name': element.bundleName,

              'module_name': element.modelName,

              'ability_name': element.abilityName,

              'card_id': element.cardId,

              'card_name': element.cardName,

              'type_id': element.typeId,

              'area': element.area[0] + ',' + element.area[1],

              'page': element.page,

              'column': element.column,

              'row': element.row,

              'container': -100

            }

            let ret = await this.mRdbStore.insert(TABLE_NAME, item)

            Logger.debug(TAG, `insertGridLayoutInfo type is card ${i} ret: ${ret}`)

          }

        }

      }

    } catch (e) {

      Logger.error(TAG, `insertGridLayoutInfo error: ${e}`)

    }

    return result

  }



  async queryLayoutInfo() {

    Logger.info(TAG, 'queryLayoutInfo start')

    const resultList: GridLayoutItemInfo[] = []

    const predicates = new dataRdb.RdbPredicates(TABLE_NAME)

    predicates.equalTo(GridLayoutInfoColumns.CONTAINER, -100)

      .and().orderByAsc('page').and().orderByAsc('row').and().orderByAsc('column')

    let resultSet = await this.mRdbStore.query(predicates)

    Logger.info(TAG, `queryLayoutInfo query,count=${resultSet.rowCount}`)

    let isLast = resultSet.goToFirstRow()

    while (isLast) {

      const layoutInfo: GridLayoutItemInfo = GridLayoutItemInfo.fromResultSet(resultSet)

      resultList.push(layoutInfo)

      isLast = resultSet.goToNextRow()

    }

    resultSet.close()

    resultSet = null

    return resultList

  }



  async insertItem(item: GridLayoutItemInfo) {

    if (CheckEmptyUtils.isEmpty(item)) {

      return

    }

    let element = {

      'app_name': item.appName,

      'module_name': item.moduleName,

      'bundle_name': item.bundleName,

      'ability_name': item.abilityName,

      'appIcon_id': item.appIconId,

      'card_id': item.cardId,

      'card_name': item.cardName,

      'type_id': item.typeId,

      'area': item.area[0] + ',' + item.area[1],

      'page': item.page,

      'column': item.column,

      'row': item.row,

      'container': -100

    }

    let ret = await this.mRdbStore.insert(TABLE_NAME, element)

    Logger.debug(TAG, `insertGridLayoutInfo ret: ${ret}`)

  }



  async deleteItemByPosition(page: number, row: number, column: number) {

    const predicates = new dataRdb.RdbPredicates(TABLE_NAME);

    predicates.equalTo('page', page)

      .and().equalTo('row', row)

      .and().equalTo('column', column);

    let query = await this.mRdbStore.query(predicates);

    if (query.rowCount > 0) {

      let ret = await this.mRdbStore.delete(predicates);

      Logger.debug(TAG, `deleteItem ret: ${ret}`);

    }

  }

}



export let RdbManager = new RdbManagerModel()
  • 接口参考:[@ohos.data.relationalStore]

  • 加锁、解锁、清理后台任务功能模块

    • 使用missionManager模块接口(系统能力:SystemCapability.Ability.AbilityRuntime.Mission),获取最近任务信息,并实现加锁、解锁、清理后台任务的功能。
    • 源码链接:[MissionModel.ts]
    • 接口参考:[@ohos.application.missionManager]
  • 点击桌面应用拉起最近任务至前台功能模块

    • 使用ServiceExtensionContext模块的startRecentAbility接口(系统能力:SystemCapability.Ability.AbilityRuntime.Core),拉起最近任务至前台显示,若应用Ability未启动时,则拉起新创建的应用Ability显示到前台。 源码链接:[LauncherAbilityManager.ts]

  
/*

 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */



import installer from '@ohos.bundle.installer';

import launcherBundleManager from '@ohos.bundle.launcherBundleManager';

import bundleMonitor from '@ohos.bundle.bundleMonitor';

import osAccount from '@ohos.account.osAccount'

import { AppItemInfo } from '../bean/AppItemInfo'

import { CheckEmptyUtils } from '../utils/CheckEmptyUtils'

import { CommonConstants } from '../constants/CommonConstants'

import { EventConstants } from '../constants/EventConstants'

import { ResourceManager } from './ResourceManager'

import { Logger } from '../utils/Logger'

import type { BusinessError } from '@ohos.base';



const TAG: string = 'LauncherAbilityManager'



/**

 * Wrapper class for innerBundleManager and formManager interfaces.

 */

export class LauncherAbilityManager {

  private static readonly BUNDLE_STATUS_CHANGE_KEY_REMOVE = 'remove'

  private static readonly BUNDLE_STATUS_CHANGE_KEY_ADD = 'add'

  private static launcherAbilityManager: LauncherAbilityManager = undefined

  private readonly mAppMap = new Map<string, AppItemInfo>()

  private mResourceManager: ResourceManager = undefined

  private readonly mLauncherAbilityChangeListeners: any[] = []

  private mUserId: number = 100

  private context: any = undefined



  constructor(context) {

    this.context = context

    this.mResourceManager = ResourceManager.getInstance(context)

    const osAccountManager = osAccount.getAccountManager()

    osAccountManager.getOsAccountLocalIdFromProcess((err, localId) => {

      Logger.debug(TAG, `getOsAccountLocalIdFromProcess localId ${localId}`)

      this.mUserId = localId

    })

  }



  /**

   * Get the application data model object.

   *

   * @return {object} application data model singleton

   */

  static getInstance(context): LauncherAbilityManager {

    if (this.launcherAbilityManager === null || this.launcherAbilityManager === undefined) {

      this.launcherAbilityManager = new LauncherAbilityManager(context)

    }

    return this.launcherAbilityManager

  }



  /**

   * get all app List info from BMS

   *

   * @return 应用的入口Ability信息列表

   */

  async getLauncherAbilityList(): Promise<AppItemInfo[]> {

    Logger.info(TAG, 'getLauncherAbilityList begin')

    let abilityList = await launcherBundleManager.getAllLauncherAbilityInfo(this.mUserId)

    const appItemInfoList = new Array<AppItemInfo>()

    if (CheckEmptyUtils.isEmpty(abilityList)) {

      Logger.info(TAG, 'getLauncherAbilityList Empty')

      return appItemInfoList

    }

    for (let i = 0; i < abilityList.length; i++) {

      let appItem = await this.transToAppItemInfo(abilityList[i])

      appItemInfoList.push(appItem)

    }

    return appItemInfoList

  }



  /**

   * get AppItemInfo from BMS with bundleName

   * @params bundleName

   * @return AppItemInfo

   */

  async getAppInfoByBundleName(bundleName: string): Promise<AppItemInfo | undefined> {

    let appItemInfo: AppItemInfo | undefined = undefined

    // get from cache

    if (this.mAppMap != null && this.mAppMap.has(bundleName)) {

      appItemInfo = this.mAppMap.get(bundleName)

    }

    if (appItemInfo != undefined) {

      Logger.info(TAG, `getAppInfoByBundleName from cache: ${JSON.stringify(appItemInfo)}`)

      return appItemInfo

    }

    // get from system

    let abilityInfos = await launcherBundleManager.getLauncherAbilityInfo(bundleName, this.mUserId)

    if (abilityInfos == undefined || abilityInfos.length == 0) {

      Logger.info(TAG, `${bundleName} has no launcher ability`)

      return undefined

    }

    let appInfo = abilityInfos[0]

    const data = await this.transToAppItemInfo(appInfo)

    Logger.info(TAG, `getAppInfoByBundleName from BMS: ${JSON.stringify(data)}`)

    return data

  }



  private async transToAppItemInfo(info): Promise<AppItemInfo> {

    const appItemInfo = new AppItemInfo()

    appItemInfo.appName = await this.mResourceManager.getAppNameSync(

    info.labelId, info.elementName.bundleName, info.applicationInfo.label

    )

    appItemInfo.isSystemApp = info.applicationInfo.systemApp

    appItemInfo.isUninstallAble = info.applicationInfo.removable

    appItemInfo.appIconId = info.iconId

    appItemInfo.appLabelId = info.labelId

    appItemInfo.bundleName = info.elementName.bundleName

    appItemInfo.abilityName = info.elementName.abilityName

    await this.mResourceManager.updateIconCache(appItemInfo.appIconId, appItemInfo.bundleName)

    this.mAppMap.set(appItemInfo.bundleName, appItemInfo)

    return appItemInfo

  }





  /**

   * 启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbility(paramAbilityName, paramBundleName) {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`)

    this.context.startAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success')

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`)

    })

  }



  /**

   * 通过桌面图标启动应用

   *

   * @params paramAbilityName Ability名

   * @params paramBundleName 应用包名

   */

  startLauncherAbilityFromRecent(paramAbilityName, paramBundleName): void {

    Logger.info(TAG, `startApplication abilityName: ${paramAbilityName}, bundleName: ${paramBundleName}`);

    this.context.startRecentAbility({

      bundleName: paramBundleName,

      abilityName: paramAbilityName

    }).then(() => {

      Logger.info(TAG, 'startApplication promise success');

    }, (err) => {

      Logger.error(TAG, `startApplication promise error: ${JSON.stringify(err)}`);

    });

  }



  /**

   * 卸载应用

   *

   * @params bundleName 应用包名

   * @params callback 卸载回调

   */

  async uninstallLauncherAbility(bundleName: string, callback): Promise<void> {

    Logger.info(TAG, `uninstallLauncherAbility bundleName: ${bundleName}`);

    const bundlerInstaller = await installer.getBundleInstaller();

    bundlerInstaller.uninstall(bundleName, {

      userId: this.mUserId,

      installFlag: 0,

      isKeepData: false

    }, (err: BusinessError) => {

      Logger.info(TAG, `uninstallLauncherAbility result => ${JSON.stringify(err)}`);

      callback(err);

    })

  }



  /**

   * 开始监听系统应用状态.

   *

   * @params listener 监听对象

   */

  registerLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback add bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_ADDED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

        })

        bundleMonitor.on(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE, (bundleChangeInfo) => {

          Logger.debug(TAG, `mBundleStatusCallback remove bundleName: ${bundleChangeInfo.bundleName},

            userId: ${bundleChangeInfo.userId}, mUserId ${this.mUserId}`)

          if (this.mUserId === bundleChangeInfo.userId) {

            this.notifyLauncherAbilityChange(EventConstants.EVENT_PACKAGE_REMOVED,

              bundleChangeInfo.bundleName, bundleChangeInfo.userId)

          }

          AppStorage.Set('isRefresh', true)

        })

      }

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index == CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.push(listener)

      }

    }

  }



  /**

   * 取消监听系统应用状态.

   *

   * @params listener 监听对象

   */

  unregisterLauncherAbilityChangeListener(listener: any): void {

    if (!CheckEmptyUtils.isEmpty(listener)) {

      const index = this.mLauncherAbilityChangeListeners.indexOf(listener)

      if (index != CommonConstants.INVALID_VALUE) {

        this.mLauncherAbilityChangeListeners.splice(index, 1)

      }

      if (this.mLauncherAbilityChangeListeners.length == 0) {

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_ADD)

        bundleMonitor.off(LauncherAbilityManager.BUNDLE_STATUS_CHANGE_KEY_REMOVE)

      }

    }

  }



  private notifyLauncherAbilityChange(event: string, bundleName: string, userId: number): void {

    for (let index = 0; index < this.mLauncherAbilityChangeListeners.length; index++) {

      this.mLauncherAbilityChangeListeners[index](event, bundleName, userId)

    }

  }

}

接口参考:[@ohos.app.ability.ServiceExtensionAbility]

鸿蒙开发岗位需要掌握那些核心要领?

目前还有很多小伙伴不知道要学习哪些鸿蒙技术?不知道重点掌握哪些?为了避免学习时频繁踩坑,最终浪费大量时间的。

自己学习时必须要有一份实用的鸿蒙(Harmony NEXT)资料非常有必要。 这里我推荐,根据鸿蒙开发官网梳理与华为内部人员的分享总结出的开发文档。内容包含了:【ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战】等技术知识点。

废话就不多说了,接下来好好看下这份资料。

如果你是一名Android、Java、前端等等开发人员,想要转入鸿蒙方向发展。可以直接领取这份资料辅助你的学习鸿蒙OpenHarmony知识←前往。下面是鸿蒙开发的学习路线图。

针对鸿蒙成长路线打造的鸿蒙学习文档。鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,帮助大家在技术的道路上更进一步。

其中内容包含:

《鸿蒙开发基础》鸿蒙OpenHarmony知识←前往

  1. ArkTS语言
  2. 安装DevEco Studio
  3. 运用你的第一个ArkTS应用
  4. ArkUI声明式UI开发
  5. .……

《鸿蒙开发进阶》鸿蒙OpenHarmony知识←前往

  1. Stage模型入门
  2. 网络管理
  3. 数据管理
  4. 电话服务
  5. 分布式应用开发
  6. 通知与窗口管理
  7. 多媒体技术
  8. 安全技能
  9. 任务管理
  10. WebGL
  11. 国际化开发
  12. 应用测试
  13. DFX面向未来设计
  14. 鸿蒙系统移植和裁剪定制
  15. ……

《鸿蒙开发实战》鸿蒙OpenHarmony知识←前往

  1. ArkTS实践
  2. UIAbility应用
  3. 网络案例
  4. ……

最后

鸿蒙是完全具备无与伦比的机遇和潜力的;预计到年底将有 5,000 款的应用完成原生鸿蒙开发,这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行!

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-23 01:42:04       17 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-23 01:42:04       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-23 01:42:04       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-23 01:42:04       18 阅读

热门阅读

  1. xml开发mybatis

    2024-04-23 01:42:04       12 阅读
  2. es 深入了解和索引生命周期管理

    2024-04-23 01:42:04       14 阅读
  3. 【UnityShader预备知识】内置变量和函数

    2024-04-23 01:42:04       15 阅读
  4. 自然语言处理(Natural Language Processing, NLP)简介

    2024-04-23 01:42:04       13 阅读
  5. 国产人工智能语言大模型相关网站

    2024-04-23 01:42:04       12 阅读
  6. C++ STL 概述

    2024-04-23 01:42:04       9 阅读
  7. Linux bridge forwarding table

    2024-04-23 01:42:04       12 阅读
  8. 2024-04-22(AJAX)

    2024-04-23 01:42:04       12 阅读
  9. Ubuntu22.04.4 - 安装后使用笔记目录-VMware

    2024-04-23 01:42:04       13 阅读
  10. 基于Python对豆瓣电影数据爬虫的设计与实现

    2024-04-23 01:42:04       11 阅读