HarmonyOS 鸿蒙Next中关于用TextPicker封装一个DateSelector的时候遇到的困难

HarmonyOS 鸿蒙Next中关于用TextPicker封装一个DateSelector的时候遇到的困难 各位大佬,我在用TextPicker封装一个日期选择组件的时候遇到了这样的问题,日期从3月1号跳转到2月的28/29号时以及从2月的28/29号跳转到3月1号的时候,封装的组件要么是日期混乱,要么就是一下子跳转到了1月份,请各位大佬帮我看一下我的这个组件的代码,如果可以的话请把我的bug指出,非常感谢。

初步判断bug应该出现在161~177行之间。

import { application, common, EnvironmentCallback } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

@ComponentV2
export struct LoopDatePage {
  build() {
    NavDestination() {
      this.DateSelectorBuilder()
    }
    .title("LoopDatePage")
    .width("100%")
    .height("100%")
  }

  @Builder
  DateSelectorBuilder() {
    Column({ space: 20 }) {
      DatePicker()

      DateSelector()
    }
    .width("100%")
    .height("100%")
  }
}


// 选择器组件
@ComponentV2
struct DateSelector {
  /**
   * @param canLoop
  * 是否可以循环
   */
  @Param canLoop: boolean = true
  /**
   * @param startYear
  * 年份开始范围
   */
  @Param startYear: number = 150
  /**
   * @param endYear
  * 年份结束范围
   */
  @Param endYear: number = 12
  /**
   * 默认年/月/日传参
   */
  @Param yearParam: number | undefined = undefined
  @Param monthParam: number | undefined = undefined
  @Param dayParam: number | undefined = undefined
  @Local date: Date = new Date()
  // 年/月/日列表
  @Local yearArr: string[] = []
  @Local monthArr: string[] = []
  @Local dayArr: string[] = []
  // 选中的日期
  @Local selectedYearIndex: number = 0
  @Local selectedMonthIndex: number = 0
  @Local selectedDayIndex: number = 0
  // 处理联动 年 - 月
  @Local nowMonthIndex: number = 0
  @Local nextMonthIndex: number = 0
  // 处理联动 月 - 日
  @Local nowIndex: number = 0
  @Local nextIndex: number = 0
  @Local nowArrLength: number = 0
  @Local nextArrLength: number = 0
  // 获取上下文
  private moduleContext: common.Context | undefined = undefined
  // 国际化
  @Local isEnglishLanguage: boolean = false
  // 是否显示单位
  @Local showUnit: boolean = true
  /**
   * 取值回调返回
   */
  @Event callbackDate: (year: number, month: number, day: number) => void =
    (year: number, month: number, day: number) => {
    }

  async aboutToAppear() {
    // 获取上下文
    this.registerLocalesChangeEvent(async () => {
      this.moduleContext = await this.initLocalesModuleResourceContextAsync()
      this.toCheckLanguageEnvironment()
      this.getDateArr()
    })

    this.moduleContext = await this.initLocalesModuleResourceContextAsync()
    this.toCheckLanguageEnvironment()

    this.initLoopDate()

    this.getDateArr()
  }

