HarmonyOS NEXT 实战开发:实现日常提醒应用

为什么要开发这个日常提醒应用?

  • 最近鸿蒙热度一直不减,而且前端的就业环境越来越差,所以心里面萌生了换一个赛道的想法。
  • HarmonyOS NEXT 是华为打造的国产之光,而且是纯血版不再是套壳,更加激起了我的好奇心。
  • ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。所以对于我们前端开发来说非常友好。
  • HarmonyOS NEXT 文档也比较齐全。而且官方也有相关示例极大的方便了开发。
  • 根据文档以及自己之前开发经验做一个日常提醒demo 加深自己对HarmonyOS NEXT的理解和实际应用。

日常提醒主要包含功能有哪些?

  • 首页和个人中心tab页切换,以及tab 底部自定义实现。

  • 封装公共弹窗日期选择、列表选择等组件。

  • 访问本地用户首选项preferences实现数据本地化持久存储。

  • 实现后台任务reminderAgentManager提醒。

  • 提醒列表展示,以及删除等。

  • 新增编辑日常提醒记录。

1.实现首页自定义tab页切换

主要依据tab组件以及tab 组件的BottomTabBarStyle的构造函数。

首页page/MinePage.ets代码如下

复制代码
import {HomeTabs} from  "./components/TabHome"
import {UserBaseInfo} from  "./components/UserBaseInfo"


@Entry
@Component
struct MinePage {
  @State currentIndex: number = 0

  // 构造类 自定义底部切换按钮
  @Builder TabBuilder(index: number,icon:Resource,selectedIcon:Resource,name:string) {
    Column() {
      Image(this.currentIndex === index ? selectedIcon : icon)
        .width(24)
        .height(24)
        .margin({ bottom: 4 })
        .objectFit(ImageFit.Contain)
      Text(`${name}`)
        .fontColor(this.currentIndex === index ? '#007DFF' : '#000000')
        .fontSize('14vp')
        .fontWeight(500)
        .lineHeight(14)
    }.width('100%').height('100%')
    .backgroundColor('#ffffff')
  }
  build() {

    Column() {
      Tabs({ barPosition: BarPosition.End }) {
        TabContent() {
          HomeTabs(); //首页
        }.tabBar(this.TabBuilder(0,$r('app.media.ic_home'),$r('app.media.ic_home_selected'),'首页'))

        TabContent() {
          UserBaseInfo()//个人中心
        }.tabBar(this.TabBuilder(1,$r('app.media.ic_mine'),$r('app.media.ic_mine_selected'),'我的'))
      }
      .vertical(false)
      .scrollable(true)
      .barMode(BarMode.Fixed)
      .onChange((index: number) => {
        this.currentIndex = index;
      })
      .width('100%')
    }
    .width('100%')
    .height('100%')

    .backgroundColor('#f7f7f7')
  }
}

2.封装公共弹窗组件

在ets/common/utils 目录下新建 CommonUtils.ets 文件

复制代码
import CommonConstants from '../constants/CommonConstants';

/**
 * This is a pop-up window tool class, which is used to encapsulate dialog code.
 * Developers can directly invoke the methods in.
 */
export class CommonUtils {
  /**
   * 确认取消弹窗
   */
  alertDialog(content:{message:string},Callback: Function) {
    AlertDialog.show({
      message: content.message,
      alignment: DialogAlignment.Bottom,
      offset: {
        dx: 0,
        dy: CommonConstants.DY_OFFSET
      },
      primaryButton: {
        value: '取消',
        action: () => {
          Callback({
            type:1
          })
        }
      },
      secondaryButton: {
        value: '确认',
        action: () => {
          Callback({
            type:2
          })
        }
      }
    });
  }


  /**
   * 日期选择
   */
  datePickerDialog(dateCallback: Function) {
    DatePickerDialog.show({
      start: new Date(),
      end: new Date(CommonConstants.END_TIME),
      selected: new Date(CommonConstants.SELECT_TIME),
      lunar: false,
      onAccept: (value: DatePickerResult) => {
        let year: number = Number(value.year);
        let month: number = Number(value.month) + CommonConstants.PLUS_ONE;
        let day: number = Number(value.day);
        let birthdate: string = `${year}-${this.padZero(month)}-${this.padZero(day)}`
        dateCallback(birthdate,[year, month, day]);
      }
    });
  }

