YTCalendarPicker.ets 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import { DateInfo } from "../../models";
  2. @Component
  3. @CustomDialog
  4. export struct YTCalendarPicker {
  5. /**
  6. * 外部传入
  7. */
  8. // 主题色
  9. linearInfo: LinearGradientOptions = { colors: [['#B9FD2A', 0.01], ['#F5FD6D', 1]], angle: 110 }
  10. // 边距
  11. cPadding: Padding | Length | LocalizedPadding = { left: 0, right: 0, top: 20, bottom: 4 }
  12. // yyyy-mm-dd 格式的数组, 符合数组内格式的日期下会有标记
  13. dateList: string[] = []
  14. // 取消
  15. onCancel: () => void = () => {}
  16. // 确定
  17. onConfirm: (date: Date) => void = () => {}
  18. // 点击某一天的回调
  19. onClickItem: (date: Date) => void = () => {}
  20. // 是否显示头部标题
  21. showTitle: boolean = true
  22. /**
  23. * 内部方法
  24. */
  25. // 年月
  26. @State year: number = new Date().getFullYear();
  27. @State month: number = new Date().getMonth() + 1;
  28. // 开始是星期几
  29. @State private weekStart: number = 1;
  30. // 本月的天数
  31. @State private daysInMonth: number = 31;
  32. // 渲染用的月份数组
  33. @State private monthArray: DateInfo[] = [];
  34. // 当前选中的日期
  35. @State private selectDay: Date = new Date()
  36. // 显示选择日期的菜单
  37. @State private showDatePickerMenu: boolean = false
  38. // 弹窗控制器 -
  39. diaLogControl?: CustomDialogController
  40. // 星期中文映射表
  41. private readonly WEEK_MAP = ['日', '一', '二', '三', '四', '五', '六'];
  42. // 日期选择器的范围
  43. private declare range: TextCascadePickerRangeContent[]
  44. // 年份 - 日期选择器使用
  45. private years = Array.from<number, number>({ length: new Date().getFullYear() - 1970 + 1 }, (_: number, i: number) => 1970 + i)
  46. //当前年月选择的下标
  47. private selectedIndex: number[] = [new Date().getFullYear() - 1970, new Date().getMonth()]
  48. /**
  49. * 回调
  50. */
  51. // 更改月份
  52. changeMonth(year: number, month: number) {
  53. // 1. 获取当前月份的天数
  54. this.daysInMonth = this.getDaysInMonth(year, month);
  55. // 2. 获取当月的第一天是星期几
  56. this.weekStart = new Date(year, month - 1, 1).getDay() + 1;
  57. // 3. 初始化月份数组
  58. this.monthArray = [...new Array(this.weekStart - 1).fill({} as DateInfo),
  59. ...this.generateBackwardDateArray(new Date(year, month - 1, 1), this.daysInMonth, false)];
  60. return this.monthArray
  61. }
  62. // 点击单个日期
  63. clickItem(item: DateInfo){
  64. this.selectDay = item.id
  65. this.onClickItem(this.selectDay)
  66. }
  67. // 更改日期选择器的显示和隐藏
  68. changeDatePickerMenu(show: boolean) {
  69. animateToImmediately({ duration: 200 }, () => {
  70. this.showDatePickerMenu = show
  71. })
  72. }
  73. /**
  74. * 获取指定月份的天数
  75. * @param year 年
  76. * @param month 月份
  77. * @returns 当月的天数
  78. */
  79. getDaysInMonth(year: number, month: number): number {
  80. return new Date(year, month, 0).getDate();
  81. }
  82. /**
  83. * 判断两个 Date 对象是否为同一天
  84. * @param date1
  85. * @param date2
  86. * @returns
  87. */
  88. isSameDay(date1: Date, date2: Date): boolean {
  89. return (
  90. date1.getFullYear() === date2.getFullYear() &&
  91. date1.getMonth() === date2.getMonth() &&
  92. date1.getDate() === date2.getDate()
  93. );
  94. }
  95. /**
  96. * 格式化日期对象为自定义字符串
  97. * @param date 日期对象
  98. * @param needTime 是否需要时间 ( 时、分、秒 )
  99. * @param needSecond 是否需要秒
  100. * @returns
  101. */
  102. formatDateToCustomString(date: Date, needTime: boolean = true, needSecond: boolean = true): string {
  103. // 转换为 YY-MM-DD HH:mm:ss 格式
  104. const year = date.getFullYear().toString();
  105. const month = (date.getMonth() + 1).toString().padStart(2, '0');
  106. const day = date.getDate().toString().padStart(2, '0');
  107. const hours = date.getHours().toString().padStart(2, '0');
  108. const minutes = date.getMinutes().toString().padStart(2, '0');
  109. const seconds = date.getSeconds().toString().padStart(2, '0');
  110. const result =
  111. `${year}-${month}-${day}` + (needTime ? ` ${hours}:${minutes}` + (needSecond ? `:${seconds}` : '') : '');
  112. return result;
  113. }
  114. /**
  115. * 生成从指定日期开始向后的连续日期数组,可以包含今天但不能超过今天
  116. * @param startDate 起始日期(默认当前日期)
  117. * @param count 生成的日期数量(默认7天)
  118. * @returns 日期对象数组,包含今天但不包含超过今天的日期
  119. */
  120. generateBackwardDateArray(
  121. startDate: Date = new Date(),
  122. count: number = 7,
  123. isCheck: boolean = true
  124. ): DateInfo[] {
  125. const dateArray: DateInfo[] = [];
  126. // 复制起始日期,避免修改原对象
  127. const currentDate = new Date(startDate);
  128. // 获取今天的日期
  129. // 获取今天的日期并设置时间为23:59:59:999,便于比较
  130. const today = new Date();
  131. today.setHours(0, 0, 0, 0);
  132. today.setDate(today.getDate() + 1);
  133. today.setTime(today.getTime() - 1); // 设置为今天的最后一毫秒
  134. for (let i = 0; i < count; i++) {
  135. // 检查当前日期是否超过今天
  136. if (isCheck && currentDate > today) {
  137. break;
  138. }
  139. // 添加日期信息到数组
  140. dateArray.push(this.createDateInfo(currentDate));
  141. // 日期加1天(向后)
  142. currentDate.setDate(currentDate.getDate() + 1);
  143. }
  144. return dateArray;
  145. }
  146. /**
  147. * 创建一个日期信息对象
  148. * @param date 日期对象
  149. * @returns DateInfo 对象
  150. */
  151. createDateInfo(date: Date): DateInfo {
  152. const year = date.getFullYear();
  153. const month = date.getMonth() + 1;
  154. const day = date.getDate();
  155. const week = this.WEEK_MAP[date.getDay()];
  156. return {
  157. year: year,
  158. month: month,
  159. day: day,
  160. week: week,
  161. id: new Date(date)
  162. };
  163. }
  164. aboutToAppear(): void {
  165. this.changeMonth(this.year, this.month);
  166. const monthsRange =
  167. Array.from<number, number>({ length: 12 }, (_: number, i: number) => 1 + i).map(month => {
  168. return { text: month.toString() } as TextCascadePickerRangeContent
  169. })
  170. this.range = this.years.map(year => {
  171. return { text: year.toString(), children: monthsRange } as TextCascadePickerRangeContent
  172. })
  173. }
  174. build() {
  175. Column({ space: 20 }) {
  176. // 月份切换和头部组件
  177. if(this.showTitle) {
  178. Row() {
  179. // 月份切换
  180. Row({ space: 8 }) {
  181. Text(`${this.year}-${this.month.toString().padStart(2, '0')}`)
  182. .fontSize(16)
  183. .fontWeight(600)
  184. .fontColor(Color.Black)
  185. Image($r('app.media.ic_back'))
  186. .width(14)
  187. .height(8)
  188. .rotate({angle: this.showDatePickerMenu ? 90 : 270})
  189. }
  190. .onClick(() => {
  191. this.changeDatePickerMenu(true)
  192. })
  193. .bindMenu(this.showDatePickerMenu, this.dateSelectMenu, {
  194. onDisappear: () => {
  195. this.changeDatePickerMenu(false)
  196. this.year = this.range[this.selectedIndex[0]].text.valueOf() as number
  197. this.month = this.range[this.selectedIndex[0]].children![this.selectedIndex[1]].text.valueOf() as number
  198. this.changeMonth(this.year, this.month)
  199. },
  200. placement: Placement.Bottom
  201. })
  202. // 右上角的结构
  203. this.buttonRow()
  204. }
  205. .width("100%")
  206. .alignItems(VerticalAlign.Center)
  207. .justifyContent(FlexAlign.SpaceBetween)
  208. }
  209. // 周 title 的显示
  210. Row() {
  211. ForEach(this.WEEK_MAP, (item: string, index: number) => {
  212. Row(){
  213. Text(item)
  214. .fontColor(Color.Black)
  215. }
  216. .layoutWeight(1)
  217. .justifyContent(FlexAlign.Center)
  218. })
  219. }
  220. .width("100%")
  221. .justifyContent(FlexAlign.SpaceBetween)
  222. // 日历主体
  223. Grid() {
  224. ForEach(this.monthArray, (item: DateInfo, index: number) => {
  225. GridItem() {
  226. Column({ space: 5 }) {
  227. if (item.id) {
  228. // 日历-单个日期的组件
  229. Row() {
  230. Text(item.day + '')
  231. .fontSize(12)
  232. .fontColor(this.isSameDay(item.id, this.selectDay) ? Color.Black : '#979797')
  233. }
  234. .width("100%")
  235. .aspectRatio(1)
  236. .borderRadius(8)
  237. .alignItems(VerticalAlign.Center)
  238. .justifyContent(FlexAlign.Center)
  239. .linearGradient(this.isSameDay(item.id, this.selectDay) ? this.linearInfo : {
  240. colors: [['#F6F6F6', 1]],
  241. })
  242. .onClick(() => {
  243. this.clickItem(item)
  244. })
  245. // 日期标记 - 如无需求,可删除
  246. Text()
  247. .width(4)
  248. .aspectRatio(1)
  249. .borderRadius(2)
  250. .linearGradient(this.dateList.indexOf(this.formatDateToCustomString(item.id, false)) !== -1 ? this.linearInfo : null)
  251. } else {
  252. // 空白占位符
  253. Text('')
  254. }
  255. }
  256. .width(32)
  257. .alignItems(HorizontalAlign.Center)
  258. .justifyContent(FlexAlign.Center)
  259. }
  260. })
  261. }
  262. .rowsGap(10)
  263. .maxCount(6)
  264. .width("100%")
  265. .columnsTemplate('repeat(7, 1fr)')
  266. }
  267. .width("100%")
  268. .padding(this.cPadding)
  269. .backgroundColor(Color.White)
  270. }
  271. // 日期选择菜单
  272. @Builder
  273. private dateSelectMenu() {
  274. Stack({ alignContent: Alignment.Center }) {
  275. TextPicker({ range: this.range, selected: this.selectedIndex })
  276. .divider(null)
  277. .defaultPickerItemHeight(36)
  278. .backgroundColor(Color.White)
  279. .selectedTextStyle({ color: '#FF353C46', font: { weight: 500, size: 16 } })
  280. .onScrollStop((value, index) => {
  281. console.log(`testLog ${value} ${index}`)
  282. this.selectedIndex = index as number[]
  283. })
  284. //自定义选择遮罩
  285. Column() {
  286. }
  287. .width('100%')
  288. .height(36)
  289. .backgroundColor('#52D9D9D9')
  290. .borderRadius(8)
  291. }
  292. .height(140)
  293. .width(160)
  294. .padding(12)
  295. .borderRadius(8)
  296. .backgroundColor(Color.White)
  297. }
  298. // 右上角结构 - 确定和取消按钮
  299. @Builder
  300. private buttonRow() {
  301. Row({ space: 14 }) {
  302. Text("取消")
  303. .borderRadius(36)
  304. .backgroundColor('#F6F6F6')
  305. .fontColor(Color.Black)
  306. .padding({
  307. left: 20,
  308. top: 5,
  309. right: 20,
  310. bottom: 5
  311. })
  312. .onClick(this.onCancel)
  313. Text("确认")
  314. .borderRadius(36)
  315. .linearGradient(this.linearInfo)
  316. .fontColor(Color.Black)
  317. .padding({
  318. left: 20,
  319. top: 5,
  320. right: 20,
  321. bottom: 5
  322. })
  323. .onClick(() => {
  324. this.onConfirm(this.selectDay)
  325. })
  326. }
  327. }
  328. }