  build() {
    Column() {
      Stack() {
        // 选中项背景阴影
        Row() {
          Row() {
          }
          .width("100%")
          .height("100%")
          .backgroundColor("#f4f6f7")
          .borderRadius(16)
        }
        .padding({
          right: 20,
          left: 20
        })
        .width("100%")
        .height(48)

        // 年月日滚动选择器
        Row() {
          TextPicker({
            range: this.yearArr,
            selected: $$this.selectedYearIndex,
          })
            .pickerStyle(this.canLoop)
            .visibility(this.isEnglishLanguage ? Visibility.None : Visibility.Visible)
            .onChange((item, index) => {
              this.selectedYearIndex = index as number
            })
            .onScrollStop(() => {
              this.callbackDate((this.selectedYearIndex + (this.date.getFullYear() - this.startYear)), this.selectedMonthIndex + 1, this.selectedDayIndex + 1)
            })

          TextPicker({
            range: this.monthArr,
            selected: $$this.selectedMonthIndex
          }).pickerStyle(this.canLoop)
            .onEnterSelectedArea((item, index) => {
              let trans = index as number
              this.nextMonthIndex = trans
              if ((this.nowMonthIndex == 11 && this.nextMonthIndex == 0) || (this.nowMonthIndex == 0 && this.nextMonthIndex == 0)) {
                this.selectedYearIndex = ((this.selectedYearIndex == this.yearArr.length) ? 0 : (this.selectedYearIndex + 1))
              }
              if ((this.nowMonthIndex == 0 && this.nextMonthIndex == 11) || (this.nowMonthIndex == 11 && this.nextMonthIndex == 11)) {
                this.selectedYearIndex = ((this.selectedYearIndex == 0) ? (this.yearArr.length) : (this.selectedYearIndex - 1))
              }
            })
            .onChange((item, index) => {
              let trans = index as number
              this.nowMonthIndex = trans
              this.selectedMonthIndex = trans
              this.changeDayArr()
            })
            .onScrollStop(() => {
              this.callbackDate((this.selectedYearIndex + (this.date.getFullYear() - this.startYear)), this.selectedMonthIndex + 1, this.selectedDayIndex + 1)
            })


          TextPicker({
            range: this.dayArr,
            selected: $$this.selectedDayIndex
          }).pickerStyle(this.canLoop)
            .onEnterSelectedArea((item, index) => {
              this.nextIndex = index as number
              let arrLength = (this.dayArr.length - 1)
              if ((this.nowIndex == arrLength && this.nextIndex == 0) || (this.nowIndex == 0 && this.nextIndex == 0)) {
                this.selectedMonthIndex = (this.selectedMonthIndex == 11) ? 0 : this.selectedMonthIndex + 1
              }
              if ((this.nowIndex == 0 && this.nextIndex == arrLength) || (this.nowIndex == arrLength && this.nextIndex == arrLength)) {
                this.selectedMonthIndex = (this.selectedMonthIndex == 0) ? 11 : this.selectedMonthIndex - 1
                // Todo 有问题,需要重构逻辑
              }
            })
            .onChange((item, index) => {
              let trans = index as number
              this.nowIndex = trans
              this.selectedDayIndex = trans
              this.changeDayArr()
            })
            .onScrollStop(() => {
              this.callbackDate((this.selectedYearIndex + (this.date.getFullYear() - this.startYear)), this.selectedMonthIndex + 1, this.selectedDayIndex + 1)
            })

          TextPicker({
            range: this.yearArr,
          }).pickerStyle(this.canLoop)
            .visibility(this.isEnglishLanguage ? Visibility.Visible : Visibility.None)
            .onChange((item, index) => {
              this.selectedYearIndex = index as number
            })
            .onScrollStop(() => {
              this.callbackDate((this.selectedYearIndex + (this.date.getFullYear() - this.startYear)), this.selectedMonthIndex + 1, this.selectedDayIndex + 1)
            })
        }
        .width("100%")
        .justifyContent(FlexAlign.Center)
      }
      .width("100%")
      .alignContent(Alignment.Center)
    }
    .width("100%")
  }

  /**
   * 注册国际化改变回调
   */
  registerLocalesChangeEvent(callback: VoidCallback) {
    let environmentCallback: EnvironmentCallback = {
      onConfigurationUpdated(config) {
        callback()
      },
      onMemoryLevel(level) {
      }
    };
    getContext(this).getApplicationContext().on('environment', environmentCallback);
  }

  /**
   * 初始化语言模块上下文
   * @returns
   */
  initLocalesModuleResourceContextAsync(): Promise<Context | undefined> {
    return new Promise<common.Context | undefined>((res) => {
      try {
        application.createModuleContext(getContext(this), 'locales').then((data: Context) => {
          res(data)
        }).catch((error: BusinessError) => {
          res(undefined)
        })
      } catch (error) {
        res(undefined)
      }
    })
  }