  /**
   * 时间选择
   */
  timePickerDialog(dateCallback: Function) {
    TimePickerDialog.show({
      selected:new Date(CommonConstants.SELECT_TIME),
      useMilitaryTime: true,
      onAccept: (value: TimePickerResult) => {
        let hour: number = Number(value.hour);
        let minute: number = Number(value.minute);
        let time: string =`${this.padZero(hour)}:${this.padZero(minute)}`
        dateCallback(time,[hour, minute]);
      }
    });
  }
  padZero(value:number):number|string {
    return value < 10 ? `0${value}` : value;
  }

  /**
   * 文本选择
   */
  textPickerDialog(sexArray?: string[], sexCallback?: Function) {
    if (this.isEmpty(sexArray)) {
      return;
    }
    TextPickerDialog.show({
      range: sexArray,
      selected: 0,
      onAccept: (result: TextPickerResult) => {
        sexCallback(result.value);
      },
      onCancel: () => {
      }
    });
  }


  /**
   * Check obj is empty
   *
   * @param {object} obj
   * @return {boolean} true(empty)
   */
  isEmpty(obj: object | string): boolean {
    return obj === undefined || obj === null || obj === '';
  }

}

export default new CommonUtils();

3.封装本地持久化数据preferences操作

在ets/model/database 新建文件 PreferencesHandler.ets

复制代码
import data_preferences from '@ohos.data.preferences';
import CommonConstants from '../../common/constants/CommonConstants';
import PreferencesListener from './PreferencesListener';

/**
 * Based on lightweight databases preferences handler.
 */
export default class PreferencesHandler {
  static instance: PreferencesHandler = new PreferencesHandler();
  private preferences: data_preferences.Preferences | null = null;
  private defaultValue = '';
  private listeners: PreferencesListener[];

  private constructor() {
    this.listeners = new Array();
  }

  /**
   * Configure PreferencesHandler.
   *
   * @param context Context
   */
  public async configure(context: Context) {
    this.preferences = await data_preferences.getPreferences(context, CommonConstants.PREFERENCE_ID);
    this.preferences.on('change', (data: Record<string, Object>) => {
      for (let preferencesListener of this.listeners) {
        preferencesListener.onDataChanged(data.key as string);
      }
    });
  }

  /**
   * Set data in PreferencesHandler.
   *
   * @param key string
   * @param value any
   */
  public async set(key: string, value: string) {
    if (this.preferences != null) {
      await this.preferences.put(key, value);
      await this.preferences.flush();
    }
  }

  /**
   * 获取数据
   *
   * @param key string
   * @param defValue any
   * @return data about key
   */
  public async get(key: string) {
    let data: string = '';
    if (this.preferences != null) {
      data = await this.preferences.get(key, this.defaultValue) as string;
    }
    return data;
  }

  /**
   * 删除数据
   *
   * @param key string
   * @param defValue any
   * @return data about key
   */
  public async delete(key: string) {
    if (this.preferences != null) {
       await this.preferences.delete(key);
    }
  }

  /**
   * Clear data in PreferencesHandler.
   */
  public clear() {
    if (this.preferences != null) {
      this.preferences.clear();
    }
  }

  /**
   * Add preferences listener in PreferencesHandler.
   *
   * @param listener PreferencesListener
   */
  public addPreferencesListener(listener: PreferencesListener) {
    this.listeners.push(listener);
  }
}

4.封装代理提醒reminderAgentManager

在ets/model 目录下新建 ReminderService.ets

复制代码
/*
 * 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 reminderAgent from '@ohos.reminderAgentManager';
import notification from '@ohos.notificationManager';
import ReminderItem from '../viewmodel/ReminderItem';

/**
 * Base on ohos reminder agent service
 */
export default class ReminderService {
  /**
   * 打开弹窗
   */
  public openNotificationPermission() {
    notification.requestEnableNotification().then(() => {
    }).catch((err: Error) => {
    });
  }

