HarmonyOS 实现下拉刷新,上拉加载更多

news/发布时间2024/5/20 13:58:20

组件介绍

PullToRefreshList允许用户通过下拉动作来刷新列表内容,以及通过上拉动作来加载更多的数据。组件内部封装了滚动监听、状态管理和动画效果,使得开发者可以轻松集成到自己的项目中。

1. 实现思路

  • 封装成可复用的公共控件:将下拉刷新和上拉加载更多功能封装为一个可复用的组件,便于在不同列表场景中应用。
  • 状态管理:使用状态变量来管理刷新状态(如refreshingloadingMoreIng)、列表是否滑动到顶部(isAtTopOfList)等。
  • 事件回调:通过定义回调函数(如onRefreshonLoadMoreonStatusChanged)来处理刷新和加载逻辑,并与外部逻辑解耦。
  • 手势识别:利用触摸事件(TouchType.DownTouchType.MoveTouchType.Up)来识别用户的下拉的手势。
  • 动画效果:通过控制滚动偏移量和刷新头部的显示与隐藏,实现平滑的下拉刷新动画效果。

3. 下拉刷新实现原理

下拉刷新的实现依赖于对触摸事件的监听和处理:

  • 触摸按下(TouchType.Down:记录用户触摸按下时的坐标,用于后续的滑动判断。
  • 触摸移动(TouchType.Move:在用户移动手指时,计算移动的距离,并根据列表的当前滚动位置和偏移量来判断是否触发下拉刷新的动作。若用户在列表顶部下拉,则通过更新offsetY来控制刷新头部的显示和隐藏,同时更新刷新状态。
  • 触摸抬起或取消(TouchType.UpTouchType.Cancel:在用户完成下拉动作后,根据偏移量判断是否满足刷新条件。如果满足,则触发刷新逻辑,并通过回调函数onRefresh通知外部进行数据刷新。如果不满足,则恢复列表到未拖动状态。

4. 加载更多实现原理

  • 滚动到底部:使用onReachEnd事件监听器来检测用户是否滚动到列表底部。当触发此事件时,如果loadingMoreIngfalse,表示当前没有在加载更多数据,则将其设置为true,开始加载更多数据。
  • 加载逻辑:调用onLoadMore回调函数来执行加载更多的逻辑。在加载数据的过程中,可以更新列表项的布局以显示加载提示文本,如“努力加载中...”。
  • 数据更新:加载完成后,更新数据源dataSet,并重置loadingMoreIngfalse,表示加载更多操作已完成。

PullToRefreshList完整代码:

import { Constant } from '../constant/Constant';@Preview
@Component
export struct PullToRefreshList {// 通过BuilderParam装饰器声明的函数参数,用于自定义列表项的布局@BuilderParamitemLayout?: (item: Object, index: number) => void;// 通过Watch和Link装饰器监听刷新状态的变化,并双向绑定刷新标志@Watch("notifyRefreshingChanged")@Link refreshing: boolean;// 通过Link装饰器双向绑定,表示是否正在加载更多数据@LinkloadingMoreIng: boolean;// 状态变量,表示列表是否滑动到顶部@StateisAtTopOfList: boolean = false;// 通过Link装饰器双向绑定,表示列表的数据源@Link dataSet: Array<Object>;// 定义回调函数,用于处理刷新和加载更多事件onRefresh?: () => void;onLoadMore?: () => void;// 定义回调函数,用于处理刷新状态变化事件onStatusChanged?: (status: RefreshStatus) => void;// 私有成员变量,定义下拉刷新头部的高度private headHeight: number = 55;// 私有成员变量,用于记录触摸事件的坐标private lastX: number = 0;private lastY: number = 0;private downY: number = 0;// 私有成员变量,用于下拉刷新时的动画效果和手势识别private flingFactor: number = 0.75;private touchSlop: number = 2;private offsetStep: number = 10;private intervalTime: number = 20;// 私有成员变量,控制列表是否可以滚动private listScrollable: boolean = true;// 私有成员变量,标识是否正在拖动列表private dragging: boolean = false;// 私有成员变量,当前刷新状态private refreshStatus: RefreshStatus = RefreshStatus.Inactive;// 通过Watch和State装饰器监听并状态绑定,表示下拉刷新头部的偏移量@Watch("notifyOffsetYChanged")@State offsetY: number = -this.headHeight;// 状态变量,刷新头部图标资源@State refreshHeadIcon: Resource = $r("app.media.icon_refresh_down");// 状态变量,刷新头部提示文本@State refreshHeadText: string = Constant.REFRESH_PULL_TO_REFRESH;// 状态变量,刷新内容区域的高度@State refreshContentH: number = 0;// 状态变量,控制组件是否可以接收触摸事件@State touchEnabled: boolean = true;// 状态变量,控制刷新头部的可见性@State headerVisibility: Visibility = Visibility.None;// 私有成员变量,滚动器对象,用于控制滚动行为private listScroller: Scroller = new Scroller();/*** 当刷新状态变化时调用的方法*/private notifyRefreshingChanged() {// 根据刷新标志显示刷新状态或完成刷新if (this.refreshing) {this.showRefreshingStatus();} else {this.finishRefresh();}}/*** 当下拉刷新头部偏移量变化时调用的方法*/private notifyOffsetYChanged() {// 根据偏移量设置刷新头部的可见性this.headerVisibility = (this.offsetY == -this.headHeight) ? Visibility.None : Visibility.Visible;}/*** 构建刷新头部组件的逻辑*/@BuilderRefreshHead() {// 使用Row布局来水平排列图标和文本Row() {Blank()Image(this.refreshHeadIcon).width(30).aspectRatio(1) // 保持图片宽高比.objectFit(ImageFit.Contain) // 保持图片内容完整Text(this.refreshHeadText).fontSize(16).width(150).textAlign(TextAlign.Center)Blank()}.width("100%").height(this.headHeight).backgroundColor("#44bbccaa") // 设置背景颜色.visibility(this.headerVisibility) // 根据状态设置可见性.position({ // 设置位置x: 0,y: this.offsetY})}/*** 构建刷新内容组件的逻辑*/@BuilderRefreshContent() {List({ scroller: this.listScroller }) { // 使用List组件创建滚动列表,并传入滚动器if (this.dataSet) { // 判断数据源是否存在ForEach(this.dataSet, (item: Object, index: number) => { // 遍历数据源并为每项创建列表项ListItem() {if (this.itemLayout) { // 如果提供了列表项布局函数,则使用它this.itemLayout(item, index)}}.width("100%"); // 设置列表项宽度}, (item: Object, index: number) => item.toString()) // 为列表项提供唯一的标识符// 上拉加载更多的UI提示ListItem() {if (this.loadingMoreIng === false) {Text('努力加载中...') // 显示加载文本.width('100%') // 设置文本宽度.height(100) // 设置文本高度.fontSize(16) // 设置字体大小.textAlign(TextAlign.Center) // 设置文本居中对齐.backgroundColor(0xDCDCDC); // 设置背景颜色}}}}.width("100%") // 设置列表宽度.height("100%") // 设置列表高度.edgeEffect(EdgeEffect.None) // 设置无边缘效果.onScrollFrameBegin((offset: number, state: ScrollState) => { // 监听滚动事件offset = this.listScrollable ? offset : 0; // 根据滚动状态决定是否允许滚动return { offsetRemain: offset }}).onReachEnd(() => { // 监听滚动到列表底部的事件if (!this.loadingMoreIng) { // 如果不是正在加载更多this.loadingMoreIng = true // 设置为正在加载状态this.onLoadMore() // 调用加载更多的回调函数this.loadingMoreIng = false // 完成加载,设置为非加载状态}}).onScrollIndex((start: number, end: number) => { // 监听滚动索引变化事件this.logD("onScrollIndex() start = " + start + ",end = " + end) // 打印滚动索引// 根据滚动索引判断列表是否滑动到顶部if (start == 0) {this.isAtTopOfList = true} else {this.isAtTopOfList = false}})}// 定义组件的构建逻辑,使用 Column 组件作为根布局build() {// 创建一个 Column 组件实例,作为整个下拉刷新列表的容器Column() {// 调用 RefreshHead 方法,添加下拉刷新的头部组件this.RefreshHead()// 创建另一个 Column 组件实例,作为内容区域的容器Column() {// 调用 RefreshContent 方法,添加可滚动的内容区域// 这个内容区域包含了列表数据的展示this.RefreshContent()}// 为内容区域的 Column 设置属性.id("refresh_content") // 设置组件的 ID.width("100%") // 设置宽度为100%,占满父容器宽度.layoutWeight(1) // 设置布局权重,影响在剩余空间中的占比.backgroundColor(Color.Pink) // 设置背景颜色为粉红色.position({ // 设置内容区域的起始位置,实现下拉刷新时的上移效果x: 0, // 在水平方向上的位置y: this.offsetY + this.headHeight // 在垂直方向上的位置,根据偏移量和头部高度计算})}// 为整个下拉刷新列表的 Column 设置属性.id("refresh_list") // 设置根容器的 ID.width("100%") // 设置宽度为100%,占满父容器宽度.height("100%") // 设置高度为100%,占满父容器高度.enabled(this.touchEnabled) // 设置是否启用触摸事件,基于 touchEnabled 状态变量.onAreaChange((oldArea, newAre) => { // 监听容器大小变化事件console.log("Refresh height: " + newAre.height); // 打印新的高度值this.refreshContentH = Number(newAre.height); // 更新内容区域的高度状态变量}).clip(true) // 设置是否启用裁剪,超出部分会被隐藏.onTouch((event) => { // 监听触摸事件if (event.touches.length != 1) { // 判断是否为单指触摸console.log("TOUCHES LENGTH INVALID: " + JSON.stringify(event.touches)); // 如果不是单指触摸,则打印错误并阻止事件传播event.stopPropagation(); // 阻止事件继续传播return; // 退出当前事件处理函数}// 根据触摸事件的类型执行相应的处理函数switch (event.type) {case TouchType.Down:this.onTouchDown(event); // 处理触摸按下事件break;case TouchType.Move:this.onTouchMove(event); // 处理触摸移动事件break;case TouchType.Up:case TouchType.Cancel:this.onTouchUp(event); // 处理触摸抬起或取消事件break;}// 在所有触摸事件处理完毕后,阻止事件继续传播event.stopPropagation();})}/*** 设置下拉刷新的状态,并根据状态启用或禁用触摸事件,以及通知刷新状态变化。* @param status 下拉刷新的新状态。*/private setRefreshStatus(status: RefreshStatus) {// 更新当前的刷新状态为传入的参数statusthis.refreshStatus = status;// 根据刷新状态设置refreshing标志,如果状态是Refresh,则设置为true,否则为falsethis.refreshing = (status == RefreshStatus.Refresh);// 根据刷新状态设置touchEnabled,当状态不是Refresh和Done时,允许触摸事件this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);// 通知刷新状态发生了变化,调用onStatusChanged回调函数(如果已设置)this.notifyStatusChanged();}/*** 检查当前滚动状态是否允许触发下拉刷新动作。* @returns {boolean} 如果列表滚动到顶部或已标记为顶部,则返回true,否则返回false。*/private canRefresh() {// 检查滚动器的当前垂直偏移量的y坐标是否等于-headHeight,或者isAtTopOfList标志为true// 如果列表已滚动到顶部(yOffset为-headHeight,即刷新头部的原始隐藏位置),// 或者isAtTopOfList被设置为true(通过滚动事件监听器),则可以进行下拉刷新return this.listScroller.currentOffset().yOffset == -this.headHeight || this.isAtTopOfList;}/*** 处理触摸按下事件的方法。* @param event 触摸事件对象,包含触摸点的相关信息。*/private onTouchDown(event: TouchEvent) {// 从触摸事件对象中获取第一个触摸点的屏幕坐标this.lastX = event.touches[0].screenX;this.lastY = event.touches[0].screenY;// downY用于记录初始按下时的Y坐标,用于后续移动事件中的滑动判断this.downY = this.lastY;}/*** 处理触摸移动事件的方法。* @param event 触摸事件对象,包含触摸点的相关信息。*/private onTouchMove(event: TouchEvent) {// 获取当前触摸点的屏幕坐标let currentX = event.touches[0].screenX;let currentY = event.touches[0].screenY;// 计算自上次触摸移动以来的X、Y方向变化量let deltaX = currentX - this.lastX;let deltaY = currentY - this.lastY;// 如果当前正处于拖动状态if (this.dragging) {// 记录当前偏移量console.log("offsetY: " + this.offsetY.toFixed(2) + ",  head: " + (-this.headHeight));// 判断Y方向的滑动方向if (deltaY < 0) {// 向上拖动if (this.offsetY > -this.headHeight) {// 如果当前偏移量大于-headHeight,表示还在下拉刷新的可拖动范围内console.log("手指向上拖动还未到达临界值,不让 list 滚动")// 更新offsetY,并应用flingFactor作为滑动速度的调整因子this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;// 此时不允许列表滚动this.listScrollable = false;} else {// 如果当前偏移量小于等于-headHeight,表示已经到达临界值,可以开始滚动列表console.log("手指向上拖动到达临界值了,开始让 list 滚动")this.offsetY = -this.headHeight;// 允许列表滚动this.listScrollable = true;// 重置downY为当前的lastY,为下次拖动做准备this.downY = this.lastY;}} else {// 向下拖动console.log("手指向下拖动中 this.canRefresh() = " + this.canRefresh())// 如果可以刷新(即滚动到了列表顶部)if (this.canRefresh()) {// 更新offsetY,并应用flingFactor作为滑动速度的调整因子this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;// 此时不允许列表滚动this.listScrollable = false;} else {// 如果不是在顶部,则允许滚动this.listScrollable = true;}}// 更新lastX和lastY为当前坐标,为下次滑动做准备this.lastX = currentX;this.lastY = currentY;} else {// 如果当前不在拖动状态// 判断是否为向下的滑动,并且滑动的起始点是列表顶部if (Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) > this.touchSlop) {if (deltaY > 0 && this.canRefresh()) {// 设置拖动状态为truethis.dragging = true;// 不允许列表滚动this.listScrollable = false;// 更新lastX和lastY为当前坐标this.lastX = currentX;this.lastY = currentY;console.log("Touch MOVE: 手指向下滑动,达到了拖动条件");}}}// 如果当前正处于拖动状态if (this.dragging) {// 判断当前触摸点的Y坐标是否大于初始按下时的Y坐标if (currentY >= this.downY) {// 根据当前的offsetY值判断刷新头部的显示状态if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {// 如果已经下拉到超过头部高度的80%,则显示松开刷新的提示this.refreshHeadText = Constant.REFRESH_FREE_TO_REFRESH;this.refreshHeadIcon = $r("app.media.icon_refresh_up");// 设置当前刷新状态为OverDrag,即超过拖动区域的状态this.setRefreshStatus(RefreshStatus.OverDrag);} else {// 如果还在下拉过程中,显示下拉刷新的提示this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;this.refreshHeadIcon = $r("app.media.icon_refresh_down");// 设置当前刷新状态为Drag,即拖动状态this.setRefreshStatus(RefreshStatus.Drag);}}}// 可选的控制台日志,打印当前触摸点的坐标和偏移量,可以用于调试// console.log("Touch MOVE: " + event.touches[0].screenX + " x " + event.touches[0].screenY + ", offset: " + this.offsetY);}/*** 处理触摸抬起事件的方法,根据拖动状态和偏移量决定是否触发刷新。* @param event 触摸事件对象,包含触摸点的相关信息。*/private onTouchUp(event: TouchEvent) {// 打印抬起时的X、Y坐标和当前偏移量offsetYconsole.log("Touch UP: " + event.touches[0].screenX.toFixed(2) +" x " + event.touches[0].screenY.toFixed(2) +", offset: " + this.offsetY);// 如果当前状态为拖动状态,处理释放后的逻辑if (this.dragging) {// 判断是否满足下拉刷新的条件:offsetY大于等于0,或者下拉距离已经超过头部高度的80%if (this.offsetY >= 0 || (this.headHeight - Math.abs(this.offsetY)) > this.headHeight * 4 / 5) {// 打印日志,表示用户已经下拉到足够的距离,可以触发刷新console.log("Touch UP: 触发下拉刷新条件");// 更新刷新头部的图标和文本,提示用户正在刷新this.refreshHeadIcon = $r("app.media.icon_refresh_loading");this.refreshHeadText = Constant.REFRESH_REFRESHING;// 设置刷新状态为Refresh,表示正在刷新中this.setRefreshStatus(RefreshStatus.Refresh);// 将列表滚动到顶部,隐藏刷新头部this.scrollToTop();// 通知刷新开始,调用外部提供的onRefresh回调函数this.notifyRefreshStart();} else {// 如果没有达到刷新条件,打印日志说明console.log("Touch UP: 未达到下拉刷新条件");// 更新刷新头部的图标和文本,提示用户下拉以刷新this.refreshHeadIcon = $r("app.media.icon_refresh_down");this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;// 设置刷新状态为Drag,表示用户可以继续拖动this.setRefreshStatus(RefreshStatus.Drag);// 滚动回原位,恢复到未拖动状态this.scrollByTop();}}}/*** 将滚动偏移量设置为0,使得内容区域回到顶部。*/private scrollToTop() {// 设置滚动偏移量为0,滚动到列表顶部this.offsetY = 0;}/*** 滚动内容区域回到初始位置。*/private scrollByTop() {// 如果当前滚动偏移量不等于-headHeight,即没有滚动到顶部if (this.offsetY != -this.headHeight) {// 记录开始滚动时的偏移量this.logD("scrollByTop() start, offsetY: " + this.offsetY.toFixed(2));// 使用setInterval创建一个滚动动画,每隔intervalTime毫秒更新一次偏移量let intervalId = setInterval(() => {// 如果滚动偏移量小于等于-headHeight,即滚动到了顶部if (this.offsetY <= -this.headHeight) {// 重置刷新状态this.resetRefreshStatus();// 清除定时器,停止滚动动画clearInterval(intervalId);// 记录滚动完成时的偏移量this.logD("scrollByTop() finish, offsetY: " + this.offsetY.toFixed(2));} else {// 如果还没有滚动到顶部,则更新滚动偏移量// 每次向上滚动offsetStep像素,直到滚动到头this.offsetY = ((this.offsetY - this.offsetStep) < -this.headHeight) ?(-this.headHeight) : (this.offsetY - this.offsetStep);}}, this.intervalTime);} else {// 如果已经滚动到顶部,则无需执行滚动操作this.logD("scrollByTop(): already scrolled to top edge");}}/*** 重置刷新状态,将刷新头部恢复到初始状态。*/private resetRefreshStatus() {// 设置滚动偏移量为-headHeight,即刷新头部隐藏的状态this.offsetY = -this.headHeight;// 设置刷新头部图标为默认图标this.refreshHeadIcon = $r("app.media.icon_refresh_down");// 设置刷新头部文本为默认文本this.refreshHeadText = Constant.REFRESH_PULL_TO_REFRESH;// 调用setRefreshStatus方法,设置刷新状态为Inactive,即未激活状态this.setRefreshStatus(RefreshStatus.Inactive);}/*** 完成刷新操作后调用的方法,用于更新UI状态并滚动回列表顶部。*/private finishRefresh(): void {// 更新刷新头部的文本和图标,表示刷新成功this.refreshHeadText = Constant.REFRESH_SUCCESS;this.refreshHeadIcon = $r("app.media.icon_refresh_success");// 设置刷新状态为Done,表示刷新已完成this.setRefreshStatus(RefreshStatus.Done);// 延迟1500毫秒后滚动回列表顶部,以便用户可以看到刷新效果setTimeout(() => {this.scrollByTop();}, 1500);}/*** 组件即将显示时调用的方法,用于检查是否需要显示刷新状态。*/aboutToAppear() {// 如果当前正在刷新(由refreshing标志控制)if (this.refreshing) {// 显示刷新状态,更新UI以反映刷新正在进行中this.showRefreshingStatus();}}/*** 显示刷新状态的方法,用于更新UI以反映刷新正在进行中。*/private showRefreshingStatus() {// 将滚动偏移量设置为0,确保刷新头部可见this.offsetY = 0;// 更新刷新头部的图标为加载图标this.refreshHeadIcon = $r("app.media.icon_refresh_loading");// 更新刷新头部的文本为刷新中文本this.refreshHeadText = Constant.REFRESH_REFRESHING;// 设置刷新状态为Refresh,表示正在刷新this.setRefreshStatus(RefreshStatus.Refresh);}/*** 通知刷新开始的方法,用于调用外部提供的刷新回调函数。*/private notifyRefreshStart() {// 如果提供了onRefresh回调函数if (this.onRefresh) {// 调用onRefresh回调函数,开始执行刷新逻辑this.onRefresh();}}/*** 通知刷新状态变化的方法,用于在刷新状态变化时调用外部提供的回调函数。*/private notifyStatusChanged() {// 如果提供了onStatusChanged回调函数if (this.onStatusChanged) {// 调用onStatusChanged回调函数,通知刷新状态的变化this.onStatusChanged(this.refreshStatus);}}private logD(msg: string) {console.log(msg + ", canRefresh: " + this.canRefresh() + ", dragging: " + this.dragging + ", listScrollable: " + this.listScrollable + ", refreshing: " + this.refreshing);}
}

调用示例:

import { PullToRefreshList } from '../view/PullToRefreshList';@Entry
@Component
struct Index {@State message: string = 'Hello World';// 初始列表数据@StatedataSet: Array<string> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];@Staterefreshing: boolean = false //下拉刷新@Stateloading: boolean = false //上拉加载build() {Column() {PullToRefreshList({refreshing: $refreshing,loadingMoreIng:$loading,dataSet: $dataSet,itemLayout: (item: Object, index: number) => {this.item(item);},onRefresh: () => {this.onRefresh();},onLoadMore: () => {this.onLoadMore();},onStatusChanged: (status) => {console.log("current status: " + status);}})}.height('100%')}async onRefresh() {setTimeout(() => {console.log("finish refresh")this.dataSet = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];this.refreshing = false;}, 2500);}async onLoadMore() {setTimeout(() => {console.log("finish load More")// 生成10条0到9之间的随机数并添加到dataSet数组中for (let i = 0; i < 10; i++) {// 生成一个0到9之间的随机数const randomNumber = Math.floor(this.dataSet.length + 1);// 将随机数添加到dataSet数组中this.dataSet.push(randomNumber.toString());}this.loading = false}, 2500);}//实现真正的布局 item@Builderitem(item: Object) {Text('' + item) // 显示列表项文本.width('100%') // 设置文本宽度.height(100) // 设置文本高度.fontSize(24) // 设置字体大小.textAlign(TextAlign.Center) // 设置文本居中对齐.borderRadius(10) // 设置边框圆角.backgroundColor(0xDCDCDC); // 设置背景颜色}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.diaolansx.cn/news/25604742.html

如若内容造成侵权/违法违规/事实不符,请联系吊兰实现网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

《自动机理论、语言和计算导论》阅读笔记:p428-p525

《自动机理论、语言和计算导论》学习第 14 天,p428-p525总结,总计 98 页。 一、技术总结 1.Kruskals algorithm(克鲁斯克尔算法) 2.NP-Complete Problems p434, We say L is NP-complete if the following statements are true about L: (1)L is in NP。 (2)For every langu…

https加密机制

参考: https://www.cnblogs.com/sxiszero/p/11133747.html https://www.cnblogs.com/technology178/p/14094375.html 对称加密:只用一个秘钥的加解密,如果秘钥进行了泄漏,导致数据不安全 非对称加密:非对称加密算法需要一组密钥对,分别是公钥和私钥,这两个密钥是成对出现…

python教程5:函数编程

函数编程 特性: 1、减少重复代码 2、让程序变的可扩展 3、使程序变得易维护 定义: 默认参数 要求:默认参数放在其他参数后边 指定参数(调用的时候) 正常情况下,给函数传参数要按顺序,不想按顺序就可以⽤指定参数,只需指定参数名即可,但记住⼀个要求就是,指定参数必须放…

问题管理员的工作角色、职责和技能

问题管理就是识别、分析和解决反复出现的根本原因问题并永久修复它们。听起来很简单对吧,不幸的是,情况并非总是如此。对于组织来说,IT问题管理一直是一门棘手的 ITSM 学科。一个经常被忽视的关键因素是有效的问题 管理不仅仅是工具和流程。 它需要熟练的人来带路 问题管理员…

MySQL排序时, ORDER BY将空值NULL放在最后

我们在日常工作当中;往往业务会提到一些莫名其妙的排序等规则;例如:按照某个字段升序排列,同时空值放在后面;但mysql默认升序排列时空值是在最前面;有下面几个方法: 方法一:ORDER BY 字段 IS NULL ,字段 ;方法二:SELECT * FROM test ORDER BY IF(ISNULL(字段),1,0),字…

[西湖论剑 2022]easy_api

源码审计 下载附件得war包,bandzip解压一下,审一下源码:这个没啥东西。反序列化入口,但是访问这里是需要绕过的:其实绕过也很简单,双斜杠就绕了:web.xml filter 绕过匹配访问(针对jetty)_jetty权限绕过-CSDN博客看lib里有啥依赖:fastjson1.2.48,这不老熟人了吗.....…