dy-date-time-picker.vue 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644
  1. <template>
  2. <view class="custom-date-picker">
  3. <!-- 日期筛选触发器 -->
  4. <slot
  5. name="date-trigger"
  6. :text="displayDateText || placeholder"
  7. :displayText="displayDateText"
  8. :placeholder="placeholder"
  9. :hasValue="hasDateValue"
  10. :open="openDatePopup"
  11. :clear="clearDate"
  12. :value="currentValue"
  13. >
  14. <view class="picker-trigger date-trigger" @click="openDatePopup" :class="{ 'active': hasDateValue }">
  15. <text class="text">{{ displayDateText || placeholder }}</text>
  16. <view v-if="hasDateValue" class="clear-btn" @click.stop="clearDate">
  17. <text class="clear-icon">×</text>
  18. </view>
  19. <text v-else class="arrow">▼</text>
  20. </view>
  21. </slot>
  22. <!-- 时间筛选触发器 -->
  23. <!-- <slot
  24. name="time-trigger"
  25. :text="displayTimeText || timePlaceholder"
  26. :displayText="displayTimeText"
  27. :placeholder="timePlaceholder"
  28. :hasValue="hasTimeValue"
  29. :open="openTimePopup"
  30. :clear="clearTime"
  31. :value="currentValue"
  32. >
  33. <view class="picker-trigger time-trigger" @click="openTimePopup" :class="{ 'active': hasTimeValue }">
  34. <text class="text">{{ displayTimeText || timePlaceholder }}</text>
  35. <view v-if="hasTimeValue" class="clear-btn" @click.stop="clearTime">
  36. <text class="clear-icon">×</text>
  37. </view>
  38. <text v-else class="arrow">▼</text>
  39. </view>
  40. </slot> -->
  41. <!-- 弹出层 -->
  42. <view class="picker-popup" :class="{ 'show': showPopup }" @click.stop>
  43. <view class="mask" @click="closePopup"></view>
  44. <view class="popup-content">
  45. <view class="popup-header">
  46. <view class="btn cancel" @click="closePopup">取消</view>
  47. <view class="title">{{ popupTitle }}</view>
  48. <view class="btn confirm" @click="confirm">确定</view>
  49. </view>
  50. <!-- 模式切换 Tabs (仅在日期模式下显示) -->
  51. <view class="mode-tabs" v-if="popupType === 'date'">
  52. <view
  53. class="tab-item"
  54. :class="{ active: currentMode === 'year' }"
  55. @click="switchMode('year')"
  56. v-if="isModeEnabled('year')"
  57. >年份</view>
  58. <view
  59. class="tab-item"
  60. :class="{ active: currentMode === 'month' }"
  61. @click="switchMode('month')"
  62. v-if="isModeEnabled('month')"
  63. >月份</view>
  64. <view
  65. class="tab-item"
  66. :class="{ active: currentMode === 'range' }"
  67. @click="switchMode('range')"
  68. v-if="isModeEnabled('range')"
  69. >日期</view>
  70. <view
  71. class="tab-item"
  72. :class="{ active: currentMode === 'week' }"
  73. @click="switchMode('week')"
  74. v-if="isModeEnabled('week')"
  75. >周</view>
  76. </view>
  77. <view class="picker-body">
  78. <view class="range-tip" v-if="popupType === 'date' && currentMode === 'range'">
  79. 点击一次选择单日;再次选择可形成范围
  80. </view>
  81. <!-- 年份/月份选择器头部 (用于日历视图切换年月) -->
  82. <view class="calendar-header" v-if="popupType === 'date' && currentMode === 'range'">
  83. <view class="arrow-btn" @click="changeMonth(-1)">‹</view>
  84. <view class="current-date-text">
  85. {{ displayYear }}年 {{ displayMonth }}月
  86. </view>
  87. <view class="arrow-btn" @click="changeMonth(1)">›</view>
  88. </view>
  89. <!-- 星期头部 -->
  90. <view class="week-header" v-if="popupType === 'date' && currentMode === 'range'">
  91. <text v-for="day in weeks" :key="day" class="week-item">{{ day }}</text>
  92. </view>
  93. <!-- 日历网格 (日期/范围模式) -->
  94. <scroll-view scroll-y class="calendar-body" v-if="popupType === 'date' && currentMode === 'range'">
  95. <view class="days-grid">
  96. <view
  97. v-for="(day, index) in calendarDays"
  98. :key="index"
  99. class="day-item"
  100. :class="{
  101. 'empty': !day.day,
  102. 'today': isToday(day),
  103. 'selected': isSelected(day),
  104. 'in-range': isInRange(day),
  105. 'range-start': isRangeStart(day),
  106. 'range-end': isRangeEnd(day),
  107. 'disabled': day.disabled
  108. }"
  109. @click="onDayClick(day)"
  110. >
  111. <text>{{ day.day }}</text>
  112. </view>
  113. </view>
  114. </scroll-view>
  115. <!-- 月份选择列表 -->
  116. <scroll-view scroll-y class="list-body month-list" v-if="popupType === 'date' && currentMode === 'month'">
  117. <!-- <view class="range-tip">点击一次选择单月;再次选择可形成范围</view> -->
  118. <view class="year-selector">
  119. <view class="arrow-btn" @click="changeYear(-1)">‹</view>
  120. <view class="current-year">{{ tempYear }}年</view>
  121. <view class="arrow-btn" @click="changeYear(1)">›</view>
  122. </view>
  123. <view class="grid-container">
  124. <view
  125. v-for="m in 12"
  126. :key="m"
  127. class="grid-item"
  128. :class="{
  129. selected: isMonthSelected(m),
  130. 'range-start': isMonthRangeStart(m),
  131. 'range-end': isMonthRangeEnd(m),
  132. 'in-range': isMonthInRange(m)
  133. }"
  134. @click="onMonthSelect(m)"
  135. >
  136. {{ m }}月
  137. </view>
  138. </view>
  139. </scroll-view>
  140. <!-- 年份选择列表 -->
  141. <scroll-view scroll-y class="list-body year-list" v-if="popupType === 'date' && currentMode === 'year'">
  142. <!-- <view class="year-range-tip">点击一次选择单年;再次选择可形成区间范围</view> -->
  143. <view class="grid-container">
  144. <view
  145. v-for="y in yearList"
  146. :key="y"
  147. class="grid-item"
  148. :class="{
  149. selected: isYearSelected(y),
  150. 'range-start': isYearRangeStart(y),
  151. 'range-end': isYearRangeEnd(y),
  152. 'in-range': isYearInRange(y)
  153. }"
  154. @click="onYearSelect(y)"
  155. >
  156. {{ y }}年
  157. </view>
  158. </view>
  159. </scroll-view>
  160. <!-- 周区间选择 -->
  161. <view class="week-range-section" v-if="popupType === 'date' && currentMode === 'week'">
  162. <view class="range-tip">点击一次选择起始周;再次选择可形成区间范围</view>
  163. <view class="year-selector">
  164. <view class="arrow-btn" @click="changeWeekYear(-1)">‹</view>
  165. <view class="current-year">{{ tempWeekYear }}年</view>
  166. <view class="arrow-btn" @click="changeWeekYear(1)">›</view>
  167. </view>
  168. <scroll-view scroll-y class="list-body week-list">
  169. <view class="grid-container week-grid">
  170. <view
  171. v-for="w in weekList"
  172. :key="w"
  173. class="grid-item"
  174. :class="{
  175. selected: isWeekSelected(w),
  176. 'range-start': isWeekStart(w),
  177. 'range-end': isWeekEnd(w),
  178. 'in-range': isWeekInRange(w)
  179. }"
  180. @click="onWeekSelect(w)"
  181. >
  182. 第{{ w }}周
  183. </view>
  184. </view>
  185. </scroll-view>
  186. </view>
  187. <!-- 时间筛选区域 (仅在时间模式下显示) -->
  188. <view class="time-filter-section" v-if="popupType === 'time'">
  189. <!-- <view class="section-divider"></view> -->
  190. <view class="time-header">
  191. <!-- <view class="title">时间筛选</view> -->
  192. <view class="quick-btns">
  193. <text class="btn" :class="{ active: isQuickTimeActive('all') }" @click="selectQuickTime('all')">全天</text>
  194. <text class="btn" :class="{ active: isQuickTimeActive('am') }" @click="selectQuickTime('am')">上午(09:00-12:00)</text>
  195. <text class="btn" :class="{ active: isQuickTimeActive('pm') }" @click="selectQuickTime('pm')">下午(14:00-18:00)</text>
  196. </view>
  197. </view>
  198. <!-- 自定义时间范围 (固定Header) -->
  199. <view class="time-selector-container">
  200. <view class="time-table-header">
  201. <view class="th">开始时间</view>
  202. <view class="th-separator"></view>
  203. <view class="th">结束时间</view>
  204. </view>
  205. <view class="time-table-body">
  206. <scroll-view scroll-y class="time-column">
  207. <view
  208. v-for="t in timeList"
  209. :key="'start-' + t"
  210. class="time-item"
  211. :class="{ selected: tempTimeValue.startTime === t }"
  212. @click="onTimeSelect('start', t)"
  213. >
  214. {{ t }}
  215. </view>
  216. </scroll-view>
  217. <view class="body-separator">至</view>
  218. <scroll-view scroll-y class="time-column">
  219. <view
  220. v-for="t in timeList"
  221. :key="'end-' + t"
  222. class="time-item"
  223. :class="{ selected: tempTimeValue.endTime === t }"
  224. @click="onTimeSelect('end', t)"
  225. >
  226. {{ t }}
  227. </view>
  228. </scroll-view>
  229. </view>
  230. </view>
  231. </view>
  232. </view>
  233. </view>
  234. </view>
  235. </view>
  236. </template>
  237. <script>
  238. export default {
  239. name: 'dy-date-time-picker',
  240. props: {
  241. // 选中的值
  242. // 日期模式(单选/范围合一):
  243. // { startDate: 'YYYY-MM-DD', endDate: 'YYYY-MM-DD' }(单选时返回 { date: 'YYYY-MM-DD' })
  244. // 月份模式(单选/范围合一):
  245. // { startMonth: 'YYYY-MM', endMonth: 'YYYY-MM' }(单选时返回 { month: 'YYYY-MM' })
  246. // 年份模式(单选/范围合一):
  247. // { startYear: 'YYYY', endYear: 'YYYY' }(单选时返回 { year: 'YYYY' })
  248. // 周区间模式:
  249. // { startWeek: 'YYYY-WW', endWeek: 'YYYY-WW', weekStartDate: 'YYYY-MM-DD', weekEndDate: 'YYYY-MM-DD' }
  250. // (单选时返回 { week: 'YYYY-WW', weekStartDate: 'YYYY-MM-DD', weekEndDate: 'YYYY-MM-DD' })
  251. // time: { startTime, endTime }
  252. value: {
  253. type: [String, Object],
  254. default: ''
  255. },
  256. modelValue: {
  257. type: [String, Object],
  258. default: undefined
  259. },
  260. minYear: {
  261. type: Number,
  262. default: 2004
  263. },
  264. maxYear: {
  265. type: Number,
  266. default: () => new Date().getFullYear()
  267. },
  268. placeholder: {
  269. type: String,
  270. default: '选择日期'
  271. },
  272. timePlaceholder: {
  273. type: String,
  274. default: '筛选时间'
  275. },
  276. typeEnabled: {
  277. type: Object,
  278. default: () => ({
  279. date: true,
  280. month: true,
  281. year: true,
  282. week: true
  283. })
  284. }
  285. },
  286. data() {
  287. return {
  288. showPopup: false,
  289. popupType: 'date', // 'date' 或 'time',决定弹窗显示内容
  290. currentMode: 'range', // range, month, year, week
  291. weeks: ['日', '一', '二', '三', '四', '五', '六'],
  292. // 内部显示的年月(用于日历翻页)
  293. displayYear: new Date().getFullYear(),
  294. displayMonth: new Date().getMonth() + 1,
  295. // 临时选中的值(未确认前)
  296. tempValue: null,
  297. // 时间筛选临时值
  298. tempTimeValue: {
  299. startTime: '',
  300. endTime: ''
  301. },
  302. // 辅助变量
  303. tempYear: new Date().getFullYear(), // 月份模式下的年份
  304. tempWeekYear: new Date().getFullYear()
  305. }
  306. },
  307. computed: {
  308. popupTitle() {
  309. return this.popupType === 'date' ? '选择日期' : '选择时间'
  310. },
  311. enabledTypeMap() {
  312. const defaults = { date: true, month: true, year: true, week: true }
  313. const config = this.typeEnabled || {}
  314. return { ...defaults, ...config }
  315. },
  316. currentValue() {
  317. return this.modelValue !== undefined ? this.modelValue : this.value
  318. },
  319. hasDateValue() {
  320. const val = this.currentValue
  321. if (!val) return false
  322. if (typeof val === 'object') {
  323. if (val.date) return true
  324. if (val.startDate || val.endDate) return true
  325. if (val.month) return true
  326. if (val.startMonth || val.endMonth) return true
  327. if (val.year) return true
  328. if (val.startYear || val.endYear) return true
  329. if (val.week || val.startWeek || val.endWeek) return true
  330. } else if (val) {
  331. return true // 字符串情况
  332. }
  333. return false
  334. },
  335. hasTimeValue() {
  336. const val = this.currentValue
  337. if (!val || typeof val !== 'object') return false
  338. const time = val.time || {}
  339. return !!(time.startTime || time.endTime)
  340. },
  341. displayDateText() {
  342. if (!this.hasDateValue) return ''
  343. const val = this.currentValue
  344. if (typeof val === 'object') {
  345. if (val.date) return val.date
  346. if (val.startDate || val.endDate) {
  347. const start = val.startDate
  348. const end = val.endDate
  349. if (start && end) return start === end ? start : `${start} 至 ${end}`
  350. if (start) return `${start} (未完成)`
  351. }
  352. if (val.startMonth || val.endMonth) {
  353. const start = val.startMonth
  354. const end = val.endMonth
  355. if (start && end) return start === end ? start : `${start} 至 ${end}`
  356. if (start) return `${start} (未完成)`
  357. }
  358. if (val.month) return val.month
  359. if (val.startYear || val.endYear) {
  360. const start = val.startYear
  361. const end = val.endYear
  362. if (start && end) return start === end ? `${start}年` : `${start}年 至 ${end}年`
  363. if (start) return `${start}年 (未完成)`
  364. }
  365. if (val.year) return val.year + '年'
  366. if (val.week || val.startWeek || val.endWeek) {
  367. const start = val.startWeek
  368. const end = val.endWeek
  369. const week = val.week
  370. if (week) {
  371. const parsed = this.parseWeekValue(week)
  372. return parsed ? `${parsed.year}年第${parsed.week}周` : week
  373. }
  374. if (start && end) {
  375. const startParsed = this.parseWeekValue(start)
  376. const endParsed = this.parseWeekValue(end)
  377. if (startParsed && endParsed) {
  378. return `${startParsed.year}年第${startParsed.week}周 至 第${endParsed.week}周`
  379. }
  380. return `${start} 至 ${end}`
  381. }
  382. if (start) {
  383. const startParsed = this.parseWeekValue(start)
  384. if (startParsed) return `${startParsed.year}年第${startParsed.week}周 (未完成)`
  385. return `${start} (未完成)`
  386. }
  387. }
  388. } else {
  389. const valStr = String(val)
  390. if (valStr.length === 4) return `${valStr}年`
  391. return valStr
  392. }
  393. return ''
  394. },
  395. displayTimeText() {
  396. if (!this.hasTimeValue) return ''
  397. const val = this.currentValue
  398. const time = val.time || {}
  399. if (time.startTime && time.endTime) return `${time.startTime}-${time.endTime}`
  400. if (time.startTime || time.endTime) return '请选择结束时间'
  401. return ''
  402. },
  403. calendarDays() {
  404. const year = this.displayYear
  405. const month = this.displayMonth
  406. const firstDayOfMonth = new Date(year, month - 1, 1).getDay()
  407. const daysInMonth = new Date(year, month, 0).getDate()
  408. const days = []
  409. // 填充上个月的空白
  410. for (let i = 0; i < firstDayOfMonth; i++) {
  411. days.push({ day: '', disabled: true })
  412. }
  413. // 填充当月天数
  414. for (let i = 1; i <= daysInMonth; i++) {
  415. const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
  416. days.push({
  417. day: i,
  418. fullDate: dateStr,
  419. disabled: false
  420. })
  421. }
  422. // 填充下个月的空白,补齐整行
  423. const remaining = days.length % 7
  424. if (remaining !== 0) {
  425. const fillCount = 7 - remaining
  426. for (let i = 0; i < fillCount; i++) {
  427. days.push({ day: '', disabled: true })
  428. }
  429. }
  430. return days
  431. },
  432. yearList() {
  433. const startYear = this.minYear || 2025
  434. const currentYear = new Date().getFullYear()
  435. const endYear = (this.maxYear || currentYear)
  436. const safeEndYear = endYear < startYear ? startYear : endYear
  437. const list = []
  438. for (let i = safeEndYear; i >= startYear; i--) {
  439. list.push(i)
  440. }
  441. return list
  442. },
  443. timeList() {
  444. const times = []
  445. for (let i = 0; i <= 23; i++) {
  446. const hour = String(i).padStart(2, '0')
  447. times.push(`${hour}:00`)
  448. times.push(`${hour}:30`) // 增加半小时刻度
  449. }
  450. times.push('23:59')
  451. return times
  452. },
  453. weekList() {
  454. const year = this.tempWeekYear
  455. const totalWeeks = this.getWeeksInYear(year)
  456. const weeks = []
  457. for (let i = 1; i <= totalWeeks; i++) {
  458. weeks.push(i)
  459. }
  460. return weeks
  461. }
  462. },
  463. watch: {
  464. // 监听 value / modelValue 变化,确保外部清空时内部也响应
  465. value: {
  466. handler(val) {
  467. if (!val) {
  468. this.tempValue = null
  469. }
  470. },
  471. deep: true
  472. },
  473. modelValue: {
  474. handler(val) {
  475. if (!val) {
  476. this.tempValue = null
  477. }
  478. },
  479. deep: true
  480. }
  481. },
  482. methods: {
  483. emitValue(result) {
  484. if (result && typeof result === 'object') {
  485. Object.keys(result).forEach((key) => {
  486. if (result[key] === undefined) delete result[key]
  487. })
  488. }
  489. this.$emit('input', result)
  490. this.$emit('update:modelValue', result)
  491. this.$emit('change', result)
  492. },
  493. openDatePopup() {
  494. this.popupType = 'date'
  495. this.showPopup = true
  496. this.initTempState()
  497. },
  498. openTimePopup() {
  499. this.popupType = 'time'
  500. this.showPopup = true
  501. this.initTempState()
  502. },
  503. openPopup(type = 'date') {
  504. if (type === 'time') {
  505. this.openTimePopup()
  506. } else {
  507. this.openDatePopup()
  508. }
  509. },
  510. open(type = 'date') {
  511. this.openPopup(type)
  512. },
  513. close() {
  514. this.closePopup()
  515. },
  516. closePopup() {
  517. this.showPopup = false
  518. },
  519. normalizeDisplayDate(year, month) {
  520. const minYear = this.minYear || 2025
  521. const maxYear = this.maxYear || new Date().getFullYear()
  522. let safeYear = year
  523. if (safeYear < minYear) safeYear = minYear
  524. if (safeYear > maxYear) safeYear = maxYear
  525. let safeMonth = month
  526. if (!safeMonth || safeMonth < 1 || safeMonth > 12) safeMonth = 1
  527. return { year: safeYear, month: safeMonth }
  528. },
  529. initTempState() {
  530. const currentValue = this.currentValue
  531. // 根据当前的 value 初始化 tempValue 和 displayYear/Month
  532. if (!currentValue) {
  533. this.tempValue = null
  534. this.currentMode = this.getDefaultMode() // 默认模式
  535. const now = new Date()
  536. const safeDate = this.normalizeDisplayDate(now.getFullYear(), now.getMonth() + 1)
  537. this.displayYear = safeDate.year
  538. this.displayMonth = safeDate.month
  539. this.tempWeekYear = safeDate.year
  540. return
  541. }
  542. // 初始化时间状态 (如果存在)
  543. if (typeof currentValue === 'object' && currentValue.time) {
  544. this.tempTimeValue = { ...currentValue.time }
  545. } else {
  546. this.tempTimeValue = { startTime: '00:00', endTime: '23:59' }
  547. }
  548. if (typeof currentValue === 'object') {
  549. if (currentValue.date) {
  550. this.currentMode = 'range'
  551. this.tempValue = { startDate: currentValue.date, endDate: '' }
  552. const [y, m] = currentValue.date.split('-')
  553. const safeDate = this.normalizeDisplayDate(parseInt(y), parseInt(m))
  554. this.displayYear = safeDate.year
  555. this.displayMonth = safeDate.month
  556. } else if (currentValue.startMonth || currentValue.endMonth || currentValue.month) {
  557. this.currentMode = 'month'
  558. const monthValue = currentValue.month || currentValue.startMonth || ''
  559. this.tempValue = {
  560. startMonth: monthValue,
  561. endMonth: currentValue.endMonth || ''
  562. }
  563. const [y] = (monthValue || '').split('-')
  564. if (y) {
  565. this.tempYear = this.normalizeDisplayDate(parseInt(y), 1).year
  566. }
  567. } else if (currentValue.startYear || currentValue.endYear) {
  568. this.currentMode = 'year'
  569. this.tempValue = {
  570. startYear: currentValue.startYear || '',
  571. endYear: currentValue.endYear || ''
  572. }
  573. this.tempYear = this.normalizeDisplayDate(parseInt(currentValue.startYear || currentValue.endYear || this.tempYear), 1).year
  574. } else if (currentValue.year) {
  575. this.currentMode = 'year'
  576. this.tempValue = currentValue.year
  577. this.tempYear = this.normalizeDisplayDate(parseInt(currentValue.year), 1).year
  578. } else if (currentValue.week || currentValue.startWeek || currentValue.endWeek) {
  579. this.currentMode = 'week'
  580. const weekValue = currentValue.week || currentValue.startWeek || ''
  581. const parsed = this.parseWeekValue(weekValue)
  582. this.tempWeekYear = parsed ? parsed.year : this.tempWeekYear
  583. const startParsed = this.parseWeekValue(currentValue.startWeek || currentValue.week)
  584. const endParsed = this.parseWeekValue(currentValue.endWeek || '')
  585. this.tempValue = {
  586. startWeek: startParsed ? startParsed.week : '',
  587. endWeek: endParsed ? endParsed.week : ''
  588. }
  589. } else {
  590. // 默认 Range
  591. this.currentMode = 'range'
  592. // 清理掉 time 属性,只保留 startDate/endDate
  593. const { time, ...rest } = currentValue
  594. this.tempValue = rest
  595. if (currentValue.startDate) {
  596. const [y, m] = currentValue.startDate.split('-')
  597. const safeDate = this.normalizeDisplayDate(parseInt(y), parseInt(m))
  598. this.displayYear = safeDate.year
  599. this.displayMonth = safeDate.month
  600. }
  601. }
  602. } else {
  603. // 字符串兼容 (虽然我们现在主要用对象)
  604. const val = currentValue.toString()
  605. if (val.length === 4) {
  606. this.currentMode = 'year'
  607. this.tempValue = val
  608. this.tempYear = this.normalizeDisplayDate(parseInt(val), 1).year
  609. } else if (val.includes('-') && val.length <= 7) {
  610. this.currentMode = 'month'
  611. this.tempValue = { startMonth: val, endMonth: '' }
  612. const [y] = val.split('-')
  613. this.tempYear = this.normalizeDisplayDate(parseInt(y), 1).year
  614. } else {
  615. this.currentMode = 'range'
  616. this.tempValue = { startDate: val, endDate: '' }
  617. if (val) {
  618. const [y, m] = val.split('-')
  619. const safeDate = this.normalizeDisplayDate(parseInt(y), parseInt(m))
  620. this.displayYear = safeDate.year
  621. this.displayMonth = safeDate.month
  622. }
  623. }
  624. }
  625. if (!this.isModeEnabled(this.currentMode)) {
  626. this.currentMode = this.getDefaultMode()
  627. this.tempValue = null
  628. }
  629. },
  630. switchMode(mode) {
  631. if (!this.isModeEnabled(mode)) return
  632. if (this.currentMode === mode) return
  633. this.currentMode = mode
  634. this.tempValue = null
  635. // 切换模式时,不重置时间选择,保持通用性
  636. // 重置显示时间
  637. const now = new Date()
  638. const safeDate = this.normalizeDisplayDate(now.getFullYear(), now.getMonth() + 1)
  639. this.displayYear = safeDate.year
  640. this.displayMonth = safeDate.month
  641. this.tempYear = safeDate.year
  642. if (mode === 'week') {
  643. this.tempWeekYear = safeDate.year
  644. }
  645. },
  646. getDefaultMode() {
  647. const order = ['year', 'month', 'range', 'week']
  648. return order.find((mode) => this.isModeEnabled(mode)) || 'range'
  649. },
  650. isModeEnabled(mode) {
  651. const map = this.enabledTypeMap
  652. if (mode === 'range') return map.date !== false
  653. return map[mode] !== false
  654. },
  655. changeMonth(delta) {
  656. let newMonth = this.displayMonth + delta
  657. let newYear = this.displayYear
  658. if (newMonth > 12) {
  659. newMonth = 1
  660. newYear++
  661. } else if (newMonth < 1) {
  662. newMonth = 12
  663. newYear--
  664. }
  665. // 限制年份范围
  666. const minYear = this.minYear || 2025
  667. const maxYear = this.maxYear || new Date().getFullYear()
  668. // 检查下限
  669. if (newYear < minYear) return
  670. // 检查上限
  671. if (newYear > maxYear) return
  672. this.displayMonth = newMonth
  673. this.displayYear = newYear
  674. },
  675. changeYear(delta) {
  676. const newYear = this.tempYear + delta
  677. const minYear = this.minYear || 2025
  678. const maxYear = this.maxYear || new Date().getFullYear()
  679. if (newYear >= minYear && newYear <= maxYear) {
  680. this.tempYear = newYear
  681. }
  682. },
  683. changeWeekYear(delta) {
  684. const newYear = this.tempWeekYear + delta
  685. const minYear = this.minYear || 2025
  686. const maxYear = this.maxYear || new Date().getFullYear()
  687. if (newYear < minYear || newYear > maxYear) return
  688. this.tempWeekYear = newYear
  689. if (this.tempValue && (this.tempValue.startWeek || this.tempValue.endWeek)) {
  690. this.tempValue = null
  691. }
  692. },
  693. isToday(day) {
  694. if (!day.fullDate) return false
  695. const now = new Date()
  696. const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
  697. return day.fullDate === todayStr
  698. },
  699. isSelected(day) {
  700. if (!day.fullDate) return false
  701. if (this.currentMode !== 'range' || !this.tempValue) return false
  702. return this.tempValue.startDate === day.fullDate && !this.tempValue.endDate
  703. },
  704. isRangeStart(day) {
  705. if (this.currentMode !== 'range' || !this.tempValue || !day.fullDate) return false
  706. return this.tempValue.startDate === day.fullDate
  707. },
  708. isRangeEnd(day) {
  709. if (this.currentMode !== 'range' || !this.tempValue || !day.fullDate) return false
  710. return this.tempValue.endDate === day.fullDate
  711. },
  712. isInRange(day) {
  713. if (this.currentMode !== 'range' || !this.tempValue || !day.fullDate) return false
  714. const { startDate, endDate } = this.tempValue
  715. if (!startDate || !endDate) return false
  716. return day.fullDate > startDate && day.fullDate < endDate
  717. },
  718. onDayClick(day) {
  719. if (!day.fullDate) return
  720. if (this.currentMode === 'range') {
  721. if (!this.tempValue || (this.tempValue.startDate && this.tempValue.endDate)) {
  722. // 开始新选择
  723. this.tempValue = { startDate: day.fullDate, endDate: '' }
  724. } else if (!this.tempValue.startDate) {
  725. this.tempValue = { startDate: day.fullDate, endDate: '' }
  726. } else {
  727. this.tempValue = { startDate: day.fullDate, endDate: '' }
  728. }
  729. }
  730. },
  731. formatDate(date, fmt) {
  732. const o = {
  733. "M+": date.getMonth() + 1,
  734. "d+": date.getDate(),
  735. "H+": date.getHours(),
  736. "m+": date.getMinutes(),
  737. "s+": date.getSeconds()
  738. }
  739. if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length))
  740. for (const k in o)
  741. if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)))
  742. return fmt
  743. },
  744. toMonthIndex(monthStr) {
  745. if (!monthStr) return 0
  746. const [y, m] = monthStr.split('-').map((v) => parseInt(v, 10))
  747. return y * 12 + m
  748. },
  749. isMonthSelected(m) {
  750. if (this.currentMode !== 'month' || !this.tempValue) return false
  751. const val = `${this.tempYear}-${String(m).padStart(2, '0')}`
  752. if (typeof this.tempValue === 'object') {
  753. const start = this.tempValue.startMonth
  754. const end = this.tempValue.endMonth
  755. if (start && end) {
  756. if (start === end) return val === start
  757. return false
  758. }
  759. if (start) return val === start
  760. return false
  761. }
  762. return this.tempValue === val
  763. },
  764. isMonthRangeStart(m) {
  765. if (this.currentMode !== 'month' || !this.tempValue || typeof this.tempValue !== 'object') return false
  766. const { startMonth, endMonth } = this.tempValue
  767. if (!startMonth || !endMonth) return false
  768. const val = `${this.tempYear}-${String(m).padStart(2, '0')}`
  769. return val === startMonth
  770. },
  771. isMonthRangeEnd(m) {
  772. if (this.currentMode !== 'month' || !this.tempValue || typeof this.tempValue !== 'object') return false
  773. const { startMonth, endMonth } = this.tempValue
  774. if (!startMonth || !endMonth) return false
  775. const val = `${this.tempYear}-${String(m).padStart(2, '0')}`
  776. return val === endMonth
  777. },
  778. isMonthInRange(m) {
  779. if (this.currentMode !== 'month' || !this.tempValue || typeof this.tempValue !== 'object') return false
  780. const { startMonth, endMonth } = this.tempValue
  781. if (!startMonth || !endMonth) return false
  782. const val = `${this.tempYear}-${String(m).padStart(2, '0')}`
  783. const idx = this.toMonthIndex(val)
  784. return idx > this.toMonthIndex(startMonth) && idx < this.toMonthIndex(endMonth)
  785. },
  786. onMonthSelect(m) {
  787. const val = `${this.tempYear}-${String(m).padStart(2, '0')}`
  788. if (!this.tempValue || (typeof this.tempValue === 'object' && this.tempValue.startMonth && this.tempValue.endMonth)) {
  789. this.tempValue = { startMonth: val, endMonth: '' }
  790. return
  791. }
  792. if (typeof this.tempValue === 'object') {
  793. const start = this.tempValue.startMonth
  794. if (!start) {
  795. this.tempValue = { startMonth: val, endMonth: '' }
  796. return
  797. }
  798. // const startIdx = this.toMonthIndex(start)
  799. // const valIdx = this.toMonthIndex(val)
  800. this.tempValue = { startMonth: val, endMonth: '' }
  801. return
  802. }
  803. this.tempValue = val
  804. },
  805. isYearSelected(y) {
  806. if (this.currentMode !== 'year' || !this.tempValue) return false
  807. if (typeof this.tempValue === 'object') {
  808. const start = parseInt(this.tempValue.startYear || 0)
  809. const end = parseInt(this.tempValue.endYear || 0)
  810. if (start && end) {
  811. if (start === end) return y === start
  812. return false
  813. }
  814. if (start) return y === start
  815. return false
  816. }
  817. return parseInt(this.tempValue) === y
  818. },
  819. isYearRangeStart(y) {
  820. if (this.currentMode !== 'year' || !this.tempValue || typeof this.tempValue !== 'object') return false
  821. const start = parseInt(this.tempValue.startYear || 0)
  822. const end = parseInt(this.tempValue.endYear || 0)
  823. if (!start || !end) return false
  824. return y === start
  825. },
  826. isYearRangeEnd(y) {
  827. if (this.currentMode !== 'year' || !this.tempValue || typeof this.tempValue !== 'object') return false
  828. const start = parseInt(this.tempValue.startYear || 0)
  829. const end = parseInt(this.tempValue.endYear || 0)
  830. if (!start || !end) return false
  831. return y === end
  832. },
  833. isYearInRange(y) {
  834. if (this.currentMode !== 'year' || !this.tempValue || typeof this.tempValue !== 'object') return false
  835. const start = parseInt(this.tempValue.startYear || 0)
  836. const end = parseInt(this.tempValue.endYear || 0)
  837. if (!start || !end) return false
  838. return y > start && y < end
  839. },
  840. onYearSelect(y) {
  841. if (!this.tempValue || (typeof this.tempValue === 'object' && this.tempValue.startYear && this.tempValue.endYear)) {
  842. this.tempValue = { startYear: y.toString(), endYear: '' }
  843. return
  844. }
  845. if (typeof this.tempValue === 'object') {
  846. const start = parseInt(this.tempValue.startYear || 0)
  847. if (!start) {
  848. this.tempValue = { startYear: y.toString(), endYear: '' }
  849. return
  850. }
  851. if (y < start) {
  852. this.tempValue = { startYear: y.toString(), endYear: start.toString() }
  853. } else if (y === start) {
  854. this.tempValue = { startYear: y.toString(), endYear: '' }
  855. } else {
  856. this.tempValue = { startYear: start.toString(), endYear: y.toString() }
  857. }
  858. return
  859. }
  860. this.tempValue = y.toString()
  861. },
  862. formatWeekValue(year, week) {
  863. return `${year}-W${String(week).padStart(2, '0')}`
  864. },
  865. parseWeekValue(value) {
  866. if (!value) return null
  867. const match = String(value).match(/^(\d{4})-W(\d{1,2})$/)
  868. if (!match) return null
  869. return { year: parseInt(match[1], 10), week: parseInt(match[2], 10) }
  870. },
  871. getISOWeekNumber(date) {
  872. const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
  873. const dayNum = d.getUTCDay() || 7
  874. d.setUTCDate(d.getUTCDate() + 4 - dayNum)
  875. const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
  876. return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
  877. },
  878. getWeeksInYear(year) {
  879. return this.getISOWeekNumber(new Date(year, 11, 28))
  880. },
  881. getWeekStartEnd(year, week) {
  882. const simple = new Date(Date.UTC(year, 0, 1 + (week - 1) * 7))
  883. const dayOfWeek = simple.getUTCDay() || 7
  884. const start = new Date(simple)
  885. start.setUTCDate(simple.getUTCDate() - (dayOfWeek - 1))
  886. const end = new Date(start)
  887. end.setUTCDate(start.getUTCDate() + 6)
  888. return {
  889. start: this.formatDate(new Date(start), 'yyyy-MM-dd'),
  890. end: this.formatDate(new Date(end), 'yyyy-MM-dd')
  891. }
  892. },
  893. isWeekSelected(week) {
  894. if (this.currentMode !== 'week' || !this.tempValue || !this.tempValue.startWeek) return false
  895. const start = parseInt(this.tempValue.startWeek, 10)
  896. const end = parseInt(this.tempValue.endWeek || 0, 10)
  897. if (start && end) {
  898. if (start === end) return week === start
  899. return false
  900. }
  901. return week === start
  902. },
  903. isWeekStart(week) {
  904. if (this.currentMode !== 'week' || !this.tempValue || !this.tempValue.startWeek) return false
  905. if (!this.tempValue.endWeek) return false
  906. return week === parseInt(this.tempValue.startWeek, 10)
  907. },
  908. isWeekEnd(week) {
  909. if (this.currentMode !== 'week' || !this.tempValue || !this.tempValue.endWeek) return false
  910. return week === parseInt(this.tempValue.endWeek, 10)
  911. },
  912. isWeekInRange(week) {
  913. if (this.currentMode !== 'week' || !this.tempValue) return false
  914. const start = parseInt(this.tempValue.startWeek || 0, 10)
  915. const end = parseInt(this.tempValue.endWeek || 0, 10)
  916. if (!start || !end) return false
  917. return week > start && week < end
  918. },
  919. onWeekSelect(week) {
  920. if (!this.tempValue || (this.tempValue.startWeek && this.tempValue.endWeek)) {
  921. this.tempValue = { startWeek: week, endWeek: '' }
  922. return
  923. }
  924. if (!this.tempValue.startWeek) {
  925. this.tempValue = { startWeek: week, endWeek: '' }
  926. return
  927. }
  928. const start = parseInt(this.tempValue.startWeek, 10)
  929. if (week < start) {
  930. this.tempValue = { startWeek: week, endWeek: start }
  931. } else if (week === start) {
  932. this.tempValue = { startWeek: week, endWeek: '' }
  933. } else {
  934. this.tempValue = { startWeek: start, endWeek: week }
  935. }
  936. },
  937. confirm() {
  938. // 如果是时间选择
  939. if (this.popupType === 'time') {
  940. if (!this.tempTimeValue.startTime) {
  941. uni.showToast({ title: '请选择开始时间', icon: 'none' })
  942. return
  943. }
  944. if (!this.tempTimeValue.endTime) {
  945. uni.showToast({ title: '请选择结束时间', icon: 'none' })
  946. return
  947. }
  948. if (this.tempTimeValue.startTime >= this.tempTimeValue.endTime) {
  949. uni.showToast({ title: '开始时间不能大于或等于结束时间', icon: 'none' })
  950. return
  951. }
  952. // 更新时间,保留日期
  953. const currentValue = this.currentValue
  954. let result = currentValue && typeof currentValue === 'object' ? { ...currentValue } : {}
  955. // 如果原来的 value 不是对象(是字符串),需要转换结构
  956. if (currentValue && typeof currentValue !== 'object') {
  957. // 尝试推断当前模式,或者重置日期?
  958. // 简单起见,如果之前是纯字符串,我们这里需要把它转成 date 对象
  959. const valStr = String(currentValue)
  960. if (valStr.length === 10) result = { date: valStr } // YYYY-MM-DD
  961. else if (valStr.length === 7) result = { month: valStr } // YYYY-MM
  962. else if (valStr.length === 4) result = { year: valStr } // YYYY
  963. }
  964. result.time = this.tempTimeValue
  965. this.emitValue(result)
  966. this.closePopup()
  967. return
  968. }
  969. // 如果是日期选择
  970. let result = null
  971. // 验证日期
  972. if (this.currentMode === 'range') {
  973. if (!this.tempValue || !this.tempValue.startDate) {
  974. uni.showToast({ title: '请选择日期', icon: 'none' })
  975. return
  976. }
  977. // 保留原有的时间选择 (如果有)
  978. const currentValue = this.currentValue
  979. const oldTime = currentValue && currentValue.time ? currentValue.time : null
  980. if (this.tempValue.endDate) {
  981. result = { ...this.tempValue }
  982. } else {
  983. result = { date: this.tempValue.startDate }
  984. }
  985. if (oldTime) result.time = oldTime
  986. }
  987. else if (this.currentMode === 'month') {
  988. if (!this.tempValue) {
  989. uni.showToast({ title: '请选择月份', icon: 'none' })
  990. return
  991. }
  992. const currentValue = this.currentValue
  993. const oldTime = currentValue && currentValue.time ? currentValue.time : null
  994. if (typeof this.tempValue === 'object') {
  995. const startMonth = this.tempValue.startMonth
  996. const endMonth = this.tempValue.endMonth
  997. if (startMonth && endMonth) {
  998. result = { startMonth, endMonth }
  999. } else if (startMonth) {
  1000. result = { month: startMonth }
  1001. } else {
  1002. uni.showToast({ title: '请选择月份', icon: 'none' })
  1003. return
  1004. }
  1005. } else {
  1006. result = { month: this.tempValue }
  1007. }
  1008. if (oldTime) result.time = oldTime
  1009. }
  1010. else if (this.currentMode === 'year') {
  1011. if (!this.tempValue) {
  1012. uni.showToast({ title: '请选择年份', icon: 'none' })
  1013. return
  1014. }
  1015. const currentValue = this.currentValue
  1016. const oldTime = currentValue && currentValue.time ? currentValue.time : null
  1017. if (typeof this.tempValue === 'object') {
  1018. const startYear = this.tempValue.startYear
  1019. const endYear = this.tempValue.endYear
  1020. if (startYear && endYear) {
  1021. result = { startYear, endYear }
  1022. } else if (startYear) {
  1023. result = { year: startYear }
  1024. } else {
  1025. uni.showToast({ title: '请选择年份', icon: 'none' })
  1026. return
  1027. }
  1028. } else {
  1029. result = { year: this.tempValue }
  1030. }
  1031. if (oldTime) result.time = oldTime
  1032. }
  1033. else if (this.currentMode === 'week') {
  1034. if (!this.tempValue || !this.tempValue.startWeek) {
  1035. uni.showToast({ title: '请选择起始周', icon: 'none' })
  1036. return
  1037. }
  1038. const currentValue = this.currentValue
  1039. const oldTime = currentValue && currentValue.time ? currentValue.time : null
  1040. const startWeek = this.formatWeekValue(this.tempWeekYear, this.tempValue.startWeek)
  1041. const startRange = this.getWeekStartEnd(this.tempWeekYear, this.tempValue.startWeek)
  1042. if (this.tempValue.endWeek) {
  1043. const endWeek = this.formatWeekValue(this.tempWeekYear, this.tempValue.endWeek)
  1044. const endRange = this.getWeekStartEnd(this.tempWeekYear, this.tempValue.endWeek)
  1045. result = { startWeek, endWeek, weekStartDate: startRange.start, weekEndDate: endRange.end }
  1046. } else {
  1047. result = { week: startWeek, weekStartDate: startRange.start, weekEndDate: startRange.end }
  1048. }
  1049. if (oldTime) result.time = oldTime
  1050. }
  1051. if (result) {
  1052. this.emitValue(result)
  1053. this.closePopup()
  1054. }
  1055. },
  1056. clearDate() {
  1057. // 清除日期,保留时间
  1058. const currentValue = this.currentValue
  1059. let result = currentValue && typeof currentValue === 'object' ? { ...currentValue } : {}
  1060. delete result.date
  1061. delete result.startDate
  1062. delete result.endDate
  1063. delete result.month
  1064. delete result.startMonth
  1065. delete result.endMonth
  1066. delete result.year
  1067. delete result.startYear
  1068. delete result.endYear
  1069. delete result.week
  1070. delete result.startWeek
  1071. delete result.endWeek
  1072. delete result.weekStartDate
  1073. delete result.weekEndDate
  1074. if (Object.keys(result).length === 0) {
  1075. this.emitValue(null)
  1076. return
  1077. }
  1078. this.emitValue(result)
  1079. },
  1080. clearTime() {
  1081. // 清除时间,保留日期
  1082. const currentValue = this.currentValue
  1083. if (!currentValue || typeof currentValue !== 'object') {
  1084. this.emitValue(null)
  1085. return
  1086. }
  1087. const result = { ...currentValue }
  1088. delete result.time
  1089. if (Object.keys(result).length === 0) {
  1090. this.emitValue(null)
  1091. return
  1092. }
  1093. this.emitValue(result)
  1094. },
  1095. // 时间筛选相关方法
  1096. isQuickTimeActive(type) {
  1097. if (type === 'all') return this.tempTimeValue.startTime === '00:00' && this.tempTimeValue.endTime === '23:59'
  1098. if (type === 'am') return this.tempTimeValue.startTime === '09:00' && this.tempTimeValue.endTime === '12:00'
  1099. if (type === 'pm') return this.tempTimeValue.startTime === '14:00' && this.tempTimeValue.endTime === '18:00'
  1100. return false
  1101. },
  1102. selectQuickTime(type) {
  1103. if (type === 'all') {
  1104. this.tempTimeValue = { startTime: '00:00', endTime: '23:59' }
  1105. } else if (type === 'am') {
  1106. this.tempTimeValue = { startTime: '09:00', endTime: '12:00' }
  1107. } else if (type === 'pm') {
  1108. this.tempTimeValue = { startTime: '14:00', endTime: '18:00' }
  1109. }
  1110. },
  1111. onTimeSelect(type, time) {
  1112. if (type === 'start') {
  1113. this.tempTimeValue.startTime = time
  1114. } else {
  1115. this.tempTimeValue.endTime = time
  1116. }
  1117. }
  1118. }
  1119. }
  1120. </script>
  1121. <style lang="scss" scoped>
  1122. .custom-date-picker {
  1123. width: 100%;
  1124. .picker-trigger {
  1125. height: 70rpx;
  1126. background-color: #fff;
  1127. border: 2rpx solid #e0e6ed;
  1128. border-radius: 8rpx;
  1129. padding: 0 20rpx;
  1130. display: flex;
  1131. align-items: center;
  1132. transition: all 0.3s;
  1133. &.time-trigger {
  1134. margin-top: 20rpx; // 上下两个触发器之间的间距
  1135. }
  1136. .text {
  1137. flex: 1;
  1138. font-size: 26rpx;
  1139. color: #333;
  1140. }
  1141. .arrow {
  1142. font-size: 20rpx;
  1143. color: #c0c4cc;
  1144. }
  1145. .clear-btn {
  1146. width: 36rpx;
  1147. height: 36rpx;
  1148. background: rgba(0,0,0,0.1);
  1149. border-radius: 50%;
  1150. display: flex;
  1151. align-items: center;
  1152. justify-content: center;
  1153. .clear-icon {
  1154. font-size: 24rpx;
  1155. color: #fff;
  1156. line-height: 1;
  1157. }
  1158. }
  1159. }
  1160. .picker-popup {
  1161. position: fixed;
  1162. top: 0;
  1163. left: 0;
  1164. width: 100%;
  1165. height: 100vh;
  1166. z-index: 99999;
  1167. visibility: hidden;
  1168. opacity: 0;
  1169. transition: all 0.3s;
  1170. &.show {
  1171. visibility: visible;
  1172. opacity: 1;
  1173. .popup-content {
  1174. transform: translateY(0);
  1175. }
  1176. }
  1177. .mask {
  1178. position: absolute;
  1179. top: 0;
  1180. left: 0;
  1181. width: 100%;
  1182. height: 100%;
  1183. background-color: rgba(0,0,0,0.5);
  1184. }
  1185. .popup-content {
  1186. position: absolute;
  1187. bottom: 0;
  1188. left: 0;
  1189. width: 100%;
  1190. background-color: #fff;
  1191. border-radius: 24rpx 24rpx 0 0;
  1192. transform: translateY(100%);
  1193. transition: all 0.3s;
  1194. padding-bottom: constant(safe-area-inset-bottom);
  1195. padding-bottom: env(safe-area-inset-bottom);
  1196. .popup-header {
  1197. display: flex;
  1198. align-items: center;
  1199. justify-content: space-between;
  1200. padding: 30rpx;
  1201. border-bottom: 1rpx solid #eee;
  1202. .btn {
  1203. font-size: 28rpx;
  1204. padding: 10rpx 20rpx;
  1205. &.cancel { color: #909399; }
  1206. &.confirm { color: #1890ff; font-weight: 500; }
  1207. }
  1208. .title {
  1209. font-size: 32rpx;
  1210. font-weight: 600;
  1211. color: #333;
  1212. }
  1213. }
  1214. .mode-tabs {
  1215. display: flex;
  1216. padding: 20rpx 30rpx 0;
  1217. gap: 20rpx;
  1218. .tab-item {
  1219. padding: 12rpx 24rpx;
  1220. background-color: #f5f7fa;
  1221. border-radius: 30rpx;
  1222. font-size: 26rpx;
  1223. color: #606266;
  1224. transition: all 0.3s;
  1225. &.active {
  1226. background-color: #1890ff;
  1227. color: #fff;
  1228. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
  1229. }
  1230. }
  1231. }
  1232. .picker-body {
  1233. padding: 30rpx;
  1234. min-height: 600rpx;
  1235. .calendar-header {
  1236. display: flex;
  1237. align-items: center;
  1238. justify-content: center;
  1239. margin-bottom: 30rpx;
  1240. .current-date-text {
  1241. font-size: 32rpx;
  1242. font-weight: 600;
  1243. margin: 0 40rpx;
  1244. color: #333;
  1245. }
  1246. .arrow-btn {
  1247. width: 60rpx;
  1248. height: 60rpx;
  1249. display: flex;
  1250. align-items: center;
  1251. justify-content: center;
  1252. font-size: 36rpx;
  1253. color: #606266;
  1254. background-color: #f5f7fa;
  1255. border-radius: 50%;
  1256. &:active {
  1257. background-color: #e6e6e6;
  1258. }
  1259. }
  1260. }
  1261. .week-header {
  1262. display: flex;
  1263. margin-bottom: 20rpx;
  1264. .week-item {
  1265. flex: 1;
  1266. text-align: center;
  1267. font-size: 24rpx;
  1268. color: #909399;
  1269. }
  1270. }
  1271. .range-tip {
  1272. margin-bottom: 16rpx;
  1273. font-size: 24rpx;
  1274. color: #909399;
  1275. text-align: center;
  1276. }
  1277. .calendar-body {
  1278. height: 500rpx;
  1279. .days-grid {
  1280. display: flex;
  1281. flex-wrap: wrap;
  1282. .day-item {
  1283. width: 14.28%;
  1284. height: 80rpx;
  1285. display: flex;
  1286. align-items: center;
  1287. justify-content: center;
  1288. font-size: 28rpx;
  1289. color: #333;
  1290. position: relative;
  1291. margin-bottom: 10rpx;
  1292. &.empty { pointer-events: none; }
  1293. &.disabled { opacity: 0.3; pointer-events: none; }
  1294. &.today {
  1295. color: #1890ff;
  1296. font-weight: 600;
  1297. &::after {
  1298. content: '';
  1299. position: absolute;
  1300. bottom: 10rpx;
  1301. left: 50%;
  1302. transform: translateX(-50%);
  1303. width: 8rpx;
  1304. height: 8rpx;
  1305. background-color: #1890ff;
  1306. border-radius: 50%;
  1307. }
  1308. }
  1309. // 选中样式
  1310. &.selected {
  1311. background-color: #1890ff;
  1312. color: #fff;
  1313. border-radius: 8rpx;
  1314. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
  1315. &::after { display: none; }
  1316. }
  1317. // 范围样式
  1318. &.range-start {
  1319. background-color: #1890ff;
  1320. color: #fff;
  1321. border-radius: 8rpx 0 0 8rpx;
  1322. &::after { display: none; }
  1323. }
  1324. &.range-end {
  1325. background-color: #1890ff;
  1326. color: #fff;
  1327. border-radius: 0 8rpx 8rpx 0;
  1328. &::after { display: none; }
  1329. }
  1330. &.in-range {
  1331. background-color: #e6f7ff;
  1332. color: #1890ff;
  1333. }
  1334. // 如果开始结束是同一天
  1335. &.range-start.range-end {
  1336. border-radius: 8rpx;
  1337. }
  1338. }
  1339. }
  1340. }
  1341. .list-body {
  1342. height: 600rpx;
  1343. .year-selector {
  1344. display: flex;
  1345. justify-content: center;
  1346. align-items: center;
  1347. margin-bottom: 30rpx;
  1348. .current-year {
  1349. font-size: 32rpx;
  1350. font-weight: 600;
  1351. margin: 0 30rpx;
  1352. }
  1353. .arrow-btn {
  1354. padding: 10rpx 20rpx;
  1355. font-size: 36rpx;
  1356. color: #606266;
  1357. }
  1358. }
  1359. .year-range-tip {
  1360. font-size: 24rpx;
  1361. color: #909399;
  1362. text-align: center;
  1363. margin-bottom: 16rpx;
  1364. }
  1365. .grid-container {
  1366. display: flex;
  1367. flex-wrap: wrap;
  1368. gap: 20rpx;
  1369. padding: 10rpx;
  1370. .grid-item {
  1371. width: calc(33.33% - 14rpx);
  1372. height: 80rpx;
  1373. display: flex;
  1374. align-items: center;
  1375. justify-content: center;
  1376. background-color: #f5f7fa;
  1377. border-radius: 12rpx;
  1378. font-size: 28rpx;
  1379. color: #606266;
  1380. &.selected {
  1381. background-color: #1890ff;
  1382. color: #fff;
  1383. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
  1384. }
  1385. &.in-range {
  1386. background-color: #e6f7ff;
  1387. color: #1890ff;
  1388. }
  1389. &.range-start,
  1390. &.range-end {
  1391. background-color: #1890ff;
  1392. color: #fff;
  1393. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
  1394. }
  1395. &.active {
  1396. opacity: 0.8;
  1397. }
  1398. }
  1399. }
  1400. }
  1401. .week-range-section {
  1402. .year-selector {
  1403. display: flex;
  1404. justify-content: center;
  1405. align-items: center;
  1406. margin-bottom: 30rpx;
  1407. .current-year {
  1408. font-size: 32rpx;
  1409. font-weight: 600;
  1410. margin: 0 30rpx;
  1411. }
  1412. .arrow-btn {
  1413. padding: 10rpx 20rpx;
  1414. font-size: 36rpx;
  1415. color: #606266;
  1416. }
  1417. }
  1418. .week-list {
  1419. height: 500rpx;
  1420. }
  1421. .week-grid {
  1422. .grid-item {
  1423. width: calc(25% - 16rpx);
  1424. &.in-range {
  1425. background-color: #e6f7ff;
  1426. color: #1890ff;
  1427. }
  1428. &.range-start,
  1429. &.range-end {
  1430. background-color: #1890ff;
  1431. color: #fff;
  1432. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
  1433. }
  1434. }
  1435. }
  1436. }
  1437. // 时间筛选样式 (在 date 模式下)
  1438. .time-filter-section {
  1439. margin-top: 30rpx;
  1440. .section-divider {
  1441. height: 1rpx;
  1442. background-color: #eee;
  1443. margin-bottom: 20rpx;
  1444. }
  1445. .time-header {
  1446. display: flex;
  1447. justify-content: space-between;
  1448. align-items: center;
  1449. margin-bottom: 20rpx;
  1450. .title {
  1451. font-size: 28rpx;
  1452. font-weight: 600;
  1453. color: #333;
  1454. }
  1455. .quick-btns {
  1456. display: flex;
  1457. gap: 15rpx;
  1458. .btn {
  1459. font-size: 24rpx;
  1460. color: #666;
  1461. background-color: #f5f7fa;
  1462. padding: 6rpx 20rpx;
  1463. border-radius: 24rpx;
  1464. &.active {
  1465. background-color: #1890ff;
  1466. color: #fff;
  1467. }
  1468. }
  1469. }
  1470. }
  1471. .time-selector-container {
  1472. border: 1rpx solid #e0e6ed;
  1473. border-radius: 12rpx;
  1474. background-color: #fff;
  1475. overflow: hidden;
  1476. .time-table-header {
  1477. display: flex;
  1478. align-items: center;
  1479. background-color: #f9f9f9;
  1480. border-bottom: 1rpx solid #eee;
  1481. height: 70rpx;
  1482. .th {
  1483. flex: 1;
  1484. text-align: center;
  1485. font-size: 24rpx;
  1486. color: #909399;
  1487. font-weight: 500;
  1488. }
  1489. .th-separator {
  1490. width: 1rpx;
  1491. height: 30rpx;
  1492. background-color: #e0e6ed;
  1493. }
  1494. }
  1495. .time-table-body {
  1496. display: flex;
  1497. height: 300rpx; // 降低一点高度
  1498. align-items: center;
  1499. .time-column {
  1500. flex: 1;
  1501. height: 100%;
  1502. .time-item {
  1503. height: 70rpx;
  1504. display: flex;
  1505. align-items: center;
  1506. justify-content: center;
  1507. font-size: 26rpx;
  1508. color: #333;
  1509. &.selected {
  1510. color: #1890ff;
  1511. font-weight: 600;
  1512. background-color: #f0f9ff;
  1513. }
  1514. }
  1515. }
  1516. .body-separator {
  1517. width: 60rpx;
  1518. text-align: center;
  1519. font-size: 24rpx;
  1520. color: #ccc;
  1521. }
  1522. }
  1523. }
  1524. }
  1525. }
  1526. }
  1527. }
  1528. }
  1529. </style>