  /**
   * 发布相应的提醒代理
   *
   * @param alarmItem ReminderItem
   * @param callback callback
   */
  public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) {
    let reminder = this.initReminder(alarmItem);
    reminderAgent.publishReminder(reminder, (err, reminderId: number) => {
      if (callback != null) {
        callback(reminderId);
      }
    });
  }

  /**
   * 根据需要删除提醒任务。
   *
   * @param reminderId number
   */
  public deleteReminder(reminderId: number) {
    reminderAgent.cancelReminder(reminderId);
  }

  private initReminder(item: ReminderItem):  reminderAgent.ReminderRequestCalendar {
    return {
      reminderType: reminderAgent.ReminderType.REMINDER_TYPE_CALENDAR,
      title: item.title,
      content: item.content,
      dateTime: item.dateTime,
      repeatDays: item.repeatDays,
      ringDuration: item.ringDuration,
      snoozeTimes: item.snoozeTimes,
      timeInterval: item.timeInterval,
      actionButton: [
        {
          title: '关闭',
          type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE,
        },
        {
          title: '稍后提醒',
          type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE
        },
      ],
      wantAgent: {
        pkgName: 'com.example.wuyandeduihua2',// 点击提醒通知后跳转的目标UIAbility信息
        abilityName: 'EntryAbility'
      },
      maxScreenWantAgent: { // 全屏显示提醒到达时自动拉起的目标UIAbility信息
        pkgName: 'com.example.wuyandeduihua2',
        abilityName: 'EntryAbility'
      },
      notificationId: item.notificationId,
      expiredContent: '消息已过期',
      snoozeContent: '确定要延迟提醒嘛',
      slotType: notification.SlotType.SERVICE_INFORMATION
    }
  }
}

5.新增编辑提醒页面

新增编辑AddNeedPage.ets页面 代码如下

复制代码
import router from '@ohos.router';

import  {FormList,FormItemType,formDataType} from  '../viewmodel/AddNeedModel'
import CommonUtils from '../common/utils/CommonUtils';
import addModel from '../viewmodel/AddNeedModel';

@Entry
@Component
struct AddNeedPage {
  @State formData:formDataType={
     id:0,
     title:"",
     content:"",
     remindDay:[], //日期
     remindDay_text:"",
     remindTime:[],//时间
     remindTime_text:"",
     ringDuration:0,
     ringDuration_text:"", //提醒时长
     snoozeTimes:0,
     snoozeTimes_text:"", //延迟提醒次数
     timeInterval:0,
     timeInterval_text:"", //延迟提醒间隔
  }
  private viewModel: addModel = addModel.instant;

  aboutToAppear() {
    let params = router.getParams() as Record<string, Object|undefined>;
    if (params !== undefined) {
      let alarmItem: formDataType = params.alarmItem as formDataType;
      if (alarmItem !== undefined) {
        this.formData =  {...alarmItem}
      }
    }
  }

  build() {
    Column() {
      Column(){
        ForEach(FormList,(item:FormItemType)=>{
          Row(){
            Row(){
              Text(item.title)
            }.width('35%')
            Row(){
              TextInput({ text: item.type.includes('Picker')?this.formData[`${item.key}_text`]: this.formData[item.key],placeholder: item.placeholder })
                .borderRadius(0)
                .enabled(item.isPicker?false:true) //禁用
                .backgroundColor('#ffffff')
                .onChange((value: string) => {
                  if(!item.type.includes('Picker')){
                    this.formData[item.key] = value;
                  }
                })
              Image($r('app.media.ic_arrow'))
                .visibility(item.isPicker?Visibility.Visible:Visibility.Hidden)
                .width($r('app.float.arrow_image_width'))
                .height($r('app.float.arrow_image_height'))
                .margin({ right: $r('app.float.arrow_right_distance') })

            }.width('65%').padding({right:15})
            .onClick(()=>{
              if(item.isPicker){
                switch (item.type) {
                  case 'datePicker':
                    CommonUtils.datePickerDialog((value: string,timeArray:string[]) => {
                      this.formData[`${item.key}_text`] = value;
                      this.formData[item.key] = timeArray;
                    });
                    break;
                  case 'timePicker':
                    CommonUtils.timePickerDialog((value: string,timeArray:string[]) => {
                      this.formData[`${item.key}_text`] = value;
                      this.formData[item.key] = timeArray;
                    });
                    break;
                  case 'TextPicker':
                    CommonUtils.textPickerDialog(item.dicData, (value: string) => {
                      this.formData[`${item.key}_text`] = value;
                      this.formData[`${item.key}`] =item.dicMap[value];
                    });
                    break;

                  default:
                    break;
                }
              }
            })
          }.width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .backgroundColor('#ffffff')
          .padding(10)
          .borderWidth({
            bottom:1
          })
          .borderColor('#f7f7f7')
        })

        Button('提交',{ type: ButtonType.Normal, stateEffect: true })
          .fontSize(18)
          .width('90%')
          .height(40)
          .borderRadius(15)
          .margin({ top:45 })
          .onClick(()=>{
            this.viewModel.setAlarmRemind(this.formData);
            router.back();
          })
      }
    }.width('100%')
    .height('100%')
    .backgroundColor('#f7f7f7')
  }
}