  /**
   * 初始化数组
   */
  getDateArr() {
    this.yearArr = []
    this.monthArr = []
    this.dayArr = []
    let year = this.date.getFullYear()
    let test: string | undefined = undefined
    test = this.moduleContext?.resourceManager.getStringByNameSync("year")
    for (let i = (year - this.startYear); i <= (year - this.endYear); i++) {
      this.yearArr.push(i.toString() + (this.showUnit ? test : ""))
    }
    let arr = this.moduleContext?.resourceManager.getStringByNameSync("month_arr")
    this.monthArr = JSON.parse(arr!)
    test = this.moduleContext?.resourceManager.getStringByNameSync("date")
    for (let i = 1; i <= 31; i++) {
      this.dayArr.push(i.toString().padStart(2, " ") + (this.showUnit ? test : ""))
    }
  }

  /**
   * 检查语言环境,改变UI布局
   */
  toCheckLanguageEnvironment() {
    let trans = (this.moduleContext?.resourceManager.getConfigurationSync().locale as string).slice(0, 2)
    // this.getUIContext().showAlertDialog({
    //   message: JSON.stringify(trans)
    // })
    this.showUnit = this.checkShowUnit(trans) ? true : false
    this.isEnglishLanguage = (trans === "en" ? true : false)
  }

  /**
   * 检查是否应该显示单位(年/月/日)
   */
  checkShowUnit(str: string): boolean {
    let bool: boolean = true
    // 后续不显示单位的国家标识可以添加到数组中
    let areaArr: string[] = ["en", "ru", "fr", "es", "ar"]
    bool = areaArr.includes(str) ? false : true
    return bool
  }

  /**
   * 改变天数(月份控制)
   */
  changeDayArr() {
    let currentMonth = this.selectedMonthIndex + 1
    const month_31: number[] = [1, 3, 5, 7, 8, 10, 12]
    const month_30: number[] = [4, 6, 9, 11]
    if (month_31.includes(currentMonth)) {
      if (this.dayArr.length != 31) {
        this.dayArray(31)
      }
    } else if (month_30.includes(currentMonth)) {
      if (this.dayArr.length != 30) {
        if (this.selectedDayIndex == 30) {
          this.selectedDayIndex = 29
        }
        this.dayArray(30)
      }
    } else {
      if (this.isLeapYear(Number((this.yearArr[this.selectedYearIndex] ?? []).slice(0, 4)))) {
        if (this.dayArr.length != 29) {
          if (this.selectedDayIndex >= 28) {
            this.selectedDayIndex = 28
          }
          this.dayArray(29)
        }
      } else {
        if (this.dayArr.length != 28) {
          if (this.selectedDayIndex >= 27) {
            this.selectedDayIndex = 27
          }
          this.dayArray(28)
        }
      }
    }
  }

  /**
   * 判断是否是闰年
   */
  isLeapYear(year: number): boolean {
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
  }

  /**
   * 用于往日期数组重置
   */
  dayArray(num: number) {
    this.dayArr = []
    let test = this.moduleContext?.resourceManager.getStringByNameSync("date")
    for (let i = 1; i <= num; i++) {
      this.dayArr.push(i.toString().padStart(2, " ") + (this.showUnit ? test : ""))
    }
  }

  /**
   * 初始化组件
   */
  initLoopDate() {
    this.selectedYearIndex = (this.yearParam ? this.yearParam - (this.date.getFullYear() - this.startYear) : this.date.getFullYear() - 1909)
    this.selectedMonthIndex = (this.monthParam ? this.monthParam - 1 : 0)
    this.selectedDayIndex = this.dayParam ? this.dayParam - 1 : 0
    this.callbackDate((this.selectedYearIndex + (this.date.getFullYear() - this.startYear)), this.selectedMonthIndex + 1, this.selectedDayIndex + 1)
  }
}

/**
 * 选择器公共样式
 * @param canLoop 是否可以循环
 */