AddNeedModel.ets页面代码如下

复制代码
import CommonConstants from '../common/constants/CommonConstants';
import ReminderService from '../model/ReminderService';
import DataTypeUtils from '../common/utils/DataTypeUtils';
import { GlobalContext } from '../common/utils/GlobalContext';
import PreferencesHandler from '../model/database/PreferencesHandler';

/**
 * Detail page view model description
 */
export default class DetailViewModel {
  static instant: DetailViewModel = new DetailViewModel();
  private reminderService: ReminderService;
  private alarms: Array<formDataType>;

  private constructor() {
    this.reminderService = new ReminderService();
    this.alarms = new Array<formDataType>();
  }

  /**
   * 设置提醒
   *
   * @param alarmItem AlarmItem
   */
  public async setAlarmRemind(alarmItem: formDataType) {

    let index = await this.findAlarmWithId(alarmItem.id);
    if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) {
      this.reminderService.deleteReminder(alarmItem.id);
    } else {
      index = this.alarms.length;
      alarmItem.notificationId = index;
      this.alarms.push(alarmItem);
    }
    alarmItem.dateTime={
      year: alarmItem.remindDay[0],
      month: alarmItem.remindDay[1],
      day: alarmItem.remindDay[2],
      hour: alarmItem.remindTime[0],
      minute: alarmItem.remindTime[1],
      second: 0
    }
    // @ts-ignore
    this.reminderService.addReminder(alarmItem, (newId: number) => {
      alarmItem.id = newId;
      this.alarms[index] = alarmItem;
      let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;
      preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms));

    })
  }

  /**
   * 删除提醒
   *
   * @param id number
   */
  public async removeAlarmRemind(id: number) {
    this.reminderService.deleteReminder(id);
    let index = await this.findAlarmWithId(id);
    if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) {
      this.alarms.splice(index, CommonConstants.DEFAULT_SINGLE);
    }
    let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;
    preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms));
  }

  private async findAlarmWithId(id: number) {
    let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;
    let data = await preference.get(CommonConstants.ALARM_KEY);
    if (!DataTypeUtils.isNull(data)) {
      this.alarms = JSON.parse(data);
      for (let i = 0;i < this.alarms.length; i++) {
        if (this.alarms[i].id === id) {
          return i;
        }
      }
    }
    return CommonConstants.DEFAULT_NUMBER_NEGATIVE;
  }
}


export  interface  FormItemType{
  title:string;
  placeholder:string;
  type:string;
  key:string;
  isPicker:boolean;
  dicData?:string[]
  dicMap?:object
}


export  interface  formDataType{
  id:number;
  notificationId?:number;
  title:string;
  content:string;
  remindDay:number[];
  remindDay_text:string;
  remindTime:number[];
  remindTime_text:string;
  ringDuration:number;
  ringDuration_text:string;
  snoozeTimes:number;
  snoozeTimes_text:string;
  timeInterval:number;
  timeInterval_text:string;
  dateTime?:Object

}


export const FormList: Array<FormItemType> = [
  {
    title:"事项名称",
    placeholder:"请输入",
    key:"title",
    isPicker:false,
    type:"text"
  },
  {
    title:"事项描述",
    placeholder:"请输入",
    key:"content",
    isPicker:false,
    type:"text"
  },
  {
    title:"提醒日期",
    placeholder:"请选择",
    key:"remindDay",
    isPicker:true,
    type:"datePicker"
  },
  {
    title:"提醒时间",
    placeholder:"请选择",
    key:"remindTime",
    isPicker:true,
    type:"timePicker"
  },
  {
    title:"提醒时长",
    placeholder:"请选择",
    key:"ringDuration",
    isPicker:true,
    type:"TextPicker",
    dicData:['30秒','1分钟','5分钟'],
    dicMap:{
      '30秒':30,
      '1分钟':60,
      '5分钟':60*5,
    }
  },
  {
    title:"延迟提醒次数",
    placeholder:"请选择",
    key:"snoozeTimes",
    isPicker:true,
    type:"TextPicker",
    dicData:['1次','2次','3次','4次','5次','6次'],
    dicMap:{
      '1次':1,
      '2次':2,
      '3次':3,
      '4次':4,
      '5次':5,
      '6次':6,
    }
  },
  {
    title:"延迟提醒间隔",
    placeholder:"请选择",
    key:"timeInterval",
    isPicker:true,
    type:"TextPicker",
    dicData:['5分钟','10分钟','15分钟','30分钟'],
    dicMap:{
      '5分钟':5*60,
      '10分钟':10*60,
      '15分钟':15*60,
      '30分钟':15*60,
    }
  }
]

注意事项

  • 本项目用到了代理通知需要在module.json5 文件中 requestPermissions 中声明权限

    {
    "module": {
    "name": "entry",
    "type": "entry",
    "description": "string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ "phone", "tablet" ], "deliveryWithInstall": true, "installationFree": false, "pages": "profile:main_pages",
    "abilities": [
    {
    "name": "EntryAbility",
    "srcEntry": "./ets/entryability/EntryAbility.ets",
    "description": "string:ability_desc", "icon": "media:icon",
    "label": "string:ability_label", "startWindowIcon": "media:icon",
    "startWindowBackground": "color:start_window_background", "exported": true, "skills": [ { "entities": [ "entity.system.home" ], "actions": [ "action.system.home" ] } ] } ], "requestPermissions": [ { "name": "ohos.permission.PUBLISH_AGENT_REMINDER", "reason": "string:reason",
    "usedScene": {
    "abilities": [
    "EntryAbility"
    ],
    "when": "always"
    }
    }
    ]
    }
    }

  • 本项目还用到了 应用上下文Context在入口文件EntryAbility.ets中注册

    import type AbilityConstant from '@ohos.app.ability.AbilityConstant';
    import display from '@ohos.display';
    import hilog from '@ohos.hilog';
    import UIAbility from '@ohos.app.ability.UIAbility';
    import type Want from '@ohos.app.ability.Want';
    import type window from '@ohos.window';
    import PreferencesHandler from '../model/database/PreferencesHandler';
    import { GlobalContext } from '../common/utils/GlobalContext';
    /**

    • Lift cycle management of Ability.
      */
      export default class EntryAbility extends UIAbility {
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
      GlobalContext.getContext().setObject('preference', PreferencesHandler.instance);
      }
    复制代码
    onDestroy(): void {
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
    }
    
    async onWindowStageCreate(windowStage: window.WindowStage) {
      // Main window is created, set main page for this ability
      let globalDisplay: display.Display = display.getDefaultDisplaySync();
      GlobalContext.getContext().setObject('globalDisplay', globalDisplay);
      let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler;
      await preference.configure(this.context.getApplicationContext());
    
      // Main window is created, set main page for this ability
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    
      windowStage.loadContent("pages/MinePage", (err, data) => {
        if (err.code) {
          hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
          return;
        }
        hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
      });
    }
    
    onWindowStageDestroy(): void {
      // Main window is destroyed, release UI related resources
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
    }
    
    onForeground(): void {
      // Ability has brought to foreground
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
    }
    
    onBackground(): void {
      // Ability has back to background
      hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
    }

    }

以上,总结了应用程序的主要代码内容。相关代码我把他放在了github上有需要的小伙伴自己下载

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:


鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员: 想要拓展职业边界
零基础小白: 鸿蒙爱好者,希望从0到1学习,增加一项技能。
**技术提升/进阶跳槽:**发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频学习教程+学习PDF文档

HarmonyOS Next 最新全套视频教程

纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

相关推荐
Cobyte1 分钟前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT0614 分钟前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
剪刀石头布啊20 分钟前
生成随机数,Math.random的使用
前端
剪刀石头布啊21 分钟前
css外边距重叠问题
前端
剪刀石头布啊21 分钟前
chrome单页签内存分配上限问题,怎么解决
前端
剪刀石头布啊23 分钟前
css实现一个宽高固定百分比的布局的一个方式
前端
Fcy64825 分钟前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满26 分钟前
Linux怎么查看最新下载的文件
linux·运维·服务器
剪刀石头布啊27 分钟前
js数组之快速组、慢数组、密集数组、稀松数组
前端
代码游侠1 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法