@Extend(TextPicker)
function pickerStyle(canLoop: boolean) {
  .canLoop(canLoop)
  .divider(null)
  .disappearTextStyle({
    color: "#00dddddd",
    font: {
      size: 0
    }
  })
  .selectedTextStyle({
    color: "#1B2126",
    font: {
      size: 20,
      weight: FontWeight.Medium
    }
  })
  .textStyle({
    color: "575e66",
    font: {
      size: 15,
      weight: FontWeight.Regular
    }
  })
}

更多关于HarmonyOS 鸿蒙Next中关于用TextPicker封装一个DateSelector的时候遇到的困难的实战教程也可以访问 https://www.itying.com/category-93-b0.html

11 回复

开发者您好,

当前测试了您提供的代码,当日期由3月1日向上滑动到31日,日那一列跳转到28日,月跳转至2月,

从2月28日向下滑动到1日,日那一列跳转为1日,月那一列为3月,这个现象是正常的,

您提到一下子跳转为1月份,我这边复现步骤是:先将日志固定至3月1日,调整月份到2月,再将日由1号跳转到28号,此时月份确实会跳转到1月份,

这里的逻辑是如果您预备由3月1号跳转到2月28号,直接将日由1跳转至28即可。

上面先调月份至2月,此时的日期为2月1日,日再上调至31日时,系统默认是由2月1号调至1月31号,所有自动更新月份为1月了。

您可以参看下DatePicker实现筛选年月日的逻辑(https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-datepicker#示例3设置显示年月和月日列),跟您当前提供的现象是一致的。按这个逻辑来说,这不是BUG。

更多关于HarmonyOS 鸿蒙Next中关于用TextPicker封装一个DateSelector的时候遇到的困难的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


为什么我这段代码在本地跑的时候就会出现bug,能麻烦您再多测试一下嘛?比如快速来回滑动切换日期查看月份是否正常,或者快速朝某个方向滑动看月份是否切换正常。

尊敬的开发者,您好!您的问题已受理,请您耐心等待,感谢您的理解与支持!

直接提工单

开发者您好,为了更快解决您的问题,尽量补全以下信息: (如下信息根据实际情况选择)

  1. 问题现象,本地运行你的代码没有复现问题,请提供下复现步骤以及出现问题的截图;

  2. 版本信息(如:开发工具、手机系统版本信息);

2月份的日期,包括从3月1号到2月28/29号,或者从2月28/29号到3月1号的来回切换的时候会出现问题。 开发工具版本是win版本的6.0.0.848的DevEco,测试机版本是6.0develop版本的mate60。

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

我这边本地使用的是6.0.0.848版本的DevEco Studio,测试手机版本是6.0.0.100,我这边测试是3月1日向上滑动到31日,日那一列变成了28日,月那一列变成2月,从2月28日向下滑动到1日,日那一列变成了1日,月那一列变成3月,这个现象应该是正常的吧,另外你测试机的具体版本号是什么。

我这边的测试机是6.0.0.100版本的Mate60Pro,但是就是会出现这个问题,日期月份联动有问题,

在鸿蒙Next中使用TextPicker封装DateSelector时,主要困难在于TextPicker默认不支持日期联动逻辑。需通过自定义组件实现年、月、日的动态数据绑定,使用@State管理选中状态,并在onChange回调中更新关联数据源。需注意日期数据验证和范围限制,避免无效日期组合。可通过@Provide/@Consume实现组件间状态同步。

问题出在月份切换时的年索引计算逻辑上。在 onEnterSelectedArea 回调中,当月份从3月(索引2)切换到2月(索引1)时,代码错误地调整了年份索引。

具体问题在161-177行,特别是:

  1. 月份切换边界条件判断不准确,导致年份错误增减
  2. 年份索引计算逻辑存在缺陷,没有正确处理跨年边界

建议修复:

  1. 简化月份切换时的年份联动逻辑,避免在 onEnterSelectedArea 中直接修改年份索引
  2. onChange 回调中统一处理日期联动,确保年月日同步更新
  3. 使用更可靠的方法计算闰年2月的天数变化

关键是要确保当月数变化时,年份只在真正跨年时(1月到12月或12月到1月)才进行调整,而不是在任意月份切换时都触发年份变化。

回到顶部