Commit 9441e28e2c0927c2244657b7fb5eefca19dabefa
1 parent
3c3d6799
考勤打卡
Showing
14 changed files
with
3022 additions
and
782 deletions
api/index/index.js
| @@ -48,5 +48,27 @@ export const taskDetailsWithSmartSort = (params) => { | @@ -48,5 +48,27 @@ export const taskDetailsWithSmartSort = (params) => { | ||
| 48 | return post('/app-api/homePage/summary/taskDetailsWithSmartSort', params); | 48 | return post('/app-api/homePage/summary/taskDetailsWithSmartSort', params); |
| 49 | }; | 49 | }; |
| 50 | 50 | ||
| 51 | +/** | ||
| 52 | + * 组织 | ||
| 53 | + * @param {Object} params {mobile, password, code} | ||
| 54 | + * @returns {Promise} | ||
| 55 | + */ | ||
| 56 | +export const getCurrentOrg = (params) => { | ||
| 57 | + return get('/app-api/homePage/summary/currentOrg', params); | ||
| 58 | +}; | ||
| 59 | + | ||
| 60 | +/** | ||
| 61 | + * 组织 | ||
| 62 | + * @param {Object} params {mobile, password, code} | ||
| 63 | + * @returns {Promise} | ||
| 64 | + */ | ||
| 65 | +export const orgTaskIndicator = (params) => { | ||
| 66 | + return get('/app-api/homePage/summary/orgTaskIndicator', params); | ||
| 67 | +}; | ||
| 68 | + | ||
| 69 | + | ||
| 70 | + | ||
| 71 | + | ||
| 72 | + | ||
| 51 | 73 | ||
| 52 | 74 |
manifest.json
pages-sub/daily/attendanceRecord.vue
0 → 100644
| 1 | +<template> | ||
| 2 | +<view class="attendance-page"> | ||
| 3 | + <!-- 顶部统计卡片 --> | ||
| 4 | + <view class="stat-card"> | ||
| 5 | + <view class="stat-item"> | ||
| 6 | + <view class="stat-label">本月打卡</view> | ||
| 7 | + <view class="stat-value">{{ statData.checkInDays }}</view> | ||
| 8 | + </view> | ||
| 9 | + <view class="stat-item"> | ||
| 10 | + <view class="stat-label">正常天数</view> | ||
| 11 | + <view class="stat-value">{{ statData.normalDays }}</view> | ||
| 12 | + </view> | ||
| 13 | + <view class="stat-item"> | ||
| 14 | + <view class="stat-label">异常天数</view> | ||
| 15 | + <view class="stat-value">{{ statData.errorDays }}</view> | ||
| 16 | + </view> | ||
| 17 | + </view> | ||
| 18 | + | ||
| 19 | + <!-- 手写内联日历,无弹窗 --> | ||
| 20 | + <view class="calendar-wrap"> | ||
| 21 | + <view class="calendar-header"> | ||
| 22 | + <!-- 左右横向排列:上一月 | 年月标题 | 下一月 --> | ||
| 23 | + <view class="calendar-btn" @click="prevMonth">上一月</view> | ||
| 24 | + <view class="calendar-title">{{ currentYear }}年{{ currentMonth }}月</view> | ||
| 25 | + <!-- 当前是最新月份时,隐藏下个月按钮 --> | ||
| 26 | + <view v-if="canNext" class="calendar-btn" @click="nextMonth">下一月</view> | ||
| 27 | + <view v-else class="calendar-btn empty-btn"></view> | ||
| 28 | + </view> | ||
| 29 | + <!-- 星期头部 --> | ||
| 30 | + <view class="week-row"> | ||
| 31 | + <view class="week-item" v-for="w in weekList" :key="w">{{ w }}</view> | ||
| 32 | + </view> | ||
| 33 | + <!-- 日期网格 --> | ||
| 34 | + <view class="day-row" v-for="(row, rIdx) in dayRows" :key="rIdx"> | ||
| 35 | + <view | ||
| 36 | + class="day-item" | ||
| 37 | + v-for="(day, dIdx) in row" | ||
| 38 | + :key="dIdx" | ||
| 39 | + :class="{ | ||
| 40 | + 'day-item--other': day.otherMonth, | ||
| 41 | + 'day-item--active': day.dateStr === selectDate, | ||
| 42 | + }" | ||
| 43 | + @click="onDayClick(day)" | ||
| 44 | + > | ||
| 45 | + <view class="day-text">{{ day.day }}</view> | ||
| 46 | + <view v-if="day.dot" class="day-dot" :style="{backgroundColor: day.dotColor}"></view> | ||
| 47 | + </view> | ||
| 48 | + </view> | ||
| 49 | + </view> | ||
| 50 | + | ||
| 51 | + <!-- 打卡详情区域 --> | ||
| 52 | + <view class="detail-card"> | ||
| 53 | + <view v-if="!currentAttendance"> | ||
| 54 | + <view class="empty-text">暂无考勤记录</view> | ||
| 55 | + </view> | ||
| 56 | + <view v-else> | ||
| 57 | + <view class="detail-row"> | ||
| 58 | + <view class="detail-title">上班打卡时间:</view> | ||
| 59 | + <view class="detail-text">{{ currentAttendance.clockInTime || '缺少打卡记录' }}</view> | ||
| 60 | + </view> | ||
| 61 | + <view class="detail-row"> | ||
| 62 | + <view class="detail-title">上班打卡地点:</view> | ||
| 63 | + <view class="detail-text">{{ currentAttendance.clockInAddress || '-' }}</view> | ||
| 64 | + </view> | ||
| 65 | + <view class="divider"></view> | ||
| 66 | + <view class="detail-row"> | ||
| 67 | + <view class="detail-title">下班打卡时间:</view> | ||
| 68 | + <view class="detail-text">{{ currentAttendance.clockOutTime || '缺少打卡记录' }}</view> | ||
| 69 | + </view> | ||
| 70 | + <view class="detail-row"> | ||
| 71 | + <view class="detail-title">下班打卡地点:</view> | ||
| 72 | + <view class="detail-text">{{ currentAttendance.clockOutAddress || '-' }}</view> | ||
| 73 | + </view> | ||
| 74 | + </view> | ||
| 75 | + </view> | ||
| 76 | +</view> | ||
| 77 | +</template> | ||
| 78 | + | ||
| 79 | +<script setup lang="ts"> | ||
| 80 | +import { ref, onMounted, computed } from 'vue'; | ||
| 81 | + | ||
| 82 | +const weekList = ['日','一','二','三','四','五','六']; | ||
| 83 | +const today = new Date(); | ||
| 84 | +const todayTs = today.getTime(); | ||
| 85 | +// 当前显示月份 | ||
| 86 | +const currentYear = ref(today.getFullYear()); | ||
| 87 | +const currentMonth = ref(today.getMonth() + 1); | ||
| 88 | +const selectDate = ref(''); | ||
| 89 | +const dayRows = ref<any[]>([]); | ||
| 90 | + | ||
| 91 | +// 是否允许点击下一月:判断当前展示月份是否小于本月 | ||
| 92 | +const canNext = computed(() => { | ||
| 93 | + const showMonthDate = new Date(currentYear.value, currentMonth.value -1, 1); | ||
| 94 | + const nowMonthDate = new Date(today.getFullYear(), today.getMonth(), 1); | ||
| 95 | + return showMonthDate < nowMonthDate; | ||
| 96 | +}); | ||
| 97 | + | ||
| 98 | +// ========== 写死静态统计数据 ========== | ||
| 99 | +const statData = ref({ | ||
| 100 | + checkInDays: 28, | ||
| 101 | + normalDays: 25, | ||
| 102 | + errorDays: 3 | ||
| 103 | +}); | ||
| 104 | + | ||
| 105 | +// ========== 写死日历标记:红点error异常,灰点normal正常 ========== | ||
| 106 | +const markMap = ref<Record<string, {type:'error'|'normal'}>>({ | ||
| 107 | + '2026-10-02': { type: 'error' }, | ||
| 108 | + '2026-10-10': { type: 'error' }, | ||
| 109 | + '2026-10-16': { type: 'error' }, | ||
| 110 | + '2026-10-05': { type: 'normal' }, | ||
| 111 | + '2026-10-12': { type: 'normal' }, | ||
| 112 | +}); | ||
| 113 | + | ||
| 114 | +// ========== 写死每日考勤详情数据 ========== | ||
| 115 | +const dayAttendanceMap = ref<Record<string, any>>({ | ||
| 116 | + '2026-10-02': { | ||
| 117 | + clockInTime: '2026-10-02 08:02:10', | ||
| 118 | + clockInAddress: '月坛南街安华大厦', | ||
| 119 | + clockOutTime: '', | ||
| 120 | + clockOutAddress: '' | ||
| 121 | + }, | ||
| 122 | + '2026-10-13': { | ||
| 123 | + clockInTime: '2026-10-13 07:58:25', | ||
| 124 | + clockInAddress: '月坛南街安华大厦', | ||
| 125 | + clockOutTime: '', | ||
| 126 | + clockOutAddress: '南礼士地铁站' | ||
| 127 | + }, | ||
| 128 | + '2026-10-05': { | ||
| 129 | + clockInTime: '2026-10-05 08:00:00', | ||
| 130 | + clockInAddress: '月坛南街安华大厦', | ||
| 131 | + clockOutTime: '2026-10-05 18:00:00', | ||
| 132 | + clockOutAddress: '月坛南街安华大厦' | ||
| 133 | + }, | ||
| 134 | + '2026-10-10': null, | ||
| 135 | + '2026-10-12': { | ||
| 136 | + clockInTime: '2026-10-12 07:55:10', | ||
| 137 | + clockInAddress: '月坛南街安华大厦', | ||
| 138 | + clockOutTime: '2026-10-12 17:50:30', | ||
| 139 | + clockOutAddress: '月坛南街安华大厦' | ||
| 140 | + }, | ||
| 141 | + '2026-10-16': { | ||
| 142 | + clockInTime: '2026-10-16 07:59:22', | ||
| 143 | + clockInAddress: '月坛南街安华大厦', | ||
| 144 | + clockOutTime: '', | ||
| 145 | + clockOutAddress: '' | ||
| 146 | + } | ||
| 147 | +}); | ||
| 148 | +const currentAttendance = ref<any>(null); | ||
| 149 | + | ||
| 150 | +// 生成月份日期网格 | ||
| 151 | +const renderMonth = () => { | ||
| 152 | + const year = currentYear.value; | ||
| 153 | + const month = currentMonth.value; | ||
| 154 | + // 当月第一天 | ||
| 155 | + const firstDay = new Date(year, month - 1, 1); | ||
| 156 | + // 当月最后一天 | ||
| 157 | + const lastDay = new Date(year, month, 0); | ||
| 158 | + // 上个月最后一天 | ||
| 159 | + const prevLastDay = new Date(year, month - 1, 0).getDate(); | ||
| 160 | + // 当月第一天星期 0周日~6周六 | ||
| 161 | + const firstWeek = firstDay.getDay(); | ||
| 162 | + | ||
| 163 | + const rows: any[] = []; | ||
| 164 | + let row: any[] = []; | ||
| 165 | + // 填充上月尾部 | ||
| 166 | + for(let i = 0; i < firstWeek; i++){ | ||
| 167 | + const dayNum = prevLastDay - firstWeek + i + 1; | ||
| 168 | + row.push({ | ||
| 169 | + day: dayNum, | ||
| 170 | + otherMonth: true, | ||
| 171 | + dateStr: `${year}-${String(month-1).padStart(2,'0')}-${String(dayNum).padStart(2,'0')}` | ||
| 172 | + }) | ||
| 173 | + } | ||
| 174 | + // 填充当月日期 | ||
| 175 | + for(let d = 1; d <= lastDay.getDate(); d++){ | ||
| 176 | + const dateStr = `${year}-${String(month).padStart(2,'0')}-${String(d).padStart(2,'0')}`; | ||
| 177 | + const mark = markMap.value[dateStr]; | ||
| 178 | + const item:any = { | ||
| 179 | + day: d, | ||
| 180 | + otherMonth: false, | ||
| 181 | + dateStr, | ||
| 182 | + dot: false, | ||
| 183 | + dotColor: '' | ||
| 184 | + }; | ||
| 185 | + if(mark){ | ||
| 186 | + item.dot = true; | ||
| 187 | + item.dotColor = mark.type === 'error' ? '#f56c6c' : '#cccccc'; | ||
| 188 | + } | ||
| 189 | + row.push(item); | ||
| 190 | + if(row.length ===7){ | ||
| 191 | + rows.push(row); | ||
| 192 | + row = []; | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + // 填充下月开头 | ||
| 196 | + let nextDay = 1; | ||
| 197 | + while(row.length <7){ | ||
| 198 | + row.push({ | ||
| 199 | + day: nextDay, | ||
| 200 | + otherMonth:true, | ||
| 201 | + dateStr: `${year}-${String(month+1).padStart(2,'0')}-${String(nextDay).padStart(2,'0')}` | ||
| 202 | + }) | ||
| 203 | + nextDay++; | ||
| 204 | + } | ||
| 205 | + rows.push(row); | ||
| 206 | + dayRows.value = rows; | ||
| 207 | +}; | ||
| 208 | + | ||
| 209 | +// 上一月 | ||
| 210 | +const prevMonth = () => { | ||
| 211 | + if(currentMonth.value === 1){ | ||
| 212 | + currentYear.value--; | ||
| 213 | + currentMonth.value = 12; | ||
| 214 | + }else{ | ||
| 215 | + currentMonth.value--; | ||
| 216 | + } | ||
| 217 | + renderMonth(); | ||
| 218 | +}; | ||
| 219 | +// 下一月 | ||
| 220 | +const nextMonth = () => { | ||
| 221 | + let nextY = currentYear.value; | ||
| 222 | + let nextM = currentMonth.value + 1; | ||
| 223 | + if(nextM > 12){ | ||
| 224 | + nextY++; | ||
| 225 | + nextM =1; | ||
| 226 | + } | ||
| 227 | + currentYear.value = nextY; | ||
| 228 | + currentMonth.value = nextM; | ||
| 229 | + renderMonth(); | ||
| 230 | +}; | ||
| 231 | + | ||
| 232 | +// 点击日期 | ||
| 233 | +const onDayClick = (day:any) => { | ||
| 234 | + // 禁止点击其他月份 | ||
| 235 | + if(day.otherMonth) return; | ||
| 236 | + const selectDay = new Date(day.dateStr); | ||
| 237 | + if(selectDay > today){ | ||
| 238 | + currentAttendance.value = null; | ||
| 239 | + return; | ||
| 240 | + } | ||
| 241 | + selectDate.value = day.dateStr; | ||
| 242 | + const data = dayAttendanceMap.value[day.dateStr]; | ||
| 243 | + currentAttendance.value = data ?? null; | ||
| 244 | +}; | ||
| 245 | + | ||
| 246 | +onMounted(() => { | ||
| 247 | + const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`; | ||
| 248 | + selectDate.value = todayStr; | ||
| 249 | + fetchDayAttendance(todayStr); | ||
| 250 | + renderMonth(); | ||
| 251 | +}); | ||
| 252 | + | ||
| 253 | +const fetchDayAttendance = (date:string) => { | ||
| 254 | + const selectDay = new Date(date); | ||
| 255 | + if(selectDay > today){ | ||
| 256 | + currentAttendance.value = null; | ||
| 257 | + return; | ||
| 258 | + } | ||
| 259 | + const data = dayAttendanceMap.value[date]; | ||
| 260 | + currentAttendance.value = data ?? null; | ||
| 261 | +}; | ||
| 262 | +</script> | ||
| 263 | + | ||
| 264 | +<style scoped lang="scss"> | ||
| 265 | +.attendance-page{ | ||
| 266 | + padding: 20rpx; | ||
| 267 | + background-color: #f5f5f7; | ||
| 268 | + min-height: 100vh; | ||
| 269 | +} | ||
| 270 | +.stat-card{ | ||
| 271 | + display: flex; | ||
| 272 | + background-color: #3c9cff; | ||
| 273 | + border-radius: 16rpx; | ||
| 274 | + padding:30rpx 0; | ||
| 275 | + margin-bottom:20rpx; | ||
| 276 | + .stat-item{ | ||
| 277 | + flex:1; | ||
| 278 | + text-align:center; | ||
| 279 | + color:#fff; | ||
| 280 | + .stat-label{ | ||
| 281 | + font-size:28rpx; | ||
| 282 | + opacity:0.9; | ||
| 283 | + } | ||
| 284 | + .stat-value{ | ||
| 285 | + font-size:36rpx; | ||
| 286 | + margin-top:10rpx; | ||
| 287 | + font-weight:bold; | ||
| 288 | + } | ||
| 289 | + } | ||
| 290 | +} | ||
| 291 | +.calendar-wrap{ | ||
| 292 | + background:#fff; | ||
| 293 | + border-radius:12rpx; | ||
| 294 | + padding:20rpx; | ||
| 295 | + margin-bottom:20rpx; | ||
| 296 | +} | ||
| 297 | +.calendar-header{ | ||
| 298 | + display:flex; | ||
| 299 | + justify-content: space-between; | ||
| 300 | + align-items:center; | ||
| 301 | + margin-bottom:20rpx; | ||
| 302 | +} | ||
| 303 | +.calendar-title{ | ||
| 304 | + font-size:32rpx; | ||
| 305 | + font-weight:bold; | ||
| 306 | +} | ||
| 307 | +.calendar-btn{ | ||
| 308 | + padding:8rpx 16rpx; | ||
| 309 | + border:1rpx solid #eee; | ||
| 310 | + border-radius:8rpx; | ||
| 311 | + font-size:26rpx; | ||
| 312 | +} | ||
| 313 | +.empty-btn{ | ||
| 314 | + border-color:transparent; | ||
| 315 | + color:transparent; | ||
| 316 | +} | ||
| 317 | +.week-row{ | ||
| 318 | + display:flex; | ||
| 319 | + .week-item{ | ||
| 320 | + flex:1; | ||
| 321 | + text-align:center; | ||
| 322 | + font-size:26rpx; | ||
| 323 | + color:#666; | ||
| 324 | + padding:10rpx 0; | ||
| 325 | + } | ||
| 326 | +} | ||
| 327 | +.day-row{ | ||
| 328 | + display:flex; | ||
| 329 | + .day-item{ | ||
| 330 | + flex:1; | ||
| 331 | + min-height:70rpx; | ||
| 332 | + display:flex; | ||
| 333 | + flex-direction:column; | ||
| 334 | + align-items:center; | ||
| 335 | + justify-content:center; | ||
| 336 | + padding:8rpx 0; | ||
| 337 | + position:relative; | ||
| 338 | + .day-text{ | ||
| 339 | + font-size:28rpx; | ||
| 340 | + } | ||
| 341 | + .day-dot{ | ||
| 342 | + width:10rpx; | ||
| 343 | + height:10rpx; | ||
| 344 | + border-radius:50%; | ||
| 345 | + margin-top:4rpx; | ||
| 346 | + } | ||
| 347 | + } | ||
| 348 | + .day-item--other{ | ||
| 349 | + color:#ccc; | ||
| 350 | + } | ||
| 351 | + .day-item--active{ | ||
| 352 | + background:#3c9cff; | ||
| 353 | + color:#fff; | ||
| 354 | + border-radius:50%; | ||
| 355 | + } | ||
| 356 | +} | ||
| 357 | +.detail-card{ | ||
| 358 | + background:#fff; | ||
| 359 | + border-radius:12rpx; | ||
| 360 | + padding:30rpx; | ||
| 361 | + .empty-text{ | ||
| 362 | + text-align:center; | ||
| 363 | + color:#999; | ||
| 364 | + font-size:30rpx; | ||
| 365 | + padding:40rpx 0; | ||
| 366 | + } | ||
| 367 | + .detail-row{ | ||
| 368 | + margin-bottom:20rpx; | ||
| 369 | + display:flex; | ||
| 370 | + .detail-title{ | ||
| 371 | + font-size:30rpx; | ||
| 372 | + color:#333; | ||
| 373 | + width:220rpx; | ||
| 374 | + } | ||
| 375 | + .detail-text{ | ||
| 376 | + flex:1; | ||
| 377 | + font-size:30rpx; | ||
| 378 | + color:#666; | ||
| 379 | + } | ||
| 380 | + } | ||
| 381 | + .divider{ | ||
| 382 | + height:1px; | ||
| 383 | + background:#eee; | ||
| 384 | + margin:30rpx 0; | ||
| 385 | + } | ||
| 386 | +} | ||
| 387 | +</style> |
pages-sub/problem/work-order-manage/add-order.vue
| @@ -2,22 +2,19 @@ | @@ -2,22 +2,19 @@ | ||
| 2 | <view class="page-container"> | 2 | <view class="page-container"> |
| 3 | <view class="work-order-form-content commonPageLRpadding"> | 3 | <view class="work-order-form-content commonPageLRpadding"> |
| 4 | <up-form label-position="left" :model="workOrderForm" ref="workOrderFormRef" labelWidth="200rpx"> | 4 | <up-form label-position="left" :model="workOrderForm" ref="workOrderFormRef" labelWidth="200rpx"> |
| 5 | - <!-- 业务线单选框:移入form内,作为第一个表单项(核心调整) --> | 5 | + <!-- 业务线单选框 --> |
| 6 | <up-form-item label="业务线" prop="busiLineCn" border-bottom required> | 6 | <up-form-item label="业务线" prop="busiLineCn" border-bottom required> |
| 7 | <up-radio-group v-model="workOrderForm.busiLineCn" placement="row" @change="handleBusiLineChange"> | 7 | <up-radio-group v-model="workOrderForm.busiLineCn" placement="row" @change="handleBusiLineChange"> |
| 8 | <up-radio v-for="item in busiLineOptions" :key="item.name" :label="item.name" :name="item.name"></up-radio> | 8 | <up-radio v-for="item in busiLineOptions" :key="item.name" :label="item.name" :name="item.name"></up-radio> |
| 9 | </up-radio-group> | 9 | </up-radio-group> |
| 10 | - | ||
| 11 | </up-form-item> | 10 | </up-form-item> |
| 12 | - | ||
| 13 | <!-- 1. 工单位置(地图选择) --> | 11 | <!-- 1. 工单位置(地图选择) --> |
| 14 | <up-form-item label="工单位置" prop="workLocation" border-bottom required | 12 | <up-form-item label="工单位置" prop="workLocation" border-bottom required |
| 15 | @click="chooseWorkLocation(); hideKeyboard()"> | 13 | @click="chooseWorkLocation(); hideKeyboard()"> |
| 16 | <up-input v-model="workOrderForm.workLocation" border="none" readonly suffix-icon="map-fill" | 14 | <up-input v-model="workOrderForm.workLocation" border="none" readonly suffix-icon="map-fill" |
| 17 | placeholder="点击选择工单位置"></up-input> | 15 | placeholder="点击选择工单位置"></up-input> |
| 18 | </up-form-item> | 16 | </up-form-item> |
| 19 | - | ||
| 20 | - <!-- 2. 道路名称(下拉框) --> | 17 | + <!-- 2. 道路名称(下拉框action-sheet,保留不变) --> |
| 21 | <up-form-item label="道路名称" prop="roadName" border-bottom required | 18 | <up-form-item label="道路名称" prop="roadName" border-bottom required |
| 22 | @click="handleActionSheetOpen('roadName'); hideKeyboard()"> | 19 | @click="handleActionSheetOpen('roadName'); hideKeyboard()"> |
| 23 | <up-input v-model="workOrderForm.roadName" readonly disabled-color="#ffffff" placeholder="请先选择工单位置" | 20 | <up-input v-model="workOrderForm.roadName" readonly disabled-color="#ffffff" placeholder="请先选择工单位置" |
| @@ -26,17 +23,14 @@ | @@ -26,17 +23,14 @@ | ||
| 26 | <up-icon name="arrow-right" size="16"></up-icon> | 23 | <up-icon name="arrow-right" size="16"></up-icon> |
| 27 | </template> | 24 | </template> |
| 28 | </up-form-item> | 25 | </up-form-item> |
| 29 | - | ||
| 30 | - <!-- 3. 问题类型(下拉框) --> | ||
| 31 | - <up-form-item label="问题类型" prop="orderName" border-bottom required | ||
| 32 | - @click="handleActionSheetOpen('orderName'); hideKeyboard()"> | 26 | + <!-- 3. 问题类型 级联选择器【对齐参考页面】 --> |
| 27 | + <up-form-item label="问题类型" prop="orderName" border-bottom required @click="openCascader(); hideKeyboard()"> | ||
| 33 | <up-input v-model="workOrderForm.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型" | 28 | <up-input v-model="workOrderForm.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型" |
| 34 | border="none"></up-input> | 29 | border="none"></up-input> |
| 35 | <template #right> | 30 | <template #right> |
| 36 | <up-icon name="arrow-right" size="16"></up-icon> | 31 | <up-icon name="arrow-right" size="16"></up-icon> |
| 37 | </template> | 32 | </template> |
| 38 | </up-form-item> | 33 | </up-form-item> |
| 39 | - | ||
| 40 | <!-- 新增:紧急程度选择 --> | 34 | <!-- 新增:紧急程度选择 --> |
| 41 | <up-form-item label="紧急程度" prop="pressingTypeName" border-bottom required | 35 | <up-form-item label="紧急程度" prop="pressingTypeName" border-bottom required |
| 42 | @click="handleActionSheetOpen('pressingType'); hideKeyboard()"> | 36 | @click="handleActionSheetOpen('pressingType'); hideKeyboard()"> |
| @@ -46,61 +40,60 @@ | @@ -46,61 +40,60 @@ | ||
| 46 | <up-icon name="arrow-right" size="16"></up-icon> | 40 | <up-icon name="arrow-right" size="16"></up-icon> |
| 47 | </template> | 41 | </template> |
| 48 | </up-form-item> | 42 | </up-form-item> |
| 49 | - | ||
| 50 | <!-- 4. 情况描述(文本域) --> | 43 | <!-- 4. 情况描述(文本域) --> |
| 51 | <up-form-item label="情况描述" prop="problemDesc" required> | 44 | <up-form-item label="情况描述" prop="problemDesc" required> |
| 52 | <up-textarea placeholder="请输入情况描述(最多200字)" v-model.trim="workOrderForm.problemDesc" count maxlength="200" | 45 | <up-textarea placeholder="请输入情况描述(最多200字)" v-model.trim="workOrderForm.problemDesc" count maxlength="200" |
| 53 | - rows="4" @blur="() => workOrderFormRef.validateField('problemDesc')"></up-textarea> | 46 | + rows="4" @blur="() => workOrderFormRef.value?.validateField('problemDesc')"></up-textarea> |
| 54 | </up-form-item> | 47 | </up-form-item> |
| 55 | - | ||
| 56 | - <!-- 问题照片(核心修复:绑定纯数组) --> | 48 | + <!-- 问题照片 --> |
| 57 | <up-form-item label="问题照片" prop="problemImgs" border-bottom required> | 49 | <up-form-item label="问题照片" prop="problemImgs" border-bottom required> |
| 58 | <up-upload :file-list="problemImgs.imgList.value || []" @after-read="problemImgs.uploadImgs" | 50 | <up-upload :file-list="problemImgs.imgList.value || []" @after-read="problemImgs.uploadImgs" |
| 59 | @delete="problemImgs.deleteImg" multiple :width="70" :height="70" | 51 | @delete="problemImgs.deleteImg" multiple :width="70" :height="70" |
| 60 | :max-count="problemImgs.uploadConfig.maxCount" :upload-text="problemImgs.uploadConfig.uploadText" | 52 | :max-count="problemImgs.uploadConfig.maxCount" :upload-text="problemImgs.uploadConfig.uploadText" |
| 61 | :size-type="problemImgs.uploadConfig.sizeType"></up-upload> | 53 | :size-type="problemImgs.uploadConfig.sizeType"></up-upload> |
| 62 | </up-form-item> | 54 | </up-form-item> |
| 63 | - | ||
| 64 | <!-- 完成时间 --> | 55 | <!-- 完成时间 --> |
| 65 | <up-form-item label="希望完成时间" prop="expectedFinishDate" @click="show = true; hideKeyboard()"> | 56 | <up-form-item label="希望完成时间" prop="expectedFinishDate" @click="show = true; hideKeyboard()"> |
| 66 | <up-input v-model="workOrderForm.expectedFinishDate" border="none" readonly placeholder="点击选择时间"></up-input> | 57 | <up-input v-model="workOrderForm.expectedFinishDate" border="none" readonly placeholder="点击选择时间"></up-input> |
| 67 | <template #right> | 58 | <template #right> |
| 68 | - <!-- 核心修复:用view包裹图标,添加双重@click.stop --> | ||
| 69 | <view v-if="workOrderForm.expectedFinishDate" @click.stop> | 59 | <view v-if="workOrderForm.expectedFinishDate" @click.stop> |
| 70 | <up-icon name="close" size="20" @click.stop="clearExpectedFinishDate"></up-icon> | 60 | <up-icon name="close" size="20" @click.stop="clearExpectedFinishDate"></up-icon> |
| 71 | </view> | 61 | </view> |
| 72 | <up-icon name="arrow-right" size="16" v-else></up-icon> | 62 | <up-icon name="arrow-right" size="16" v-else></up-icon> |
| 73 | </template> | 63 | </template> |
| 74 | </up-form-item> | 64 | </up-form-item> |
| 75 | - | ||
| 76 | -<!-- 修改后 --> | ||
| 77 | -<up-form-item label="重点巡检区域" prop="keyInspection" border-bottom required> | ||
| 78 | - <up-radio-group v-model="workOrderForm.keyInspection" @change="groupChange"> | ||
| 79 | - <up-radio | ||
| 80 | - :customStyle="{marginBottom: '8px'}" | ||
| 81 | - v-for="(item, index) in keyInspectionOptions" | ||
| 82 | - :key="index" | ||
| 83 | - :label="item.name" | ||
| 84 | - :name="item.name" | ||
| 85 | - > | ||
| 86 | - </up-radio> | ||
| 87 | - </up-radio-group> | ||
| 88 | -</up-form-item> | ||
| 89 | - | ||
| 90 | - | 65 | + <!-- 重点巡检区域 --> |
| 66 | + <up-form-item label="重点巡检区域" prop="keyInspection" border-bottom required> | ||
| 67 | + <up-radio-group v-model="workOrderForm.keyInspection" @change="groupChange"> | ||
| 68 | + <up-radio | ||
| 69 | + :customStyle="{marginBottom: '8px'}" | ||
| 70 | + v-for="(item, index) in keyInspectionOptions" | ||
| 71 | + :key="index" | ||
| 72 | + :label="item.name" | ||
| 73 | + :name="item.name" | ||
| 74 | + > | ||
| 75 | + </up-radio> | ||
| 76 | + </up-radio-group> | ||
| 77 | + </up-form-item> | ||
| 91 | </up-form> | 78 | </up-form> |
| 92 | </view> | 79 | </view> |
| 93 | - | ||
| 94 | <!-- 底部提交按钮 --> | 80 | <!-- 底部提交按钮 --> |
| 95 | <view class="fixed-bottom-btn-wrap"> | 81 | <view class="fixed-bottom-btn-wrap"> |
| 96 | <up-button type="primary" text="提交工单" @click="submitWorkOrder"></up-button> | 82 | <up-button type="primary" text="提交工单" @click="submitWorkOrder"></up-button> |
| 97 | </view> | 83 | </view> |
| 98 | - | ||
| 99 | - <!-- 合并后的通用下拉弹窗 --> | 84 | + <!-- 通用下拉弹窗:道路名称、紧急程度保留 --> |
| 100 | <up-action-sheet :show="showActionSheet" :actions="currentActionSheetData.list" | 85 | <up-action-sheet :show="showActionSheet" :actions="currentActionSheetData.list" |
| 101 | :title="currentActionSheetData.title" @close="handleActionSheetClose" | 86 | :title="currentActionSheetData.title" @close="handleActionSheetClose" |
| 102 | @select="handleActionSheetSelect"></up-action-sheet> | 87 | @select="handleActionSheetSelect"></up-action-sheet> |
| 103 | - | 88 | + <!-- 级联选择器 up-cascader【对齐参考页面写法】 --> |
| 89 | + <up-cascader | ||
| 90 | + v-model:show="showCascader" | ||
| 91 | + v-model="cascaderValue" | ||
| 92 | + :data="cascaderOptions" | ||
| 93 | + title="选择问题类型" | ||
| 94 | + :auto-close="false" | ||
| 95 | + @confirm="cascaderConfirm" | ||
| 96 | + ></up-cascader> | ||
| 104 | <!-- 完成时间选择器 --> | 97 | <!-- 完成时间选择器 --> |
| 105 | <up-datetime-picker :show="show" v-model="expectedFinishDate" mode="datetime" :min-date="new Date()" | 98 | <up-datetime-picker :show="show" v-model="expectedFinishDate" mode="datetime" :min-date="new Date()" |
| 106 | @cancel="show = false" @confirm="expectedFinishDateConfirm"></up-datetime-picker> | 99 | @cancel="show = false" @confirm="expectedFinishDateConfirm"></up-datetime-picker> |
| @@ -111,74 +104,37 @@ | @@ -111,74 +104,37 @@ | ||
| 111 | import { ref, reactive } from 'vue' | 104 | import { ref, reactive } from 'vue' |
| 112 | import { onReady, onShow, onLoad } from '@dcloudio/uni-app'; | 105 | import { onReady, onShow, onLoad } from '@dcloudio/uni-app'; |
| 113 | import { useUploadImgs } from '@/common/utils/useUploadImgs' | 106 | import { useUploadImgs } from '@/common/utils/useUploadImgs' |
| 114 | -import { getRoadListByLatLng } from '@/api/common' | 107 | +import { getRoadListByLatLng, getProblemListByType } from '@/api/common' |
| 115 | import { universalApproval, workorderCreate } from '@/api/work-order-manage/work-order-manage' | 108 | import { universalApproval, workorderCreate } from '@/api/work-order-manage/work-order-manage' |
| 116 | import { timeFormat } from '@/uni_modules/uview-plus' | 109 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 117 | import { nextStepMap } from '@/common/utils/common' | 110 | import { nextStepMap } from '@/common/utils/common' |
| 118 | import { useUserStore } from '@/pinia/user'; | 111 | import { useUserStore } from '@/pinia/user'; |
| 119 | import { getDictLabel, getDictValue } from '@/common/utils/dict'; | 112 | import { getDictLabel, getDictValue } from '@/common/utils/dict'; |
| 120 | - | ||
| 121 | -// ========== 状态管理 ========== | ||
| 122 | const userStore = useUserStore(); | 113 | const userStore = useUserStore(); |
| 123 | - | ||
| 124 | -// ========== 业务线相关状态(核心改造:已整合到workOrderForm中,删除冗余selectedBusiLine) ========== | ||
| 125 | -// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码 | ||
| 126 | const BUSI_LINE_DICT_TYPE = 'business_line'; | 114 | const BUSI_LINE_DICT_TYPE = 'business_line'; |
| 127 | -// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」 | ||
| 128 | const CJK_RE = /[一-龥]/; | 115 | const CJK_RE = /[一-龥]/; |
| 129 | - | ||
| 130 | -// 业务线选项列表(仅保留name字段=中文名称,适配单选框配置) | ||
| 131 | const busiLineOptions = ref([]); | 116 | const busiLineOptions = ref([]); |
| 132 | -// 格式化业务线选项(兼容1个/2个/3个的情况,生成中文名称列表) | ||
| 133 | const formatBusiLineOptions = () => { | 117 | const formatBusiLineOptions = () => { |
| 134 | if (!userStore.userInfo?.user?.busiLine) { | 118 | if (!userStore.userInfo?.user?.busiLine) { |
| 135 | busiLineOptions.value = []; | 119 | busiLineOptions.value = []; |
| 136 | return; | 120 | return; |
| 137 | } | 121 | } |
| 138 | - const rawBusiLines = userStore.userInfo.user.busiLine.split(','); // 拆分字符串为数组 ["yl", "sz", "wy"] | ||
| 139 | - console.log(rawBusiLines) | ||
| 140 | - // 核心改造:仅生成name字段(值为中文名称),适配单选框:label="item.name" | 122 | + const rawBusiLines = userStore.userInfo.user.busiLine.split(','); |
| 141 | busiLineOptions.value = rawBusiLines.map(item => ({ | 123 | busiLineOptions.value = rawBusiLines.map(item => ({ |
| 142 | - name: getBusiLineCnByEn(item.trim()) // name=中文名称(园林/市政/物业) | 124 | + name: getBusiLineCnByEn(item.trim()) |
| 143 | })); | 125 | })); |
| 144 | - console.log(busiLineOptions.value) | ||
| 145 | }; | 126 | }; |
| 146 | - | ||
| 147 | - | ||
| 148 | const keyInspectionOptions = reactive([ | 127 | const keyInspectionOptions = reactive([ |
| 149 | - { | ||
| 150 | - name: '否', | ||
| 151 | - disabled: false, | ||
| 152 | - }, | ||
| 153 | - { | ||
| 154 | - name: '是', | ||
| 155 | - disabled: false, | ||
| 156 | - }, | ||
| 157 | - | 128 | + { name: '否', disabled: false }, |
| 129 | + { name: '是', disabled: false }, | ||
| 158 | ]); | 130 | ]); |
| 159 | - | ||
| 160 | -// up-radio-group的v-model绑定的值如果设置为某个radio的name,就会被默认选中 | ||
| 161 | -const radiovalue1 = ref('否'); | ||
| 162 | - | ||
| 163 | const groupChange = (n) => { | 131 | const groupChange = (n) => { |
| 164 | console.log('groupChange', n); | 132 | console.log('groupChange', n); |
| 165 | }; | 133 | }; |
| 166 | - | ||
| 167 | -const radioChange = (n) => { | ||
| 168 | - console.log('radioChange', n); | ||
| 169 | -}; | ||
| 170 | - | ||
| 171 | -// ========== 工具方法 ========== | ||
| 172 | -// 编码 → 中文名称。字典未拉到时(登录时字典请求是容错的)兜底显示原始编码, | ||
| 173 | -// 保证选项不为空、页面仍可用 | ||
| 174 | const getBusiLineCnByEn = (busiLineEn) => { | 134 | const getBusiLineCnByEn = (busiLineEn) => { |
| 175 | if (!busiLineEn) return ''; | 135 | if (!busiLineEn) return ''; |
| 176 | return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; | 136 | return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; |
| 177 | }; | 137 | }; |
| 178 | - | ||
| 179 | -// ========== 工具方法:通过中文名称获取对应的英文标识 ========== | ||
| 180 | -// 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回; | ||
| 181 | -// 中文名解析不到仍返回空串,沿用调用方原有的异常提示 | ||
| 182 | const getBusiLineEnByCn = (cnName) => { | 138 | const getBusiLineEnByCn = (cnName) => { |
| 183 | if (!cnName) return ''; | 139 | if (!cnName) return ''; |
| 184 | const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); | 140 | const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); |
| @@ -186,10 +142,75 @@ const getBusiLineEnByCn = (cnName) => { | @@ -186,10 +142,75 @@ const getBusiLineEnByCn = (cnName) => { | ||
| 186 | return CJK_RE.test(cnName) ? '' : cnName; | 142 | return CJK_RE.test(cnName) ? '' : cnName; |
| 187 | }; | 143 | }; |
| 188 | 144 | ||
| 189 | -// ========== 表单Ref ========== | ||
| 190 | -const workOrderFormRef = ref(null) | 145 | +// ========== 级联选择器变量(和参考页面对齐) ========== |
| 146 | +const showCascader = ref(false); | ||
| 147 | +const cascaderValue = ref([]); | ||
| 148 | +const cascaderOptions = ref([]); | ||
| 191 | 149 | ||
| 192 | -// ========== 公共上传逻辑复用 ========== | 150 | +// 打开级联弹窗 |
| 151 | +const openCascader = () => { | ||
| 152 | + console.log('312321dasd') | ||
| 153 | + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn) | ||
| 154 | + if (!busiLineEn) { | ||
| 155 | + uni.showToast({ title: '请先选择业务线', icon: 'none' }) | ||
| 156 | + return | ||
| 157 | + } | ||
| 158 | + if (!cascaderOptions.value || cascaderOptions.value.length === 0) { | ||
| 159 | + uni.showToast({ title: '正在加载问题类型,请稍等', icon: 'none' }); | ||
| 160 | + loadProblemCascader(busiLineEn); | ||
| 161 | + return; | ||
| 162 | + } | ||
| 163 | + cascaderValue.value = []; | ||
| 164 | + showCascader.value = true; | ||
| 165 | +}; | ||
| 166 | + | ||
| 167 | +/** | ||
| 168 | + * 级联【确认回调】,选中后点确定才赋值,和参考页面保持一致 | ||
| 169 | + */ | ||
| 170 | +const cascaderConfirm = (selectedArr) => { | ||
| 171 | + console.log('最终选中值数组:', selectedArr); | ||
| 172 | + // 递归拼接多级label,用 / 分隔 | ||
| 173 | + const getLabelByValueList = (list, valList) => { | ||
| 174 | + const labelArr = []; | ||
| 175 | + let currentList = list; | ||
| 176 | + for (const val of valList) { | ||
| 177 | + const target = currentList.find(item => item.value === val); | ||
| 178 | + if (!target) break; | ||
| 179 | + labelArr.push(target.label); | ||
| 180 | + currentList = target.children || []; | ||
| 181 | + } | ||
| 182 | + return labelArr.join('/'); | ||
| 183 | + }; | ||
| 184 | + const fullLabel = getLabelByValueList(cascaderOptions.value, selectedArr); | ||
| 185 | + workOrderForm.orderName = fullLabel; | ||
| 186 | + workOrderForm.orderCode = selectedArr.at(-1); | ||
| 187 | + cascaderValue.value = selectedArr; | ||
| 188 | + workOrderFormRef.value?.validateField('orderName'); | ||
| 189 | +}; | ||
| 190 | + | ||
| 191 | +/** | ||
| 192 | + * 加载级联树形数据 | ||
| 193 | + */ | ||
| 194 | +const loadProblemCascader = async (parentCode) => { | ||
| 195 | + if (!parentCode) { | ||
| 196 | + cascaderOptions.value = [] | ||
| 197 | + return | ||
| 198 | + } | ||
| 199 | + try { | ||
| 200 | + uni.showLoading({ title: '加载类型中...' }) | ||
| 201 | + const res = await getProblemListByType({ parentCode }) | ||
| 202 | + uni.hideLoading() | ||
| 203 | + cascaderOptions.value = res || [] | ||
| 204 | + } catch (err) { | ||
| 205 | + uni.hideLoading() | ||
| 206 | + console.error('获取问题类型失败', err) | ||
| 207 | + uni.showToast({ title: '获取问题类型失败', icon: 'none' }) | ||
| 208 | + cascaderOptions.value = [] | ||
| 209 | + } | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +// 表单Ref | ||
| 213 | +const workOrderFormRef = ref(null) | ||
| 193 | const problemImgs = useUploadImgs({ | 214 | const problemImgs = useUploadImgs({ |
| 194 | maxCount: 3, | 215 | maxCount: 3, |
| 195 | uploadText: '选择问题照片', | 216 | uploadText: '选择问题照片', |
| @@ -197,53 +218,41 @@ const problemImgs = useUploadImgs({ | @@ -197,53 +218,41 @@ const problemImgs = useUploadImgs({ | ||
| 197 | formRef: workOrderFormRef, | 218 | formRef: workOrderFormRef, |
| 198 | fieldName: 'problemImgs' | 219 | fieldName: 'problemImgs' |
| 199 | }) | 220 | }) |
| 200 | - | ||
| 201 | -// 无需再手动初始化普通数组,直接使用 ref 响应式数据 | ||
| 202 | if (!Array.isArray(problemImgs.rawImgList.value)) { | 221 | if (!Array.isArray(problemImgs.rawImgList.value)) { |
| 203 | problemImgs.rawImgList.value = []; | 222 | problemImgs.rawImgList.value = []; |
| 204 | } | 223 | } |
| 205 | - | ||
| 206 | -// ========== 页面状态 ========== | ||
| 207 | -// 通用弹窗控制 | ||
| 208 | const showActionSheet = ref(false) | 224 | const showActionSheet = ref(false) |
| 209 | -// 当前弹窗配置 | ||
| 210 | const currentActionSheetData = reactive({ | 225 | const currentActionSheetData = reactive({ |
| 211 | type: '', | 226 | type: '', |
| 212 | list: [], | 227 | list: [], |
| 213 | title: '' | 228 | title: '' |
| 214 | }) | 229 | }) |
| 215 | -// 完成时间选择器控制 | ||
| 216 | const show = ref(false) | 230 | const show = ref(false) |
| 217 | const expectedFinishDate = ref(Date.now()) | 231 | const expectedFinishDate = ref(Date.now()) |
| 218 | - | ||
| 219 | -// ========== 重新提交相关状态(核心:本地存储读取) ========== | ||
| 220 | -const isRenew = ref(false); // 是否为重新提交状态 | ||
| 221 | -const renewOrderData = ref(null); // 重新提交的原有工单数据 | ||
| 222 | - | ||
| 223 | -// ========== 下拉列表数据 ========== | 232 | +const isRenew = ref(false); |
| 233 | +const renewOrderData = ref(null); | ||
| 224 | const roadNameList = ref([]) | 234 | const roadNameList = ref([]) |
| 225 | -const orderNameList = ref([]) | ||
| 226 | const pressingTypeList = ref([]) | 235 | const pressingTypeList = ref([]) |
| 227 | 236 | ||
| 228 | -// ========== 工单表单数据(核心调整:新增busiLineCn字段,绑定业务线) ========== | 237 | +// ========== 表单数据 ========== |
| 229 | const workOrderForm = reactive({ | 238 | const workOrderForm = reactive({ |
| 230 | - busiLineCn: '', // 新增:业务线中文名称(用于form绑定、校验、回显) | ||
| 231 | - roadId: 0, // 原有字段 | ||
| 232 | - roadName: '', // 原有字段 | ||
| 233 | - workLocation: '', // 原有字段 | ||
| 234 | - orderName: '', // 原有字段 | ||
| 235 | - pressingType: '', // 原有字段 | ||
| 236 | - pressingTypeName: '', // 原有字段 | ||
| 237 | - problemDesc: '', // 原有字段 | ||
| 238 | - lat: 0, // 原有字段 | ||
| 239 | - lon: 0, // 原有字段 | ||
| 240 | - expectedFinishDate: '', // 原有字段 | 239 | + busiLineCn: '', |
| 240 | + roadId: 0, | ||
| 241 | + roadName: '', | ||
| 242 | + workLocation: '', | ||
| 243 | + orderName: '', | ||
| 244 | + orderCode: '', | ||
| 245 | + pressingType: '', | ||
| 246 | + pressingTypeName: '', | ||
| 247 | + problemDesc: '', | ||
| 248 | + lat: 0, | ||
| 249 | + lon: 0, | ||
| 250 | + expectedFinishDate: '', | ||
| 241 | keyInspection: '否' | 251 | keyInspection: '否' |
| 242 | }) | 252 | }) |
| 243 | - | ||
| 244 | -// ========== 表单校验规则(核心调整:新增busiLineCn校验规则) ========== | 253 | +// 表单校验规则:prop改成 orderName,对齐参考页面 |
| 245 | const workOrderFormRules = reactive({ | 254 | const workOrderFormRules = reactive({ |
| 246 | - busiLineCn: [ // 新增:业务线必选校验 | 255 | + busiLineCn: [ |
| 247 | { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] } | 256 | { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] } |
| 248 | ], | 257 | ], |
| 249 | workLocation: [ | 258 | workLocation: [ |
| @@ -253,7 +262,7 @@ const workOrderFormRules = reactive({ | @@ -253,7 +262,7 @@ const workOrderFormRules = reactive({ | ||
| 253 | { type: 'string', required: true, message: '请选择道路名称', trigger: ['change', 'blur'] } | 262 | { type: 'string', required: true, message: '请选择道路名称', trigger: ['change', 'blur'] } |
| 254 | ], | 263 | ], |
| 255 | orderName: [ | 264 | orderName: [ |
| 256 | - { type: 'string', required: true, message: '请选择问题类型', trigger: ['change', 'blur'] } | 265 | + { type: 'string', required: true, message: '请选择问题类型', trigger: 'change' } |
| 257 | ], | 266 | ], |
| 258 | pressingTypeName: [ | 267 | pressingTypeName: [ |
| 259 | { type: 'string', required: true, message: '请选择紧急程度', trigger: ['change'] } | 268 | { type: 'string', required: true, message: '请选择紧急程度', trigger: ['change'] } |
| @@ -262,38 +271,27 @@ const workOrderFormRules = reactive({ | @@ -262,38 +271,27 @@ const workOrderFormRules = reactive({ | ||
| 262 | { type: 'string', required: true, message: '请输入情况描述', trigger: ['change', 'blur'] }, | 271 | { type: 'string', required: true, message: '请输入情况描述', trigger: ['change', 'blur'] }, |
| 263 | { type: 'string', min: 3, max: 200, message: '情况描述需3-200字', trigger: ['change', 'blur'] } | 272 | { type: 'string', min: 3, max: 200, message: '情况描述需3-200字', trigger: ['change', 'blur'] } |
| 264 | ], | 273 | ], |
| 265 | - problemImgs: [problemImgs.imgValidateRule] // 复用上传校验规则 | 274 | + problemImgs: [problemImgs.imgValidateRule] |
| 266 | }) | 275 | }) |
| 267 | 276 | ||
| 268 | -// ========== 生命周期 ========== | ||
| 269 | -// 页面加载:读取本地存储的工单数据(适配workOrderForm.busiLineCn) | ||
| 270 | onLoad((options) => { | 277 | onLoad((options) => { |
| 271 | - // 初始化业务线选项 | ||
| 272 | formatBusiLineOptions(); | 278 | formatBusiLineOptions(); |
| 273 | - // 默认选中第一个业务线(赋值给workOrderForm.busiLineCn,核心调整) | ||
| 274 | if (busiLineOptions.value.length > 0) { | 279 | if (busiLineOptions.value.length > 0) { |
| 275 | workOrderForm.busiLineCn = busiLineOptions.value[0].name; | 280 | workOrderForm.busiLineCn = busiLineOptions.value[0].name; |
| 281 | + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn) | ||
| 282 | + loadProblemCascader(busiLineEn) | ||
| 276 | } | 283 | } |
| 277 | - | ||
| 278 | - // 判断是否为重新提交状态 | ||
| 279 | console.log(options) | 284 | console.log(options) |
| 280 | if (options.isRenew == 1 && options.tempKey) { | 285 | if (options.isRenew == 1 && options.tempKey) { |
| 281 | isRenew.value = true; | 286 | isRenew.value = true; |
| 282 | const tempKey = options.tempKey; | 287 | const tempKey = options.tempKey; |
| 283 | - | ||
| 284 | - // 1. 从本地同步存储中读取完整工单数据 | ||
| 285 | try { | 288 | try { |
| 286 | const orderData = uni.getStorageSync(tempKey); | 289 | const orderData = uni.getStorageSync(tempKey); |
| 287 | if (orderData && typeof orderData === 'object') { | 290 | if (orderData && typeof orderData === 'object') { |
| 288 | renewOrderData.value = orderData; | 291 | renewOrderData.value = orderData; |
| 289 | - | ||
| 290 | - console.log('123213') | ||
| 291 | - console.log(orderData) | ||
| 292 | - // 2. 回显工单数据到表单 | ||
| 293 | echoOrderData(renewOrderData.value); | 292 | echoOrderData(renewOrderData.value); |
| 294 | } else { | 293 | } else { |
| 295 | uni.showToast({ title: '工单数据不存在,无法重新提交', icon: 'none' }); | 294 | uni.showToast({ title: '工单数据不存在,无法重新提交', icon: 'none' }); |
| 296 | - // 跳转回列表页 | ||
| 297 | setTimeout(() => uni.navigateBack(), 1000); | 295 | setTimeout(() => uni.navigateBack(), 1000); |
| 298 | return; | 296 | return; |
| 299 | } | 297 | } |
| @@ -303,106 +301,114 @@ onLoad((options) => { | @@ -303,106 +301,114 @@ onLoad((options) => { | ||
| 303 | setTimeout(() => uni.navigateBack(), 1000); | 301 | setTimeout(() => uni.navigateBack(), 1000); |
| 304 | return; | 302 | return; |
| 305 | } finally { | 303 | } finally { |
| 306 | - // 3. 关键:用完即删,避免本地冗余存储和数据泄露 | ||
| 307 | uni.removeStorageSync(tempKey); | 304 | uni.removeStorageSync(tempKey); |
| 308 | } | 305 | } |
| 309 | } | 306 | } |
| 310 | }); | 307 | }); |
| 311 | - | ||
| 312 | onReady(() => { | 308 | onReady(() => { |
| 313 | - // 设置表单校验规则 | ||
| 314 | if (workOrderFormRef.value) { | 309 | if (workOrderFormRef.value) { |
| 315 | workOrderFormRef.value.setRules(workOrderFormRules) | 310 | workOrderFormRef.value.setRules(workOrderFormRules) |
| 316 | } | 311 | } |
| 317 | console.log('工单表单规则初始化完成') | 312 | console.log('工单表单规则初始化完成') |
| 318 | }) | 313 | }) |
| 319 | - | ||
| 320 | onShow(() => { | 314 | onShow(() => { |
| 321 | - console.log(uni.$dict.getDictLabel('ai_image_status', 20)) | ||
| 322 | - console.log(uni.$dict.getDictSimpleList('work_name')) | ||
| 323 | - | ||
| 324 | - // 初始化问题类型列表 | ||
| 325 | - orderNameList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('work_name')) | ||
| 326 | - console.log('问题类型列表:', orderNameList.value) | ||
| 327 | - | ||
| 328 | - // 初始化紧急程度列表 | ||
| 329 | pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('workorder_pressing_type')) | 315 | pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('workorder_pressing_type')) |
| 330 | console.log('紧急程度列表:', pressingTypeList.value) | 316 | console.log('紧急程度列表:', pressingTypeList.value) |
| 331 | }) | 317 | }) |
| 332 | 318 | ||
| 333 | -// ========== 核心方法:工单数据回显(适配workOrderForm.busiLineCn) ========== | ||
| 334 | -const echoOrderData = (orderItem) => { | ||
| 335 | - // 1. 回显业务线:将工单的英文busiLine转为中文名称,赋值给workOrderForm.busiLineCn(核心调整) | ||
| 336 | - if (orderItem.busiLine) { | ||
| 337 | - workOrderForm.busiLineCn = getBusiLineCnByEn(orderItem.busiLine); // 英文->中文(如wy->物业) | 319 | +// 回显工单数据【重点对齐参考页面的递归查找级联路径】 |
| 320 | +const echoOrderData = async (orderItem) => { | ||
| 321 | + const busiCode = orderItem.busiLine || ''; | ||
| 322 | + if (busiCode) { | ||
| 323 | + workOrderForm.busiLineCn = getBusiLineCnByEn(busiCode); | ||
| 338 | } | 324 | } |
| 339 | - | ||
| 340 | - // 2. 原有基础表单字段回显 | ||
| 341 | workOrderForm.roadId = orderItem.roadId || 0; | 325 | workOrderForm.roadId = orderItem.roadId || 0; |
| 342 | workOrderForm.roadName = orderItem.roadName || ''; | 326 | workOrderForm.roadName = orderItem.roadName || ''; |
| 343 | workOrderForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || ''; | 327 | workOrderForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || ''; |
| 344 | workOrderForm.orderName = orderItem.orderName || ''; | 328 | workOrderForm.orderName = orderItem.orderName || ''; |
| 329 | + workOrderForm.orderCode = orderItem.orderCode || ''; | ||
| 345 | workOrderForm.pressingType = orderItem.pressingType || ''; | 330 | workOrderForm.pressingType = orderItem.pressingType || ''; |
| 346 | workOrderForm.pressingTypeName = uni.$dict.getDictLabel('workorder_pressing_type', orderItem.pressingType) || ''; | 331 | workOrderForm.pressingTypeName = uni.$dict.getDictLabel('workorder_pressing_type', orderItem.pressingType) || ''; |
| 347 | workOrderForm.problemDesc = orderItem.remark || ''; | 332 | workOrderForm.problemDesc = orderItem.remark || ''; |
| 348 | workOrderForm.lat = orderItem.lat || 0; | 333 | workOrderForm.lat = orderItem.lat || 0; |
| 349 | workOrderForm.lon = orderItem.lon || 0; | 334 | workOrderForm.lon = orderItem.lon || 0; |
| 350 | workOrderForm.expectedFinishDate = orderItem.expectedFinishDate ? timeFormat(orderItem.expectedFinishDate, 'yyyy-mm-dd hh:MM:ss') : ''; | 335 | workOrderForm.expectedFinishDate = orderItem.expectedFinishDate ? timeFormat(orderItem.expectedFinishDate, 'yyyy-mm-dd hh:MM:ss') : ''; |
| 351 | -if (orderItem.keyInspection !== undefined) { | ||
| 352 | - workOrderForm.keyInspection = orderItem.keyInspection === 1 ? '是' : '否' | ||
| 353 | -} | ||
| 354 | - // 3. 上传图片回显(兼容useUploadImgs格式) | 336 | + if (orderItem.keyInspection !== undefined) { |
| 337 | + workOrderForm.keyInspection = orderItem.keyInspection === 1 ? '是' : '否' | ||
| 338 | + } | ||
| 355 | if (orderItem.problemsImgs && Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length > 0) { | 339 | if (orderItem.problemsImgs && Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length > 0) { |
| 356 | const imgList = orderItem.problemsImgs.map((imgUrl, index) => ({ | 340 | const imgList = orderItem.problemsImgs.map((imgUrl, index) => ({ |
| 357 | url: imgUrl, | 341 | url: imgUrl, |
| 358 | name: `renew_img_${index}`, | 342 | name: `renew_img_${index}`, |
| 359 | - status: 'success' // 标记为已上传状态 | 343 | + status: 'success' |
| 360 | })); | 344 | })); |
| 361 | problemImgs.imgList.value = imgList; | 345 | problemImgs.imgList.value = imgList; |
| 362 | problemImgs.rawImgList.value = imgList; | 346 | problemImgs.rawImgList.value = imgList; |
| 363 | } | 347 | } |
| 364 | 348 | ||
| 365 | - // 4. 自动获取道路列表(修改:传递当前选中的业务线英文标识) | 349 | + // ========= 回显核心:加载级联树,递归找到选中路径,赋值 cascaderValue ========= |
| 350 | + if (busiCode) { | ||
| 351 | + await loadProblemCascader(busiCode); | ||
| 352 | + const targetCode = orderItem.orderCode | ||
| 353 | + if (targetCode && cascaderOptions.value.length) { | ||
| 354 | + const findCascaderPath = (list, targetVal, path = []) => { | ||
| 355 | + for (const node of list) { | ||
| 356 | + const newPath = [...path, node.value] | ||
| 357 | + if (node.value === targetVal) { | ||
| 358 | + return newPath | ||
| 359 | + } | ||
| 360 | + if (node.children && node.children.length) { | ||
| 361 | + const res = findCascaderPath(node.children, targetVal, newPath) | ||
| 362 | + if (res) return res | ||
| 363 | + } | ||
| 364 | + } | ||
| 365 | + return null | ||
| 366 | + } | ||
| 367 | + const selectPath = findCascaderPath(cascaderOptions.value, targetCode) | ||
| 368 | + if (selectPath) { | ||
| 369 | + cascaderValue.value = selectPath | ||
| 370 | + } | ||
| 371 | + } | ||
| 372 | + } | ||
| 373 | + | ||
| 366 | if (orderItem.lat && orderItem.lon) { | 374 | if (orderItem.lat && orderItem.lon) { |
| 367 | - getRoadListByBusiLine(); // 改用封装后的方法,自动携带选中的业务线 | 375 | + getRoadListByBusiLine(); |
| 368 | } | 376 | } |
| 369 | }; | 377 | }; |
| 370 | 378 | ||
| 371 | -// ========== 业务线相关方法(适配workOrderForm.busiLineCn) ========== | ||
| 372 | /** | 379 | /** |
| 373 | - * 业务线切换事件:清空道路信息 + 重新请求道路列表 | 380 | + * 业务线切换:清空道路、问题类型级联数据,重新加载级联树 |
| 374 | */ | 381 | */ |
| 375 | const handleBusiLineChange = () => { | 382 | const handleBusiLineChange = () => { |
| 376 | - // 1. 清空已渲染的道路名称和表单字段 | 383 | + // 清空道路 |
| 377 | workOrderForm.roadName = ''; | 384 | workOrderForm.roadName = ''; |
| 378 | workOrderForm.roadId = 0; | 385 | workOrderForm.roadId = 0; |
| 379 | - // 2. 清空道路名称列表数据 | ||
| 380 | roadNameList.value = []; | 386 | roadNameList.value = []; |
| 381 | - // 3. 若已选择工单位置,重新请求对应业务线的道路列表 | 387 | + // 清空问题类型 |
| 388 | + workOrderForm.orderName = '' | ||
| 389 | + workOrderForm.orderCode = '' | ||
| 390 | + cascaderValue.value = [] | ||
| 391 | + cascaderOptions.value = [] | ||
| 392 | + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn) | ||
| 393 | + loadProblemCascader(busiLineEn) | ||
| 382 | if (workOrderForm.workLocation) { | 394 | if (workOrderForm.workLocation) { |
| 383 | getRoadListByBusiLine(); | 395 | getRoadListByBusiLine(); |
| 384 | } | 396 | } |
| 385 | }; | 397 | }; |
| 386 | 398 | ||
| 387 | -/** | ||
| 388 | - * 封装:根据当前选中业务线(中文)获取英文标识,再请求道路列表 | ||
| 389 | - */ | ||
| 390 | const getRoadListByBusiLine = async () => { | 399 | const getRoadListByBusiLine = async () => { |
| 391 | if (!workOrderForm.lat || !workOrderForm.lon) { | 400 | if (!workOrderForm.lat || !workOrderForm.lon) { |
| 392 | return; | 401 | return; |
| 393 | } | 402 | } |
| 394 | - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整) | ||
| 395 | const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); | 403 | const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); |
| 396 | if (!busiLineEn) { | 404 | if (!busiLineEn) { |
| 397 | uni.showToast({ title: '业务线标识异常', icon: 'none' }); | 405 | uni.showToast({ title: '业务线标识异常', icon: 'none' }); |
| 398 | return; | 406 | return; |
| 399 | } | 407 | } |
| 400 | - | ||
| 401 | try { | 408 | try { |
| 402 | uni.showLoading({ title: '获取道路名称中...' }); | 409 | uni.showLoading({ title: '获取道路名称中...' }); |
| 403 | const roadRes = await getRoadListByLatLng({ | 410 | const roadRes = await getRoadListByLatLng({ |
| 404 | - busiLine: busiLineEn, // 传递英文标识(yl/sz/wy),接口正常使用 | ||
| 405 | - // companyCode: 'sls', // 传递英文标识(yl/sz/wy),接口正常使用 | 411 | + busiLine: busiLineEn, |
| 406 | latitude: workOrderForm.lat, | 412 | latitude: workOrderForm.lat, |
| 407 | longitude: workOrderForm.lon | 413 | longitude: workOrderForm.lon |
| 408 | }); | 414 | }); |
| @@ -425,115 +431,72 @@ const getRoadListByBusiLine = async () => { | @@ -425,115 +431,72 @@ const getRoadListByBusiLine = async () => { | ||
| 425 | } | 431 | } |
| 426 | }; | 432 | }; |
| 427 | 433 | ||
| 428 | -// ========== 通用弹窗方法 ========== | ||
| 429 | -/** | ||
| 430 | - * 打开通用下拉弹窗 | ||
| 431 | - */ | 434 | +// 通用action-sheet方法 |
| 432 | const handleActionSheetOpen = (type) => { | 435 | const handleActionSheetOpen = (type) => { |
| 433 | - // 道路名称需先校验工单位置是否选择 | ||
| 434 | if (type === 'roadName' && !workOrderForm.workLocation) { | 436 | if (type === 'roadName' && !workOrderForm.workLocation) { |
| 435 | uni.showToast({ title: '请先选择工单位置', icon: 'none' }) | 437 | uni.showToast({ title: '请先选择工单位置', icon: 'none' }) |
| 436 | return | 438 | return |
| 437 | } | 439 | } |
| 438 | - | ||
| 439 | - // 配置当前弹窗参数 | ||
| 440 | const configMap = { | 440 | const configMap = { |
| 441 | roadName: { | 441 | roadName: { |
| 442 | title: '请选择道路名称', | 442 | title: '请选择道路名称', |
| 443 | list: roadNameList.value | 443 | list: roadNameList.value |
| 444 | }, | 444 | }, |
| 445 | - orderName: { | ||
| 446 | - title: '请选择问题类型', | ||
| 447 | - list: orderNameList.value | ||
| 448 | - }, | ||
| 449 | pressingType: { | 445 | pressingType: { |
| 450 | title: '请选择紧急程度', | 446 | title: '请选择紧急程度', |
| 451 | list: pressingTypeList.value | 447 | list: pressingTypeList.value |
| 452 | } | 448 | } |
| 453 | } | 449 | } |
| 454 | - | ||
| 455 | currentActionSheetData.type = type | 450 | currentActionSheetData.type = type |
| 456 | currentActionSheetData.title = configMap[type].title | 451 | currentActionSheetData.title = configMap[type].title |
| 457 | currentActionSheetData.list = configMap[type].list | 452 | currentActionSheetData.list = configMap[type].list |
| 458 | showActionSheet.value = true | 453 | showActionSheet.value = true |
| 459 | } | 454 | } |
| 460 | - | ||
| 461 | -/** | ||
| 462 | - * 关闭通用下拉弹窗 | ||
| 463 | - */ | ||
| 464 | const handleActionSheetClose = () => { | 455 | const handleActionSheetClose = () => { |
| 465 | showActionSheet.value = false | 456 | showActionSheet.value = false |
| 466 | - // 重置当前弹窗配置 | ||
| 467 | currentActionSheetData.type = '' | 457 | currentActionSheetData.type = '' |
| 468 | currentActionSheetData.list = [] | 458 | currentActionSheetData.list = [] |
| 469 | currentActionSheetData.title = '' | 459 | currentActionSheetData.title = '' |
| 470 | } | 460 | } |
| 471 | - | ||
| 472 | -/** | ||
| 473 | - * 通用下拉弹窗选择事件 | ||
| 474 | - */ | ||
| 475 | const handleActionSheetSelect = (e) => { | 461 | const handleActionSheetSelect = (e) => { |
| 476 | const { type } = currentActionSheetData | 462 | const { type } = currentActionSheetData |
| 477 | - // 根据类型处理不同的选择逻辑 | ||
| 478 | switch (type) { | 463 | switch (type) { |
| 479 | case 'roadName': | 464 | case 'roadName': |
| 480 | workOrderForm.roadName = e.name | 465 | workOrderForm.roadName = e.name |
| 481 | workOrderForm.roadId = e.code | 466 | workOrderForm.roadId = e.code |
| 482 | workOrderFormRef.value?.validateField('roadName') | 467 | workOrderFormRef.value?.validateField('roadName') |
| 483 | break | 468 | break |
| 484 | - case 'orderName': | ||
| 485 | - workOrderForm.orderName = e.name | ||
| 486 | - workOrderFormRef.value?.validateField('orderName') | ||
| 487 | - break | ||
| 488 | case 'pressingType': | 469 | case 'pressingType': |
| 489 | - console.log(e) | ||
| 490 | workOrderForm.pressingType = e.value | 470 | workOrderForm.pressingType = e.value |
| 491 | workOrderForm.pressingTypeName = e.name | 471 | workOrderForm.pressingTypeName = e.name |
| 492 | workOrderFormRef.value?.validateField('pressingTypeName') | 472 | workOrderFormRef.value?.validateField('pressingTypeName') |
| 493 | break | 473 | break |
| 494 | } | 474 | } |
| 495 | - // 关闭弹窗 | ||
| 496 | showActionSheet.value = false | 475 | showActionSheet.value = false |
| 497 | } | 476 | } |
| 498 | 477 | ||
| 499 | -/** | ||
| 500 | - * 返回上一页 | ||
| 501 | - */ | ||
| 502 | const navigateBack = () => { | 478 | const navigateBack = () => { |
| 503 | uni.reLaunch({ | 479 | uni.reLaunch({ |
| 504 | - url: '/pages-sub/problem/work-order-manage/index', // 列表页路径 | 480 | + url: '/pages-sub/problem/work-order-manage/index', |
| 505 | fail: () => { | 481 | fail: () => { |
| 506 | - // 兜底:若 reLaunch 失败,返回2层(跳过提交页) | ||
| 507 | uni.navigateBack({ delta: 2 }); | 482 | uni.navigateBack({ delta: 2 }); |
| 508 | } | 483 | } |
| 509 | }); | 484 | }); |
| 510 | } | 485 | } |
| 511 | - | ||
| 512 | -// 新增:清除希望完成时间的方法 | ||
| 513 | const clearExpectedFinishDate = () => { | 486 | const clearExpectedFinishDate = () => { |
| 514 | workOrderForm.expectedFinishDate = '' | 487 | workOrderForm.expectedFinishDate = '' |
| 515 | - // 清除后触发一次字段校验,确保校验状态更新 | ||
| 516 | - // this.$refs.inspectFormRef.validateField('expectedFinishDate') | ||
| 517 | } | 488 | } |
| 518 | - | ||
| 519 | -/** | ||
| 520 | - * 选择工单位置 | ||
| 521 | - */ | ||
| 522 | const chooseWorkLocation = () => { | 489 | const chooseWorkLocation = () => { |
| 523 | uni.chooseLocation({ | 490 | uni.chooseLocation({ |
| 524 | success: async (res) => { | 491 | success: async (res) => { |
| 525 | workOrderForm.roadName = '' | 492 | workOrderForm.roadName = '' |
| 526 | workOrderForm.roadId = 0 | 493 | workOrderForm.roadId = 0 |
| 527 | roadNameList.value = [] | 494 | roadNameList.value = [] |
| 528 | - | ||
| 529 | workOrderForm.workLocation = res.name | 495 | workOrderForm.workLocation = res.name |
| 530 | workOrderForm.lat = res.latitude | 496 | workOrderForm.lat = res.latitude |
| 531 | workOrderForm.lon = res.longitude | 497 | workOrderForm.lon = res.longitude |
| 532 | - | ||
| 533 | workOrderFormRef.value?.validateField('workLocation') | 498 | workOrderFormRef.value?.validateField('workLocation') |
| 534 | workOrderFormRef.value?.validateField('roadName') | 499 | workOrderFormRef.value?.validateField('roadName') |
| 535 | - | ||
| 536 | - // 改用封装后的方法,自动携带选中的业务线英文标识 | ||
| 537 | await getRoadListByBusiLine(); | 500 | await getRoadListByBusiLine(); |
| 538 | }, | 501 | }, |
| 539 | fail: (err) => { | 502 | fail: (err) => { |
| @@ -542,44 +505,28 @@ const chooseWorkLocation = () => { | @@ -542,44 +505,28 @@ const chooseWorkLocation = () => { | ||
| 542 | } | 505 | } |
| 543 | }) | 506 | }) |
| 544 | } | 507 | } |
| 545 | - | ||
| 546 | -/** | ||
| 547 | - * 完成时间确认 | ||
| 548 | - */ | ||
| 549 | const expectedFinishDateConfirm = (e) => { | 508 | const expectedFinishDateConfirm = (e) => { |
| 550 | console.log('选择的完成时间:', e) | 509 | console.log('选择的完成时间:', e) |
| 551 | workOrderForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss') | 510 | workOrderForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss') |
| 552 | show.value = false | 511 | show.value = false |
| 553 | } | 512 | } |
| 554 | - | ||
| 555 | -/** | ||
| 556 | - * 隐藏键盘 | ||
| 557 | - */ | ||
| 558 | const hideKeyboard = () => { | 513 | const hideKeyboard = () => { |
| 559 | uni.hideKeyboard() | 514 | uni.hideKeyboard() |
| 560 | } | 515 | } |
| 561 | 516 | ||
| 562 | -/** | ||
| 563 | - * 提交工单(区分新增/重新提交,接口隔离:从workOrderForm.busiLineCn取值) | ||
| 564 | - */ | ||
| 565 | const submitWorkOrder = async () => { | 517 | const submitWorkOrder = async () => { |
| 566 | try { | 518 | try { |
| 567 | - // 先执行表单校验(包含业务线校验) | ||
| 568 | await workOrderFormRef.value.validate() | 519 | await workOrderFormRef.value.validate() |
| 569 | - | ||
| 570 | - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整) | ||
| 571 | const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); | 520 | const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); |
| 572 | if (!busiLineEn) { | 521 | if (!busiLineEn) { |
| 573 | uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' }); | 522 | uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' }); |
| 574 | return; | 523 | return; |
| 575 | } | 524 | } |
| 576 | - console.log('完成时间:' + workOrderForm.expectedFinishDate) | ||
| 577 | const keyInspectionVal = workOrderForm.keyInspection === '是' ? 1 : 0 | 525 | const keyInspectionVal = workOrderForm.keyInspection === '是' ? 1 : 0 |
| 578 | - // 构建公共提交数据 | ||
| 579 | const commonSubmitData = { | 526 | const commonSubmitData = { |
| 580 | roadId: workOrderForm.roadId, | 527 | roadId: workOrderForm.roadId, |
| 581 | roadName: workOrderForm.roadName, | 528 | roadName: workOrderForm.roadName, |
| 582 | - problemsImgs: problemImgs.getSuccessImgUrls(), // 复用上传逻辑的URL获取方法 | 529 | + problemsImgs: problemImgs.getSuccessImgUrls(), |
| 583 | remark: workOrderForm.problemDesc.trim(), | 530 | remark: workOrderForm.problemDesc.trim(), |
| 584 | latLonType: 2, | 531 | latLonType: 2, |
| 585 | lat: workOrderForm.lat, | 532 | lat: workOrderForm.lat, |
| @@ -587,62 +534,47 @@ const submitWorkOrder = async () => { | @@ -587,62 +534,47 @@ const submitWorkOrder = async () => { | ||
| 587 | lonLatAddress: workOrderForm.workLocation, | 534 | lonLatAddress: workOrderForm.workLocation, |
| 588 | pressingType: workOrderForm.pressingType, | 535 | pressingType: workOrderForm.pressingType, |
| 589 | orderName: workOrderForm.orderName, | 536 | orderName: workOrderForm.orderName, |
| 537 | + orderCode: workOrderForm.orderCode, | ||
| 590 | keyInspection: keyInspectionVal, | 538 | keyInspection: keyInspectionVal, |
| 591 | expectedFinishDate: workOrderForm.expectedFinishDate == "" ? "" : new Date(workOrderForm.expectedFinishDate).getTime(), | 539 | expectedFinishDate: workOrderForm.expectedFinishDate == "" ? "" : new Date(workOrderForm.expectedFinishDate).getTime(), |
| 592 | sourceId: 1, | 540 | sourceId: 1, |
| 593 | - sourceName: workOrderForm.busiLineCn, // 从form字段获取业务线中文名称 | ||
| 594 | - busiLine: busiLineEn // 传递英文标识(yl/sz/wy),接口正常使用 | 541 | + sourceName: workOrderForm.busiLineCn, |
| 542 | + busiLine: busiLineEn | ||
| 595 | } | 543 | } |
| 596 | - console.log('工单数据') | ||
| 597 | - console.log('完成时间:' + workOrderForm.expectedFinishDate) | ||
| 598 | console.log(commonSubmitData) | 544 | console.log(commonSubmitData) |
| 599 | - // 显示加载中 | ||
| 600 | uni.showLoading({ title: '提交中...' }) | 545 | uni.showLoading({ title: '提交中...' }) |
| 601 | let res | 546 | let res |
| 602 | - | ||
| 603 | - // 核心:根据状态区分接口调用 | ||
| 604 | if (isRenew.value) { | 547 | if (isRenew.value) { |
| 605 | - // 重新提交:调用 universalApproval 接口 | ||
| 606 | const renewSubmitData = { | 548 | const renewSubmitData = { |
| 607 | - // 原有工单必要参数 | ||
| 608 | workerDataId: renewOrderData.value.id, | 549 | workerDataId: renewOrderData.value.id, |
| 609 | taskKey: renewOrderData.value.taskKey, | 550 | taskKey: renewOrderData.value.taskKey, |
| 610 | taskId: renewOrderData.value.taskId, | 551 | taskId: renewOrderData.value.taskId, |
| 611 | operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '', | 552 | operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '', |
| 612 | agree: 0, | 553 | agree: 0, |
| 613 | reason: workOrderForm.problemDesc.trim(), | 554 | reason: workOrderForm.problemDesc.trim(), |
| 614 | - // 新编辑的工单数据 | ||
| 615 | ...commonSubmitData | 555 | ...commonSubmitData |
| 616 | } | 556 | } |
| 617 | res = await universalApproval(renewSubmitData) | 557 | res = await universalApproval(renewSubmitData) |
| 618 | } else { | 558 | } else { |
| 619 | - // 新增工单:调用原有 workorderCreate 接口(不影响原有功能) | ||
| 620 | res = await workorderCreate(commonSubmitData) | 559 | res = await workorderCreate(commonSubmitData) |
| 621 | } | 560 | } |
| 622 | - | ||
| 623 | uni.hideLoading() | 561 | uni.hideLoading() |
| 624 | uni.showToast({ | 562 | uni.showToast({ |
| 625 | title: isRenew.value ? '重新提交成功' : '工单提交成功', | 563 | title: isRenew.value ? '重新提交成功' : '工单提交成功', |
| 626 | icon: 'success', | 564 | icon: 'success', |
| 627 | duration: 1000 | 565 | duration: 1000 |
| 628 | }) | 566 | }) |
| 629 | - | ||
| 630 | - // 延迟跳转回列表页 | ||
| 631 | setTimeout(() => { | 567 | setTimeout(() => { |
| 632 | uni.reLaunch({ | 568 | uni.reLaunch({ |
| 633 | url: '/pages-sub/problem/work-order-manage/index' | 569 | url: '/pages-sub/problem/work-order-manage/index' |
| 634 | }) | 570 | }) |
| 635 | }, 1000) | 571 | }, 1000) |
| 636 | } catch (error) { | 572 | } catch (error) { |
| 637 | - // 隐藏加载框 | ||
| 638 | uni.hideLoading() | 573 | uni.hideLoading() |
| 639 | - | ||
| 640 | - // 区分是表单校验失败还是接口调用失败 | ||
| 641 | if (!Array.isArray(error)) { | 574 | if (!Array.isArray(error)) { |
| 642 | - // 接口调用失败 | ||
| 643 | console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error) | 575 | console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error) |
| 644 | uni.showToast({ | 576 | uni.showToast({ |
| 645 | - title: isRenew.value ? error.msg : error.msg, | 577 | + title: error.msg || '请求异常', |
| 646 | icon: 'none', | 578 | icon: 'none', |
| 647 | duration: 2000 | 579 | duration: 2000 |
| 648 | }) | 580 | }) |
| @@ -652,15 +584,11 @@ const submitWorkOrder = async () => { | @@ -652,15 +584,11 @@ const submitWorkOrder = async () => { | ||
| 652 | </script> | 584 | </script> |
| 653 | 585 | ||
| 654 | <style lang="scss" scoped> | 586 | <style lang="scss" scoped> |
| 655 | -// 全局页面样式 | ||
| 656 | .page-container { | 587 | .page-container { |
| 657 | min-height: 100vh; | 588 | min-height: 100vh; |
| 658 | - padding-bottom: 100rpx; // 给底部按钮留空间 | 589 | + padding-bottom: 100rpx; |
| 659 | } | 590 | } |
| 660 | - | ||
| 661 | - | ||
| 662 | -// 工单表单内容容器 | ||
| 663 | .work-order-form-content { | 591 | .work-order-form-content { |
| 664 | background: #fff; | 592 | background: #fff; |
| 665 | } | 593 | } |
| 666 | -</style> | ||
| 667 | \ No newline at end of file | 594 | \ No newline at end of file |
| 595 | +</style> |
pages.json
| @@ -41,6 +41,13 @@ | @@ -41,6 +41,13 @@ | ||
| 41 | "navigationBarTitleText": "巡查计划" | 41 | "navigationBarTitleText": "巡查计划" |
| 42 | } | 42 | } |
| 43 | }, | 43 | }, |
| 44 | +{ | ||
| 45 | + "path": "attendanceRecord", | ||
| 46 | + "style": { | ||
| 47 | + "navigationBarTitleText": "考勤记录" | ||
| 48 | + } | ||
| 49 | + }, | ||
| 50 | + | ||
| 44 | { | 51 | { |
| 45 | "path": "patrol-manage/pending-plan-detail", | 52 | "path": "patrol-manage/pending-plan-detail", |
| 46 | "style": { | 53 | "style": { |
pages/index/index.vue
| @@ -60,12 +60,12 @@ | @@ -60,12 +60,12 @@ | ||
| 60 | v-for="(baseListItem,baseListIndex) in baseList" | 60 | v-for="(baseListItem,baseListIndex) in baseList" |
| 61 | :key="baseListIndex" | 61 | :key="baseListIndex" |
| 62 | > | 62 | > |
| 63 | - <up-icon | 63 | + <!-- <up-icon |
| 64 | :customStyle="{paddingTop:20+'rpx'}" | 64 | :customStyle="{paddingTop:20+'rpx'}" |
| 65 | :name="baseListItem.name" | 65 | :name="baseListItem.name" |
| 66 | :size="22" | 66 | :size="22" |
| 67 | - ></up-icon> | ||
| 68 | - <text class="grid-text">{{baseListItem.num}}</text> | 67 | + ></up-icon> --> |
| 68 | + <text class="grid-text" style="font-size: 24px;">{{baseListItem.num}}</text> | ||
| 69 | <text class="grid-text">{{baseListItem.title}}</text> | 69 | <text class="grid-text">{{baseListItem.title}}</text> |
| 70 | </up-grid-item> | 70 | </up-grid-item> |
| 71 | </up-grid> | 71 | </up-grid> |
| @@ -140,113 +140,64 @@ | @@ -140,113 +140,64 @@ | ||
| 140 | </template> | 140 | </template> |
| 141 | 141 | ||
| 142 | <script setup lang="ts"> | 142 | <script setup lang="ts"> |
| 143 | -import { ref, computed, nextTick } from 'vue'; | ||
| 144 | -import { onShow,onUnload } from '@dcloudio/uni-app' | 143 | +import { ref, computed, nextTick, watch } from 'vue'; |
| 144 | +import { onShow, onUnload } from '@dcloudio/uni-app'; | ||
| 145 | import { useUserStore } from '@/pinia/user'; | 145 | import { useUserStore } from '@/pinia/user'; |
| 146 | -import { timeFormat } from '@/uni_modules/uview-plus'; | 146 | +// import { timeFormat } from '@/uni_modules/uview-plus/libs/function/date'; |
| 147 | import uniCharts from '@/components/uni-charts/uni-charts.vue'; | 147 | import uniCharts from '@/components/uni-charts/uni-charts.vue'; |
| 148 | import { getUnreadCount } from "@/api/user"; | 148 | import { getUnreadCount } from "@/api/user"; |
| 149 | import { | 149 | import { |
| 150 | getTaskCompletionSummary, | 150 | getTaskCompletionSummary, |
| 151 | taskDetailsWithDistance, | 151 | taskDetailsWithDistance, |
| 152 | - taskDetailsWithSmartSort | 152 | + taskDetailsWithSmartSort, |
| 153 | + getCurrentOrg, | ||
| 154 | + orgTaskIndicator | ||
| 153 | } from "@/api/index/index"; | 155 | } from "@/api/index/index"; |
| 154 | - | ||
| 155 | const userStore = useUserStore(); | 156 | const userStore = useUserStore(); |
| 156 | -const deptId = computed(() => String(userStore.userInfo?.user?.deptId ?? '')); | 157 | +const deptId = computed(() => String(userStore.userInfo?.deptId ?? '')); |
| 157 | 158 | ||
| 158 | // ====================== 分组配置(多部门归属同一组) ====================== | 159 | // ====================== 分组配置(多部门归属同一组) ====================== |
| 159 | type GroupItem = { | 160 | type GroupItem = { |
| 160 | - deptIds: string[]; // 多个部门ID数组 | 161 | + deptIds: string[]; |
| 161 | label: string; | 162 | label: string; |
| 162 | - showTipHeader: boolean; // 是否展示顶部进度条 | ||
| 163 | - useWalkText: boolean; // 是否展示步行距离文案 | 163 | + showTipHeader: boolean; |
| 164 | + useWalkText: boolean; | ||
| 164 | }; | 165 | }; |
| 165 | - | ||
| 166 | -// 创建响应式数据 | ||
| 167 | -const baseList = ref([ | ||
| 168 | - { | ||
| 169 | - num:'123', | ||
| 170 | - name: 'photo', | ||
| 171 | - title: '应办任务数' | ||
| 172 | - }, | ||
| 173 | - { | ||
| 174 | - num:'32', | ||
| 175 | - name: 'photo', | ||
| 176 | - title: '已办任务数' | ||
| 177 | - }, | ||
| 178 | - { | ||
| 179 | - num:'456', | ||
| 180 | - name: 'lock', | ||
| 181 | - title: '待办任务数' | ||
| 182 | - }, | ||
| 183 | - { | ||
| 184 | - num:'0', | ||
| 185 | - name: 'star', | ||
| 186 | - title: '超时未完成' | ||
| 187 | - }, | ||
| 188 | -]); | ||
| 189 | - | ||
| 190 | -const options1 = ref([ | ||
| 191 | - { | ||
| 192 | - label: '默认排序', | ||
| 193 | - value: 1, | ||
| 194 | - }, | ||
| 195 | - { | ||
| 196 | - label: '距离优先', | ||
| 197 | - value: 2, | ||
| 198 | - }, | ||
| 199 | - { | ||
| 200 | - label: '价格优先', | ||
| 201 | - value: 3, | ||
| 202 | - } | ||
| 203 | -]); | ||
| 204 | - | ||
| 205 | const groupConfig: GroupItem[] = [ | 166 | const groupConfig: GroupItem[] = [ |
| 206 | - // 组2 A1B2 纯效率·有提醒 | ||
| 207 | { | 167 | { |
| 208 | deptIds: ['361', '363', '279', '277', '270'], | 168 | deptIds: ['361', '363', '279', '277', '270'], |
| 209 | - label: '[组2 A1B2 纯效率·有提醒]', | 169 | + label: '[组2 A1B2 纯效率·有提醒]', |
| 210 | showTipHeader: true, | 170 | showTipHeader: true, |
| 211 | useWalkText: true | 171 | useWalkText: true |
| 212 | }, | 172 | }, |
| 213 | - // 组3 A2B1 个性化·无提醒 | ||
| 214 | { | 173 | { |
| 215 | deptIds: ['252', '273', '274', '337', '359', '360'], | 174 | deptIds: ['252', '273', '274', '337', '359', '360'], |
| 216 | - label: '[组3 A2B1 个性化·无提醒]', | 175 | + label: '[组3 A2B1 个性化·无提醒]', |
| 217 | showTipHeader: false, | 176 | showTipHeader: false, |
| 218 | useWalkText: false | 177 | useWalkText: false |
| 219 | }, | 178 | }, |
| 220 | - // 组4 A2B2 个性化·有提醒 | ||
| 221 | { | 179 | { |
| 222 | deptIds: ['281', '272', '271', '338', '280'], | 180 | deptIds: ['281', '272', '271', '338', '280'], |
| 223 | - label: '[组4 A2B2 个性化·有提醒]', | 181 | + label: '[组4 A2B2 个性化·有提醒]', |
| 224 | showTipHeader: true, | 182 | showTipHeader: true, |
| 225 | useWalkText: false | 183 | useWalkText: false |
| 226 | }, | 184 | }, |
| 227 | ]; | 185 | ]; |
| 228 | -// 默认组1:未匹配到的所有部门 | ||
| 229 | const defaultGroup: GroupItem = { | 186 | const defaultGroup: GroupItem = { |
| 230 | deptIds: [], | 187 | deptIds: [], |
| 231 | - label: '[组1 A1B1 纯效率·无提醒]', | 188 | + label: '[组1 A1B1 纯效率·无提醒]', |
| 232 | showTipHeader: false, | 189 | showTipHeader: false, |
| 233 | useWalkText: true | 190 | useWalkText: true |
| 234 | }; | 191 | }; |
| 235 | - | ||
| 236 | -// 获取当前部门所属分组 | ||
| 237 | const currentGroup = computed(() => { | 192 | const currentGroup = computed(() => { |
| 238 | const targetDept = deptId.value; | 193 | const targetDept = deptId.value; |
| 239 | - // 遍历匹配部门ID | ||
| 240 | - for (const group of groupConfig) { | ||
| 241 | - if (group.deptIds.includes(targetDept)) { | ||
| 242 | - return group; | 194 | + for (const g of groupConfig) { |
| 195 | + if (g.deptIds.includes(targetDept)) { | ||
| 196 | + return g; | ||
| 243 | } | 197 | } |
| 244 | } | 198 | } |
| 245 | - // 无匹配返回默认组1 | ||
| 246 | return defaultGroup; | 199 | return defaultGroup; |
| 247 | }); | 200 | }); |
| 248 | - | ||
| 249 | -// 自动读取分组配置,无需修改其他业务代码 | ||
| 250 | const tipText = computed(() => currentGroup.value.label); | 201 | const tipText = computed(() => currentGroup.value.label); |
| 251 | const showTipHeader = computed(() => currentGroup.value.showTipHeader); | 202 | const showTipHeader = computed(() => currentGroup.value.showTipHeader); |
| 252 | const isWalkTextMode = computed(() => currentGroup.value.useWalkText); | 203 | const isWalkTextMode = computed(() => currentGroup.value.useWalkText); |
| @@ -257,13 +208,11 @@ const finishNum = ref(0); | @@ -257,13 +208,11 @@ const finishNum = ref(0); | ||
| 257 | const avgNum = ref(0); | 208 | const avgNum = ref(0); |
| 258 | const msgCount = ref(0); | 209 | const msgCount = ref(0); |
| 259 | const currentTabValue = ref(0); | 210 | const currentTabValue = ref(0); |
| 260 | - | ||
| 261 | const dateRange = ref<string[]>([]); | 211 | const dateRange = ref<string[]>([]); |
| 262 | const yesterday = new Date(); | 212 | const yesterday = new Date(); |
| 263 | yesterday.setDate(yesterday.getDate() - 1); | 213 | yesterday.setDate(yesterday.getDate() - 1); |
| 264 | -const maxDate = ref(timeFormat(yesterday, 'yyyy-mm-dd')); | 214 | +const maxDate = ref(timeFormat(yesterday, 'YYYY-MM-DD')); |
| 265 | const minDate = '2026-01-06'; | 215 | const minDate = '2026-01-06'; |
| 266 | - | ||
| 267 | const isDatePickerOpen = ref(false); | 216 | const isDatePickerOpen = ref(false); |
| 268 | const handlePopupShow = () => { | 217 | const handlePopupShow = () => { |
| 269 | isDatePickerOpen.value = true; | 218 | isDatePickerOpen.value = true; |
| @@ -279,17 +228,15 @@ const doneFullList = ref<any[]>([]); | @@ -279,17 +228,15 @@ const doneFullList = ref<any[]>([]); | ||
| 279 | const taskList = computed(() => { | 228 | const taskList = computed(() => { |
| 280 | return currentTabValue.value === 0 ? todoFullList.value : doneFullList.value; | 229 | return currentTabValue.value === 0 ? todoFullList.value : doneFullList.value; |
| 281 | }); | 230 | }); |
| 282 | - | ||
| 283 | const chooseOptions = computed(() => [ | 231 | const chooseOptions = computed(() => [ |
| 284 | { title: `待办事项(${todoTotal.value})`, id: 'todo' }, | 232 | { title: `待办事项(${todoTotal.value})`, id: 'todo' }, |
| 285 | - { title: `已办事项(${doneTotal.value})`, id: 'done' } | 233 | + { title: `已办事项(${doneTotal.value})`, id: 'done' }, |
| 286 | ]); | 234 | ]); |
| 287 | 235 | ||
| 288 | const currentLon = ref<string>(''); | 236 | const currentLon = ref<string>(''); |
| 289 | const currentLat = ref<string>(''); | 237 | const currentLat = ref<string>(''); |
| 290 | const chartWidth = ref(0); | 238 | const chartWidth = ref(0); |
| 291 | const chartHeight = ref(300); | 239 | const chartHeight = ref(300); |
| 292 | - | ||
| 293 | const klineCategories = ref<string[]>([]); | 240 | const klineCategories = ref<string[]>([]); |
| 294 | const klineChartData = ref([ | 241 | const klineChartData = ref([ |
| 295 | { | 242 | { |
| @@ -303,12 +250,11 @@ const klineChartData = ref([ | @@ -303,12 +250,11 @@ const klineChartData = ref([ | ||
| 303 | color: '#B34C17' | 250 | color: '#B34C17' |
| 304 | } | 251 | } |
| 305 | ]); | 252 | ]); |
| 306 | - | ||
| 307 | const klineOption = ref({ | 253 | const klineOption = ref({ |
| 308 | grid: { | 254 | grid: { |
| 309 | top: '25%', | 255 | top: '25%', |
| 310 | left: '12%', | 256 | left: '12%', |
| 311 | - right: '8%', | 257 | + right: '12%', |
| 312 | bottom: '16%' | 258 | bottom: '16%' |
| 313 | }, | 259 | }, |
| 314 | xAxis: { | 260 | xAxis: { |
| @@ -323,8 +269,8 @@ const klineOption = ref({ | @@ -323,8 +269,8 @@ const klineOption = ref({ | ||
| 323 | trigger: 'axis', | 269 | trigger: 'axis', |
| 324 | formatter: (params: any[]) => { | 270 | formatter: (params: any[]) => { |
| 325 | return `${params[0].name}<br/> | 271 | return `${params[0].name}<br/> |
| 326 | - ${params[0].seriesName}: ${params[0].data}<br/> | ||
| 327 | - ${params[1].seriesName}: ${params[1].data}`; | 272 | +${params[0].seriesName}: ${params[0].value}<br/> |
| 273 | +${params[1].seriesName}: ${params[1].value}`; | ||
| 328 | } | 274 | } |
| 329 | }, | 275 | }, |
| 330 | legend: { | 276 | legend: { |
| @@ -334,171 +280,123 @@ const klineOption = ref({ | @@ -334,171 +280,123 @@ const klineOption = ref({ | ||
| 334 | } | 280 | } |
| 335 | }); | 281 | }); |
| 336 | 282 | ||
| 337 | -const value1 = ref(1) | ||
| 338 | -const showActionSheet = ref(false) | 283 | +const baseList = ref<Array<{ num: string; title: string }>>([]); |
| 284 | +const orgOptions = ref<any[]>([]); | ||
| 285 | +const value1 = ref<number | string>(''); | ||
| 286 | +const showActionSheet = ref(false); | ||
| 287 | + | ||
| 288 | +watch(orgOptions, async (list) => { | ||
| 289 | + if (list.length && !value1.value) { | ||
| 290 | + value1.value = list[0].value; | ||
| 291 | + // 拿到默认组织后,拉指标 | ||
| 292 | + if (dateRange.value.length === 2) { | ||
| 293 | + await fetchOrgIndicator(Number(value1.value), dateRange.value[0], dateRange.value[1]); | ||
| 294 | + } | ||
| 295 | + } | ||
| 296 | +}, { immediate: true }); | ||
| 339 | 297 | ||
| 340 | -// actionSheet选项映射 | ||
| 341 | -const actionSheetOptions = computed(()=>{ | ||
| 342 | - return options1.value.map(opt => ({ | 298 | +const actionSheetOptions = computed(() => { |
| 299 | + return orgOptions.value.map(opt => ({ | ||
| 343 | name: opt.label, | 300 | name: opt.label, |
| 344 | value: opt.value | 301 | value: opt.value |
| 345 | - })) | ||
| 346 | -}) | ||
| 347 | -// 显示当前选中的文字 | ||
| 348 | -const sortLabel = computed(()=>{ | ||
| 349 | - const cur = options1.value.find(i=>i.value === value1.value) | ||
| 350 | - return cur?.label ?? "默认排序" | ||
| 351 | -}) | ||
| 352 | - | ||
| 353 | -// 打开菜单 | ||
| 354 | -const openActionSheet = ()=>{ | ||
| 355 | - showActionSheet.value = true | ||
| 356 | -} | ||
| 357 | -// 选中回调 | ||
| 358 | -const handleSheetSelect = (item: {name:string, value:number})=>{ | ||
| 359 | - value1.value = item.value | ||
| 360 | - showActionSheet.value = false | ||
| 361 | - // ========== 在这里添加排序业务逻辑 ========== | ||
| 362 | - console.log('选中排序类型:', item.value) | ||
| 363 | -} | ||
| 364 | - | ||
| 365 | - | ||
| 366 | -const userName = computed(() => userStore.userInfo?.user?.nickname || '用户'); | 302 | + })); |
| 303 | +}); | ||
| 304 | +const sortLabel = computed(() => { | ||
| 305 | + const cur = orgOptions.value.find(i => i.value === value1.value); | ||
| 306 | + return cur?.label ?? ""; | ||
| 307 | +}); | ||
| 308 | +const openActionSheet = () => { | ||
| 309 | + showActionSheet.value = true; | ||
| 310 | +}; | ||
| 311 | +// ActionSheet选中组织:刷新指标 | ||
| 312 | +const handleSheetSelect = async (item: { name: string; value: number }) => { | ||
| 313 | + value1.value = item.value; | ||
| 314 | + showActionSheet.value = false; | ||
| 315 | + console.log('选中组织:', item.value); | ||
| 316 | + if (dateRange.value.length === 2) { | ||
| 317 | + await fetchOrgIndicator(item.value, dateRange.value[0], dateRange.value[1]); | ||
| 318 | + } | ||
| 319 | +}; | ||
| 367 | 320 | ||
| 321 | +const userName = computed(() => userStore.userInfo?.nickname || '用户'); | ||
| 368 | const rpx2px = (rpx: number) => { | 322 | const rpx2px = (rpx: number) => { |
| 369 | const systemInfo = uni.getSystemInfoSync(); | 323 | const systemInfo = uni.getSystemInfoSync(); |
| 370 | return Math.floor(systemInfo.screenWidth / 750 * rpx); | 324 | return Math.floor(systemInfo.screenWidth / 750 * rpx); |
| 371 | }; | 325 | }; |
| 372 | 326 | ||
| 373 | -// ========== 滚动逻辑 终极优化 ========== | 327 | +// ========== 滚动逻辑 ========== |
| 374 | const showChartArea = ref(true); | 328 | const showChartArea = ref(true); |
| 375 | -// 隐藏阈值:超过80rpx收起 | ||
| 376 | const scrollHideThreshold = rpx2px(80); | 329 | const scrollHideThreshold = rpx2px(80); |
| 377 | -// 安全显示区间:≤80rpx一律显示,覆盖所有回弹残留(70/80/90) | ||
| 378 | const safeShowThreshold = rpx2px(130); | 330 | const safeShowThreshold = rpx2px(130); |
| 379 | let scrollTimer: number | null = null; | 331 | let scrollTimer: number | null = null; |
| 380 | - | ||
| 381 | -// 下拉触顶强制展示 | ||
| 382 | const handleTouchTop = () => { | 332 | const handleTouchTop = () => { |
| 383 | showChartArea.value = true; | 333 | showChartArea.value = true; |
| 384 | }; | 334 | }; |
| 385 | - | ||
| 386 | const onListScroll = (e: any) => { | 335 | const onListScroll = (e: any) => { |
| 387 | const scrollTop = e.detail.scrollTop; | 336 | const scrollTop = e.detail.scrollTop; |
| 388 | - console.log('scrollTop', scrollTop) | ||
| 389 | if (scrollTimer) clearTimeout(scrollTimer); | 337 | if (scrollTimer) clearTimeout(scrollTimer); |
| 390 | - | ||
| 391 | - // 实时:只要在安全区间立刻显示 | ||
| 392 | if (scrollTop <= safeShowThreshold) { | 338 | if (scrollTop <= safeShowThreshold) { |
| 393 | showChartArea.value = true; | 339 | showChartArea.value = true; |
| 394 | } else if (scrollTop > scrollHideThreshold) { | 340 | } else if (scrollTop > scrollHideThreshold) { |
| 395 | showChartArea.value = false; | 341 | showChartArea.value = false; |
| 396 | } | 342 | } |
| 397 | - | ||
| 398 | - // 滚动停止兜底二次校验 | ||
| 399 | scrollTimer = setTimeout(() => { | 343 | scrollTimer = setTimeout(() => { |
| 400 | if (scrollTop <= safeShowThreshold) { | 344 | if (scrollTop <= safeShowThreshold) { |
| 401 | showChartArea.value = true; | 345 | showChartArea.value = true; |
| 402 | } | 346 | } |
| 403 | }, 100); | 347 | }, 100); |
| 404 | }; | 348 | }; |
| 405 | - | ||
| 406 | -// 切换tab重置图表显示 | ||
| 407 | const handleTabClick = (tabIndex: number) => { | 349 | const handleTabClick = (tabIndex: number) => { |
| 408 | if (currentTabValue.value === tabIndex) return; | 350 | if (currentTabValue.value === tabIndex) return; |
| 409 | currentTabValue.value = tabIndex; | 351 | currentTabValue.value = tabIndex; |
| 410 | - // 切换tab强制显示图表 | ||
| 411 | showChartArea.value = true; | 352 | showChartArea.value = true; |
| 412 | }; | 353 | }; |
| 413 | 354 | ||
| 414 | -// 页面每次显示强制重置图表为显示状态 | ||
| 415 | -onShow(async () => { | ||
| 416 | - showChartArea.value = true; | ||
| 417 | - chartWidth.value = rpx2px(750 - 60); | ||
| 418 | - chartHeight.value = rpx2px(300); | ||
| 419 | - initRecent7Days(); | ||
| 420 | - getUnread(); | ||
| 421 | - await loadAllTaskByDept(); | ||
| 422 | -}); | ||
| 423 | - | ||
| 424 | -// 页面销毁清除定时器 | ||
| 425 | -onUnload(() => { | ||
| 426 | - if (scrollTimer) clearTimeout(scrollTimer); | ||
| 427 | -}); | ||
| 428 | -// ====================================== | ||
| 429 | - | ||
| 430 | -const getCurrentLocation = (): Promise<{ lon: string; lat: string }> => { | ||
| 431 | - return new Promise((resolve, reject) => { | ||
| 432 | - uni.getSetting({ | ||
| 433 | - success: (settingRes) => { | ||
| 434 | - if (!settingRes.authSetting['scope.userLocation']) { | ||
| 435 | - uni.authorize({ | ||
| 436 | - scope: 'scope.userLocation', | ||
| 437 | - success: () => { | ||
| 438 | - uni.getLocation({ | ||
| 439 | - type: 'gcj02', | ||
| 440 | - success: (loc) => { | ||
| 441 | - console.log(loc.longitude) | ||
| 442 | - console.log(loc.latitude) | ||
| 443 | - resolve({ | ||
| 444 | - lon: loc.longitude.toFixed(6), | ||
| 445 | - lat: loc.latitude.toFixed(6) | ||
| 446 | - }) | ||
| 447 | - }, | ||
| 448 | - fail: (err) => { | ||
| 449 | - uni.showToast({ title: '获取位置失败,请打开手机定位', icon: 'none' }) | ||
| 450 | - reject(err) | ||
| 451 | - } | ||
| 452 | - }) | ||
| 453 | - }, | ||
| 454 | - fail: () => { | ||
| 455 | - uni.showModal({ | ||
| 456 | - title: '需要定位权限', | ||
| 457 | - content: '请允许获取位置才能正常使用', | ||
| 458 | - confirmText: '去设置', | ||
| 459 | - success: (modal) => { | ||
| 460 | - if (modal.confirm) { | ||
| 461 | - uni.openSetting() | ||
| 462 | - } | ||
| 463 | - } | ||
| 464 | - }) | ||
| 465 | - reject('用户拒绝定位权限') | ||
| 466 | - } | ||
| 467 | - }) | ||
| 468 | - } else { | ||
| 469 | - uni.getLocation({ | ||
| 470 | - type: 'gcj02', | ||
| 471 | - success: (loc) => { | ||
| 472 | - console.log(loc.longitude) | ||
| 473 | - console.log(loc.latitude) | ||
| 474 | - resolve({ | ||
| 475 | - lon: loc.longitude.toFixed(6), | ||
| 476 | - lat: loc.latitude.toFixed(6) | ||
| 477 | - }) | ||
| 478 | - }, | ||
| 479 | - fail: (err) => { | ||
| 480 | - uni.showToast({ title: '获取位置失败,请打开手机定位', icon: 'none' }) | ||
| 481 | - reject(err) | ||
| 482 | - } | ||
| 483 | - }) | ||
| 484 | - } | 355 | +// ========== 公共函数:请求 orgTaskIndicator 指标接口 ========== |
| 356 | +const fetchOrgIndicator = async (orgId: number, beginDate: string, endDate: string) => { | ||
| 357 | + try { | ||
| 358 | + // 日期字符串转毫秒时间戳 | ||
| 359 | + const beginTime = new Date(beginDate).getTime(); | ||
| 360 | + const endTime = new Date(endDate).getTime(); | ||
| 361 | + const res = await orgTaskIndicator({ | ||
| 362 | + orgId, | ||
| 363 | + beginTime, | ||
| 364 | + endTime | ||
| 365 | + }); | ||
| 366 | + console.log('orgTaskIndicator 返回:', res); | ||
| 367 | + baseList.value = [ | ||
| 368 | + { | ||
| 369 | + num: String(res.totalCnt ?? 0), | ||
| 370 | + title: '应办任务数' | ||
| 485 | }, | 371 | }, |
| 486 | - fail: (err) => { | ||
| 487 | - reject('获取权限失败:' + err.errMsg) | 372 | + { |
| 373 | + num: String(res.doneCnt ?? 0), | ||
| 374 | + title: '已办任务数' | ||
| 375 | + }, | ||
| 376 | + { | ||
| 377 | + num: String(res.pendingCnt ?? 0), | ||
| 378 | + title: '待办任务数' | ||
| 379 | + }, | ||
| 380 | + { | ||
| 381 | + num: String(res.overdueCnt ?? 0), | ||
| 382 | + title: '超时未完成' | ||
| 488 | } | 383 | } |
| 489 | - }) | ||
| 490 | - }) | ||
| 491 | -} | ||
| 492 | - | ||
| 493 | -const initRecent7Days = async () => { | ||
| 494 | - const yesterday = new Date(); | ||
| 495 | - yesterday.setDate(yesterday.getDate() - 1); | ||
| 496 | - const sevenDaysAgo = new Date(yesterday); | ||
| 497 | - sevenDaysAgo.setDate(yesterday.getDate() - 6); | 384 | + ]; |
| 385 | + } catch (err) { | ||
| 386 | + console.error('orgTaskIndicator 请求失败:', err); | ||
| 387 | + } | ||
| 388 | +}; | ||
| 498 | 389 | ||
| 390 | +// ========== 初始化日期范围(近7天) ========== | ||
| 391 | +const initRecent7Days = () => { | ||
| 392 | + const now = new Date(); | ||
| 393 | + const end = new Date(now); | ||
| 394 | + end.setDate(end.getDate() - 1); | ||
| 395 | + const start = new Date(end); | ||
| 396 | + start.setDate(start.getDate() - 6); | ||
| 499 | dateRange.value = [ | 397 | dateRange.value = [ |
| 500 | - timeFormat(sevenDaysAgo, 'yyyy-mm-dd'), | ||
| 501 | - timeFormat(yesterday, 'yyyy-mm-dd') | 398 | + timeFormat(start, 'YYYY-MM-DD'), |
| 399 | + timeFormat(end, 'YYYY-MM-DD') | ||
| 502 | ]; | 400 | ]; |
| 503 | initChartData(dateRange.value[0], dateRange.value[1]); | 401 | initChartData(dateRange.value[0], dateRange.value[1]); |
| 504 | }; | 402 | }; |
| @@ -508,8 +406,18 @@ const getUnread = async () => { | @@ -508,8 +406,18 @@ const getUnread = async () => { | ||
| 508 | const res = await getUnreadCount(); | 406 | const res = await getUnreadCount(); |
| 509 | msgCount.value = res || 0; | 407 | msgCount.value = res || 0; |
| 510 | } catch (error) { | 408 | } catch (error) { |
| 511 | - console.error('获取未读消息数失败:', error); | ||
| 512 | - msgCount.value = 0; | 409 | + console.error('获取未读消息失败:', error); |
| 410 | + } | ||
| 411 | +}; | ||
| 412 | + | ||
| 413 | +// getOrg:只拉组织下拉选项,不处理指标 | ||
| 414 | +const getOrg = async () => { | ||
| 415 | + try { | ||
| 416 | + const res = await getCurrentOrg(); | ||
| 417 | + console.log('getCurrentOrg组织下拉:', res); | ||
| 418 | + orgOptions.value = res.options || []; | ||
| 419 | + } catch (error) { | ||
| 420 | + console.error('获取组织下拉失败:', error); | ||
| 513 | } | 421 | } |
| 514 | }; | 422 | }; |
| 515 | 423 | ||
| @@ -519,9 +427,9 @@ const initChartData = async (beginTime: string, endTime: string) => { | @@ -519,9 +427,9 @@ const initChartData = async (beginTime: string, endTime: string) => { | ||
| 519 | const res = await getTaskCompletionSummary(postData); | 427 | const res = await getTaskCompletionSummary(postData); |
| 520 | klineCategories.value = res.map((item: any) => item.currentDate); | 428 | klineCategories.value = res.map((item: any) => item.currentDate); |
| 521 | klineChartData.value[0].data = res.map((item: any) => item.completedNum); | 429 | klineChartData.value[0].data = res.map((item: any) => item.completedNum); |
| 522 | - klineChartData.value[1].data = res.map((item: any) => item.countpendingNum); | 430 | + klineChartData.value[1].data = res.map((item: any) => item.countPendingNum); |
| 523 | } catch (error) { | 431 | } catch (error) { |
| 524 | - console.error('获取图表数据失败:', error); | 432 | + console.error('图表数据请求失败:', error); |
| 525 | } | 433 | } |
| 526 | }; | 434 | }; |
| 527 | 435 | ||
| @@ -542,27 +450,18 @@ const loadDistanceApiTask = async (lon: string, lat: string) => { | @@ -542,27 +450,18 @@ const loadDistanceApiTask = async (lon: string, lat: string) => { | ||
| 542 | await nextTick(); | 450 | await nextTick(); |
| 543 | } catch (err) { | 451 | } catch (err) { |
| 544 | uni.showToast({ title: '加载任务失败', icon: 'none' }); | 452 | uni.showToast({ title: '加载任务失败', icon: 'none' }); |
| 545 | - console.error('加载distance接口任务报错', err); | 453 | + console.error('loadDistanceApiTask error', err); |
| 546 | todoFullList.value = []; | 454 | todoFullList.value = []; |
| 547 | doneFullList.value = []; | 455 | doneFullList.value = []; |
| 548 | todoTotal.value = 0; | 456 | todoTotal.value = 0; |
| 549 | doneTotal.value = 0; | 457 | doneTotal.value = 0; |
| 550 | } | 458 | } |
| 551 | }; | 459 | }; |
| 552 | - | ||
| 553 | const loadSmartSortTask = async (lon: string, lat: string) => { | 460 | const loadSmartSortTask = async (lon: string, lat: string) => { |
| 554 | try { | 461 | try { |
| 555 | const [todoRes, doneRes] = await Promise.all([ | 462 | const [todoRes, doneRes] = await Promise.all([ |
| 556 | - taskDetailsWithSmartSort({ | ||
| 557 | - queryType: 1, | ||
| 558 | - currentLon: lon, | ||
| 559 | - currentLat: lat | ||
| 560 | - }), | ||
| 561 | - taskDetailsWithSmartSort({ | ||
| 562 | - queryType: 2, | ||
| 563 | - currentLon: lon, | ||
| 564 | - currentLat: lat | ||
| 565 | - }) | 463 | + taskDetailsWithSmartSort({ queryType: 1, currentLon: lon, currentLat: lat }), |
| 464 | + taskDetailsWithSmartSort({ queryType: 2, currentLon: lon, currentLat: lat }) | ||
| 566 | ]); | 465 | ]); |
| 567 | todoFullList.value = todoRes?.sortedTasks || []; | 466 | todoFullList.value = todoRes?.sortedTasks || []; |
| 568 | doneFullList.value = doneRes?.sortedTasks || []; | 467 | doneFullList.value = doneRes?.sortedTasks || []; |
| @@ -573,14 +472,13 @@ const loadSmartSortTask = async (lon: string, lat: string) => { | @@ -573,14 +472,13 @@ const loadSmartSortTask = async (lon: string, lat: string) => { | ||
| 573 | await nextTick(); | 472 | await nextTick(); |
| 574 | } catch (err) { | 473 | } catch (err) { |
| 575 | uni.showToast({ title: '加载任务失败', icon: 'none' }); | 474 | uni.showToast({ title: '加载任务失败', icon: 'none' }); |
| 576 | - console.error('加载smartSort接口任务报错', err); | 475 | + console.error('loadSmartSortTask error', err); |
| 577 | todoFullList.value = []; | 476 | todoFullList.value = []; |
| 578 | doneFullList.value = []; | 477 | doneFullList.value = []; |
| 579 | todoTotal.value = 0; | 478 | todoTotal.value = 0; |
| 580 | doneTotal.value = 0; | 479 | doneTotal.value = 0; |
| 581 | } | 480 | } |
| 582 | }; | 481 | }; |
| 583 | - | ||
| 584 | const loadAllTaskByDept = async () => { | 482 | const loadAllTaskByDept = async () => { |
| 585 | const loc = await getCurrentLocation(); | 483 | const loc = await getCurrentLocation(); |
| 586 | currentLon.value = loc.lon; | 484 | currentLon.value = loc.lon; |
| @@ -592,37 +490,119 @@ const loadAllTaskByDept = async () => { | @@ -592,37 +490,119 @@ const loadAllTaskByDept = async () => { | ||
| 592 | } | 490 | } |
| 593 | }; | 491 | }; |
| 594 | 492 | ||
| 595 | -const handleDateConfirm = (e: string[]) => { | 493 | +// 日期确认回调:更新图表 + 重新请求指标orgTaskIndicator |
| 494 | +const handleDateConfirm = async (e: string[]) => { | ||
| 596 | dateRange.value = e; | 495 | dateRange.value = e; |
| 597 | - initChartData(e[0], e[1]); | 496 | + await initChartData(e[0], e[1]); |
| 497 | + if (value1.value) { | ||
| 498 | + await fetchOrgIndicator(Number(value1.value), e[0], e[1]); | ||
| 499 | + } | ||
| 598 | }; | 500 | }; |
| 599 | 501 | ||
| 600 | -const handleMsgClick = () => { | ||
| 601 | - uni.navigateTo({ url: '/pages-sub/msg/index' }); | 502 | +const getCurrentLocation = (): Promise<{ lon: string; lat: string }> => { |
| 503 | + return new Promise((resolve, reject) => { | ||
| 504 | + uni.getSetting({ | ||
| 505 | + success: (settingRes) => { | ||
| 506 | + if (!settingRes.authSetting['scope.userLocation']) { | ||
| 507 | + uni.authorize({ | ||
| 508 | + scope: 'scope.userLocation', | ||
| 509 | + success: () => { | ||
| 510 | + uni.getLocation({ | ||
| 511 | + type: 'gcj02', | ||
| 512 | + success: (loc) => { | ||
| 513 | + resolve({ | ||
| 514 | + lon: loc.longitude.toFixed(6), | ||
| 515 | + lat: loc.latitude.toFixed(6) | ||
| 516 | + }); | ||
| 517 | + }, | ||
| 518 | + fail: (err) => { | ||
| 519 | + uni.showToast({ title: '获取位置失败,请开启定位', icon: 'none' }); | ||
| 520 | + reject('定位失败'); | ||
| 521 | + } | ||
| 522 | + }); | ||
| 523 | + }, | ||
| 524 | + fail: () => { | ||
| 525 | + uni.showModal({ | ||
| 526 | + title: '需要定位权限', | ||
| 527 | + content: '允许定位才能查看任务距离', | ||
| 528 | + confirmText: '去设置', | ||
| 529 | + success: (modalRes) => { | ||
| 530 | + if (modalRes.confirm) { | ||
| 531 | + uni.openSetting(); | ||
| 532 | + } | ||
| 533 | + } | ||
| 534 | + }); | ||
| 535 | + reject('拒绝定位'); | ||
| 536 | + } | ||
| 537 | + }); | ||
| 538 | + } else { | ||
| 539 | + uni.getLocation({ | ||
| 540 | + type: 'gcj02', | ||
| 541 | + success: (loc) => { | ||
| 542 | + resolve({ | ||
| 543 | + lon: loc.longitude.toFixed(6), | ||
| 544 | + lat: loc.latitude.toFixed(6) | ||
| 545 | + }); | ||
| 546 | + }, | ||
| 547 | + fail: (err) => { | ||
| 548 | + uni.showToast({ title: '获取位置失败,请开启定位', icon: 'none' }); | ||
| 549 | + reject('定位失败'); | ||
| 550 | + } | ||
| 551 | + }); | ||
| 552 | + } | ||
| 553 | + }, | ||
| 554 | + fail: (err) => { | ||
| 555 | + reject('获取权限设置失败'); | ||
| 556 | + } | ||
| 557 | + }); | ||
| 558 | + }); | ||
| 602 | }; | 559 | }; |
| 603 | 560 | ||
| 561 | +const handleMsgClick = () => { | ||
| 562 | + uni.navigateTo({ url: '/pages-sub/msg/msg' }); | ||
| 563 | +}; | ||
| 604 | const handleTaskClick = (item: any) => { | 564 | const handleTaskClick = (item: any) => { |
| 605 | if (item.taskType == "maintain") { | 565 | if (item.taskType == "maintain") { |
| 606 | if (currentTabValue.value === 0) { | 566 | if (currentTabValue.value === 0) { |
| 607 | - // planTypeId / businessType 必须带上:填报页靠这两个参数加载动态字段, | ||
| 608 | - // 缺任一个会直接提示「缺少业务参数」、表单空白(后端的 workTypeId 也会变成 NaN) | ||
| 609 | - uni.navigateTo({ url: `/pages-sub/daily/maintain-manage/add-record?planNo=${item.planNo || ''}&planTypeId=${item.planTypeId ?? ''}&businessType=${item.busiLine ?? ''}` }); | ||
| 610 | - } | ||
| 611 | - if (currentTabValue.value === 1) { | ||
| 612 | - uni.navigateTo({ url: `/pages-sub/daily/maintain-manage/finish-plan-detail?planNo=${item.planNo || ''}` }); | 567 | + uni.navigateTo({ |
| 568 | + url: `/pages-sub/daily/maintain-add?planNo=${item.planNo || ''}&planTypeId=${item.planTypeId ?? ''}&businessLine=${item.busiLine ?? ''}` | ||
| 569 | + }); | ||
| 570 | + } else { | ||
| 571 | + uni.navigateTo({ | ||
| 572 | + url: `/pages-sub/daily/maintain-detail?planNo=${item.planNo || ''}` | ||
| 573 | + }); | ||
| 613 | } | 574 | } |
| 614 | } | 575 | } |
| 615 | if (item.taskType == "inspection") { | 576 | if (item.taskType == "inspection") { |
| 616 | if (currentTabValue.value === 0) { | 577 | if (currentTabValue.value === 0) { |
| 617 | - uni.navigateTo({ url: `/pages-sub/daily/patrol-manage/add-patrol-record?planNo=${item.planNo}&batchNo=${item.batchNo}` }); | ||
| 618 | - } | ||
| 619 | - if (currentTabValue.value === 1) { | ||
| 620 | - uni.navigateTo({ url: `/pages-sub/daily/patrol-manage/finish-plan-detail?planNo=${item.planNo || ''}` }); | 578 | + uni.navigateTo({ |
| 579 | + url: `/pages-sub/daily/inspect-add?planNo=${item.planNo}&batchNo=${item.batchNo}` | ||
| 580 | + }); | ||
| 581 | + } else { | ||
| 582 | + uni.navigateTo({ | ||
| 583 | + url: `/pages-sub/daily/inspect-detail?planNo=${item.planNo || ''}` | ||
| 584 | + }); | ||
| 621 | } | 585 | } |
| 622 | } | 586 | } |
| 623 | }; | 587 | }; |
| 588 | + | ||
| 589 | +onShow(async () => { | ||
| 590 | + showChartArea.value = true; | ||
| 591 | + chartWidth.value = rpx2px(750 - 60); | ||
| 592 | + chartHeight.value = rpx2px(300); | ||
| 593 | + initRecent7Days(); | ||
| 594 | + getUnread(); | ||
| 595 | + await getOrg(); // 拉取组织下拉列表 | ||
| 596 | + await loadAllTaskByDept(); | ||
| 597 | +}); | ||
| 598 | + | ||
| 599 | +onUnload(() => { | ||
| 600 | + if (scrollTimer) clearTimeout(scrollTimer); | ||
| 601 | +}); | ||
| 624 | </script> | 602 | </script> |
| 625 | 603 | ||
| 604 | + | ||
| 605 | + | ||
| 626 | <style scoped lang="scss"> | 606 | <style scoped lang="scss"> |
| 627 | $primary-color: #1677ff; | 607 | $primary-color: #1677ff; |
| 628 | $danger-color: #E53935; | 608 | $danger-color: #E53935; |
pages/mine/index.vue
| @@ -26,6 +26,7 @@ | @@ -26,6 +26,7 @@ | ||
| 26 | <up-cell title="账号" :value="userInfo.username"></up-cell> | 26 | <up-cell title="账号" :value="userInfo.username"></up-cell> |
| 27 | <up-cell title="电话" :value="userInfo.mobile"></up-cell> | 27 | <up-cell title="电话" :value="userInfo.mobile"></up-cell> |
| 28 | <up-cell title="角色" :value="translateRoles(userStore.userInfo.roles)"></up-cell> | 28 | <up-cell title="角色" :value="translateRoles(userStore.userInfo.roles)"></up-cell> |
| 29 | + <up-cell title="考勤" :isLink="true" url="/pages-sub/daily/attendanceRecord"></up-cell> | ||
| 29 | </up-cell-group> | 30 | </up-cell-group> |
| 30 | </view> | 31 | </view> |
| 31 | 32 |
uni_modules/uview-plus/components/u-cascader/calendar.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Author : LQ | ||
| 3 | + * @Description : | ||
| 4 | + * @version : 3.0 | ||
| 5 | + * @Date : 2021-08-20 16:44:21 | ||
| 6 | + * @LastAuthor : jry | ||
| 7 | + * @lastTime : 2025-12-19 08:55:21 | ||
| 8 | + * @FilePath : /uview-plus/libs/config/props/calendar.js | ||
| 9 | + */ | ||
| 10 | +import { t } from '../../libs/i18n' | ||
| 11 | +export default { | ||
| 12 | + // calendar 组件 | ||
| 13 | + calendar: { | ||
| 14 | + title: t("up.calendar.chooseDates"), | ||
| 15 | + showTitle: true, | ||
| 16 | + showSubtitle: true, | ||
| 17 | + mode: 'single', | ||
| 18 | + startText: t("up.common.start"), | ||
| 19 | + endText: t("up.common.end"), | ||
| 20 | + customList: [], | ||
| 21 | + color: '#3c9cff', | ||
| 22 | + minDate: 0, | ||
| 23 | + maxDate: 0, | ||
| 24 | + defaultDate: null, | ||
| 25 | + maxCount: Number.MAX_SAFE_INTEGER, // Infinity | ||
| 26 | + rowHeight: 56, | ||
| 27 | + formatter: null, | ||
| 28 | + showLunar: false, | ||
| 29 | + showMark: true, | ||
| 30 | + confirmText: t("up.common.confirm"), | ||
| 31 | + confirmDisabledText: t("up.common.confirm"), | ||
| 32 | + show: false, | ||
| 33 | + closeOnClickOverlay: false, | ||
| 34 | + readonly: false, | ||
| 35 | + showConfirm: true, | ||
| 36 | + maxRange: Number.MAX_SAFE_INTEGER, // Infinity | ||
| 37 | + rangePrompt: '', | ||
| 38 | + showRangePrompt: true, | ||
| 39 | + allowSameDay: false, | ||
| 40 | + rangeResultMode: 'all', | ||
| 41 | + enableTime: false, | ||
| 42 | + timePrecision: 'minute', | ||
| 43 | + defaultTime: '', | ||
| 44 | + round: 0, | ||
| 45 | + overlay: true, | ||
| 46 | + duration: 300, | ||
| 47 | + overlayStyle: {}, | ||
| 48 | + overlayOpacity: 0.5, | ||
| 49 | + zIndex: 10075, | ||
| 50 | + safeAreaInsetBottom: true, | ||
| 51 | + safeAreaInsetTop: false, | ||
| 52 | + bgColor: '', | ||
| 53 | + monthNum: 3, | ||
| 54 | + monthSwitch: false, | ||
| 55 | + showToday: true, | ||
| 56 | + todayColor: '', | ||
| 57 | + weekText: [t("up.week.one"), t("up.week.two"), t("up.week.three"), t("up.week.four"), t("up.week.five"), t("up.week.six"), t("up.week.seven")], | ||
| 58 | + forbidDays: [], | ||
| 59 | + forbidDaysToast: t("up.calendar.disabled"), | ||
| 60 | + monthFormat: '', | ||
| 61 | + pageInline: false | ||
| 62 | + } | ||
| 63 | +} |
uni_modules/uview-plus/components/u-cascader/header.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="u-calendar-header u-border-bottom"> | ||
| 3 | + <text | ||
| 4 | + class="u-calendar-header__title" | ||
| 5 | + v-if="showTitle" | ||
| 6 | + >{{ title }}</text> | ||
| 7 | + <view | ||
| 8 | + class="u-calendar-header__subtitle-wrap" | ||
| 9 | + v-if="showSubtitle" | ||
| 10 | + > | ||
| 11 | + <text | ||
| 12 | + v-if="showSwitch" | ||
| 13 | + class="u-calendar-header__switch" | ||
| 14 | + :class="{ 'u-calendar-header__switch--disabled': prevYearDisabled }" | ||
| 15 | + @click="prevYear" | ||
| 16 | + >«</text> | ||
| 17 | + <text | ||
| 18 | + v-if="showSwitch" | ||
| 19 | + class="u-calendar-header__switch" | ||
| 20 | + :class="{ 'u-calendar-header__switch--disabled': prevDisabled }" | ||
| 21 | + @click="prev" | ||
| 22 | + >‹</text> | ||
| 23 | + <text class="u-calendar-header__subtitle">{{ subtitle }}</text> | ||
| 24 | + <text | ||
| 25 | + v-if="showSwitch" | ||
| 26 | + class="u-calendar-header__switch" | ||
| 27 | + :class="{ 'u-calendar-header__switch--disabled': nextDisabled }" | ||
| 28 | + @click="next" | ||
| 29 | + >›</text> | ||
| 30 | + <text | ||
| 31 | + v-if="showSwitch" | ||
| 32 | + class="u-calendar-header__switch" | ||
| 33 | + :class="{ 'u-calendar-header__switch--disabled': nextYearDisabled }" | ||
| 34 | + @click="nextYear" | ||
| 35 | + >»</text> | ||
| 36 | + <text | ||
| 37 | + v-if="showToday" | ||
| 38 | + class="u-calendar-header__today" | ||
| 39 | + :class="{ 'u-calendar-header__today--disabled': todayDisabled }" | ||
| 40 | + @click="today" | ||
| 41 | + >{{ todayText }}</text> | ||
| 42 | + </view> | ||
| 43 | + <view class="u-calendar-header__weekdays"> | ||
| 44 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[0] }}</text> | ||
| 45 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[1] }}</text> | ||
| 46 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[2] }}</text> | ||
| 47 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[3] }}</text> | ||
| 48 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[4] }}</text> | ||
| 49 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[5] }}</text> | ||
| 50 | + <text class="u-calendar-header__weekdays__weekday">{{ weekText[6] }}</text> | ||
| 51 | + </view> | ||
| 52 | + </view> | ||
| 53 | +</template> | ||
| 54 | + | ||
| 55 | +<script> | ||
| 56 | + import { mpMixin } from '../../libs/mixin/mpMixin'; | ||
| 57 | + import { mixin } from '../../libs/mixin/mixin'; | ||
| 58 | + export default { | ||
| 59 | + name: 'u-calendar-header', | ||
| 60 | + mixins: [mpMixin, mixin], | ||
| 61 | + props: { | ||
| 62 | + // 标题 | ||
| 63 | + title: { | ||
| 64 | + type: String, | ||
| 65 | + default: '' | ||
| 66 | + }, | ||
| 67 | + // 副标题 | ||
| 68 | + subtitle: { | ||
| 69 | + type: String, | ||
| 70 | + default: '' | ||
| 71 | + }, | ||
| 72 | + // 是否显示标题 | ||
| 73 | + showTitle: { | ||
| 74 | + type: Boolean, | ||
| 75 | + default: true | ||
| 76 | + }, | ||
| 77 | + // 是否显示副标题 | ||
| 78 | + showSubtitle: { | ||
| 79 | + type: Boolean, | ||
| 80 | + default: true | ||
| 81 | + }, | ||
| 82 | + // 星期文本 | ||
| 83 | + weekText: { | ||
| 84 | + type: Array, | ||
| 85 | + default: () => { | ||
| 86 | + return [] | ||
| 87 | + } | ||
| 88 | + }, | ||
| 89 | + // 是否显示月份切换按钮 | ||
| 90 | + showSwitch: { | ||
| 91 | + type: Boolean, | ||
| 92 | + default: false | ||
| 93 | + }, | ||
| 94 | + // 上个月按钮是否禁用 | ||
| 95 | + prevDisabled: { | ||
| 96 | + type: Boolean, | ||
| 97 | + default: false | ||
| 98 | + }, | ||
| 99 | + // 下个月按钮是否禁用 | ||
| 100 | + nextDisabled: { | ||
| 101 | + type: Boolean, | ||
| 102 | + default: false | ||
| 103 | + }, | ||
| 104 | + // 上一年按钮是否禁用 | ||
| 105 | + prevYearDisabled: { | ||
| 106 | + type: Boolean, | ||
| 107 | + default: false | ||
| 108 | + }, | ||
| 109 | + // 下一年按钮是否禁用 | ||
| 110 | + nextYearDisabled: { | ||
| 111 | + type: Boolean, | ||
| 112 | + default: false | ||
| 113 | + }, | ||
| 114 | + // 是否显示今天按钮 | ||
| 115 | + showToday: { | ||
| 116 | + type: Boolean, | ||
| 117 | + default: true | ||
| 118 | + }, | ||
| 119 | + // 今天按钮文案 | ||
| 120 | + todayText: { | ||
| 121 | + type: String, | ||
| 122 | + default: '' | ||
| 123 | + }, | ||
| 124 | + // 今天按钮是否禁用 | ||
| 125 | + todayDisabled: { | ||
| 126 | + type: Boolean, | ||
| 127 | + default: false | ||
| 128 | + } | ||
| 129 | + }, | ||
| 130 | + data() { | ||
| 131 | + return { | ||
| 132 | + | ||
| 133 | + } | ||
| 134 | + }, | ||
| 135 | + methods: { | ||
| 136 | + prev() { | ||
| 137 | + if (!this.prevDisabled) { | ||
| 138 | + this.$emit('prev') | ||
| 139 | + } | ||
| 140 | + }, | ||
| 141 | + next() { | ||
| 142 | + if (!this.nextDisabled) { | ||
| 143 | + this.$emit('next') | ||
| 144 | + } | ||
| 145 | + }, | ||
| 146 | + prevYear() { | ||
| 147 | + if (!this.prevYearDisabled) { | ||
| 148 | + this.$emit('prevYear') | ||
| 149 | + } | ||
| 150 | + }, | ||
| 151 | + nextYear() { | ||
| 152 | + if (!this.nextYearDisabled) { | ||
| 153 | + this.$emit('nextYear') | ||
| 154 | + } | ||
| 155 | + }, | ||
| 156 | + today() { | ||
| 157 | + if (!this.todayDisabled) { | ||
| 158 | + this.$emit('today') | ||
| 159 | + } | ||
| 160 | + } | ||
| 161 | + }, | ||
| 162 | + } | ||
| 163 | +</script> | ||
| 164 | + | ||
| 165 | +<style lang="scss" scoped> | ||
| 166 | + | ||
| 167 | + .u-calendar-header { | ||
| 168 | + display: flex; | ||
| 169 | + flex-direction: column; | ||
| 170 | + padding-bottom: 4px; | ||
| 171 | + | ||
| 172 | + &__title { | ||
| 173 | + font-size: 16px; | ||
| 174 | + color: var(--up-main-color, $u-main-color); | ||
| 175 | + text-align: center; | ||
| 176 | + height: 42px; | ||
| 177 | + line-height: 42px; | ||
| 178 | + font-weight: bold; | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + &__subtitle-wrap { | ||
| 182 | + @include flex; | ||
| 183 | + align-items: center; | ||
| 184 | + justify-content: center; | ||
| 185 | + height: 40px; | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + &__subtitle { | ||
| 189 | + font-size: 14px; | ||
| 190 | + color: var(--up-main-color, $u-main-color); | ||
| 191 | + text-align: center; | ||
| 192 | + font-weight: bold; | ||
| 193 | + min-width: 120px; | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + &__switch { | ||
| 197 | + width: 44px; | ||
| 198 | + height: 40px; | ||
| 199 | + line-height: 40px; | ||
| 200 | + text-align: center; | ||
| 201 | + color: var(--up-main-color, $u-main-color); | ||
| 202 | + font-size: 22px; | ||
| 203 | + font-weight: bold; | ||
| 204 | + flex-shrink: 0; | ||
| 205 | + | ||
| 206 | + &--disabled { | ||
| 207 | + opacity: 0.35; | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + &__weekdays { | ||
| 212 | + @include flex; | ||
| 213 | + justify-content: space-between; | ||
| 214 | + | ||
| 215 | + &__weekday { | ||
| 216 | + font-size: 13px; | ||
| 217 | + color: var(--up-main-color, $u-main-color); | ||
| 218 | + line-height: 30px; | ||
| 219 | + flex: 1; | ||
| 220 | + text-align: center; | ||
| 221 | + } | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + &__today { | ||
| 225 | + margin-left: 4px; | ||
| 226 | + height: 28px; | ||
| 227 | + line-height: 28px; | ||
| 228 | + padding: 0 10px; | ||
| 229 | + border-radius: 14px; | ||
| 230 | + font-size: 13px; | ||
| 231 | + color: var(--up-primary, $u-primary); | ||
| 232 | + border: 1px solid var(--up-primary, $u-primary); | ||
| 233 | + flex-shrink: 0; | ||
| 234 | + | ||
| 235 | + &--disabled { | ||
| 236 | + opacity: 0.35; | ||
| 237 | + } | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | +</style> |
uni_modules/uview-plus/components/u-cascader/month.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper"> | ||
| 3 | + <view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]" | ||
| 4 | + :ref="`u-calendar-month-${index}`" :id="`month-${index}`"> | ||
| 5 | + <text v-if="index !== 0" class="u-calendar-month__title">{{ monthTitle(item) }}</text> | ||
| 6 | + <view class="u-calendar-month__days"> | ||
| 7 | + <view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper"> | ||
| 8 | + <text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text> | ||
| 9 | + </view> | ||
| 10 | + <view class="u-calendar-month__days__day" v-for="(item1, index1) in item.date" :key="index1" | ||
| 11 | + :style="[dayStyle(index, index1, item1)]" @tap="clickHandler(index, index1, item1)" | ||
| 12 | + :class="[item1.selected && 'u-calendar-month__days__day__select--selected']"> | ||
| 13 | + <view class="u-calendar-month__days__day__select" :style="[daySelectStyle(index, index1, item1)]"> | ||
| 14 | + <text class="u-calendar-month__days__day__select__info" | ||
| 15 | + :class="[(item1.disabled || isForbid(item1) ) ? 'u-calendar-month__days__day__select__info--disabled' : '']" | ||
| 16 | + :style="[textStyle(item1)]">{{ item1.day }}</text> | ||
| 17 | + <text v-if="getBottomInfo(index, index1, item1)" | ||
| 18 | + class="u-calendar-month__days__day__select__buttom-info" | ||
| 19 | + :class="[(item1.disabled || isForbid(item1) ) ? 'u-calendar-month__days__day__select__buttom-info--disabled' : '']" | ||
| 20 | + :style="[textStyle(item1)]">{{ getBottomInfo(index, index1, item1) }}</text> | ||
| 21 | + <text v-if="item1.dot" class="u-calendar-month__days__day__select__dot"></text> | ||
| 22 | + </view> | ||
| 23 | + </view> | ||
| 24 | + </view> | ||
| 25 | + </view> | ||
| 26 | + </view> | ||
| 27 | +</template> | ||
| 28 | + | ||
| 29 | +<script> | ||
| 30 | + // #ifdef APP-NVUE | ||
| 31 | + // 由于nvue不支持百分比单位,需要查询宽度来计算每个日期的宽度 | ||
| 32 | + const dom = uni.requireNativePlugin('dom') | ||
| 33 | + // #endif | ||
| 34 | + import { mpMixin } from '../../libs/mixin/mpMixin'; | ||
| 35 | + import { mixin } from '../../libs/mixin/mixin'; | ||
| 36 | + import { addUnit, deepClone, toast, sleep, getWindowInfo } from '../../libs/function/index'; | ||
| 37 | + import { colorGradient } from '../../libs/function/colorGradient'; | ||
| 38 | + import test from '../../libs/function/test'; | ||
| 39 | + import defProps from '../../libs/config/props'; | ||
| 40 | + import dayjs from '../u-datetime-picker/dayjs.esm.min.js'; | ||
| 41 | + import { t } from '../../libs/i18n' | ||
| 42 | + // 已经是YYYY-MM-DD形式的日期字符串,无需再交给dayjs解析 | ||
| 43 | + const DATE_STR_REG = /^\d{4}-\d{2}-\d{2}$/ | ||
| 44 | + export default { | ||
| 45 | + name: 'u-calendar-month', | ||
| 46 | + mixins: [mpMixin, mixin], | ||
| 47 | + props: { | ||
| 48 | + // 是否显示月份背景色 | ||
| 49 | + showMark: { | ||
| 50 | + type: Boolean, | ||
| 51 | + default: true | ||
| 52 | + }, | ||
| 53 | + // 主题色,对底部按钮和选中日期有效 | ||
| 54 | + color: { | ||
| 55 | + type: String, | ||
| 56 | + default: '#3c9cff' | ||
| 57 | + }, | ||
| 58 | + // 月份数据 | ||
| 59 | + months: { | ||
| 60 | + type: Array, | ||
| 61 | + default: () => [] | ||
| 62 | + }, | ||
| 63 | + // 日期选择类型 | ||
| 64 | + mode: { | ||
| 65 | + type: String, | ||
| 66 | + default: 'single' | ||
| 67 | + }, | ||
| 68 | + // 日期行高 | ||
| 69 | + rowHeight: { | ||
| 70 | + type: [String, Number], | ||
| 71 | + default: 58 | ||
| 72 | + }, | ||
| 73 | + // mode=multiple时,最多可选多少个日期 | ||
| 74 | + maxCount: { | ||
| 75 | + type: [String, Number], | ||
| 76 | + default: Infinity | ||
| 77 | + }, | ||
| 78 | + // mode=range时,第一个日期底部的提示文字 | ||
| 79 | + startText: { | ||
| 80 | + type: String, | ||
| 81 | + default: '开始' | ||
| 82 | + }, | ||
| 83 | + // mode=range时,最后一个日期底部的提示文字 | ||
| 84 | + endText: { | ||
| 85 | + type: String, | ||
| 86 | + default: '结束' | ||
| 87 | + }, | ||
| 88 | + // 默认选中的日期,mode为multiple或range是必须为数组格式 | ||
| 89 | + defaultDate: { | ||
| 90 | + type: [Array, String, Date], | ||
| 91 | + default: null | ||
| 92 | + }, | ||
| 93 | + // 最小的可选日期 | ||
| 94 | + minDate: { | ||
| 95 | + type: [String, Number], | ||
| 96 | + default: 0 | ||
| 97 | + }, | ||
| 98 | + // 最大可选日期 | ||
| 99 | + maxDate: { | ||
| 100 | + type: [String, Number], | ||
| 101 | + default: 0 | ||
| 102 | + }, | ||
| 103 | + // 如果没有设置maxDate,则往后推多少个月 | ||
| 104 | + maxMonth: { | ||
| 105 | + type: [String, Number], | ||
| 106 | + default: 2 | ||
| 107 | + }, | ||
| 108 | + // 是否为只读状态,只读状态下禁止选择日期 | ||
| 109 | + readonly: { | ||
| 110 | + type: Boolean, | ||
| 111 | + default: () => defProps.calendar.readonly | ||
| 112 | + }, | ||
| 113 | + // 日期区间最多可选天数,默认无限制,mode = range时有效 | ||
| 114 | + maxRange: { | ||
| 115 | + type: [Number, String], | ||
| 116 | + default: Infinity | ||
| 117 | + }, | ||
| 118 | + // 范围选择超过最多可选天数时的提示文案,mode = range时有效 | ||
| 119 | + rangePrompt: { | ||
| 120 | + type: String, | ||
| 121 | + default: '' | ||
| 122 | + }, | ||
| 123 | + // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 | ||
| 124 | + showRangePrompt: { | ||
| 125 | + type: Boolean, | ||
| 126 | + default: true | ||
| 127 | + }, | ||
| 128 | + // 是否允许日期范围的起止时间为同一天,mode = range时有效 | ||
| 129 | + allowSameDay: { | ||
| 130 | + type: Boolean, | ||
| 131 | + default: false | ||
| 132 | + }, | ||
| 133 | + forbidDays: { | ||
| 134 | + type: Array, | ||
| 135 | + default: () => [] | ||
| 136 | + }, | ||
| 137 | + forbidDaysToast: { | ||
| 138 | + type: String, | ||
| 139 | + default: '' | ||
| 140 | + }, | ||
| 141 | + // 今天日期,用于独立高亮 | ||
| 142 | + todayDate: { | ||
| 143 | + type: String, | ||
| 144 | + default: '' | ||
| 145 | + }, | ||
| 146 | + // 今天日期的独立高亮颜色 | ||
| 147 | + todayColor: { | ||
| 148 | + type: String, | ||
| 149 | + default: '' | ||
| 150 | + } | ||
| 151 | + }, | ||
| 152 | + data() { | ||
| 153 | + return { | ||
| 154 | + // 每个日期的宽度 | ||
| 155 | + width: 0, | ||
| 156 | + // 当前选中的日期item | ||
| 157 | + item: {}, | ||
| 158 | + selected: [] | ||
| 159 | + } | ||
| 160 | + }, | ||
| 161 | + watch: { | ||
| 162 | + selectedChange: { | ||
| 163 | + immediate: true, | ||
| 164 | + handler(n) { | ||
| 165 | + this.setDefaultDate() | ||
| 166 | + } | ||
| 167 | + } | ||
| 168 | + }, | ||
| 169 | + computed: { | ||
| 170 | + // 多个条件的变化,会引起选中日期的变化,这里统一管理监听 | ||
| 171 | + selectedChange() { | ||
| 172 | + return [this.minDate, this.maxDate, this.defaultDate] | ||
| 173 | + }, | ||
| 174 | + // selected中的元素可能是用户通过defaultDate传入的原始值(未补零的字符串、 | ||
| 175 | + // Date对象、时间戳等),这里统一归一化一次,避免在每个日期格子里反复解析 | ||
| 176 | + selectedDates() { | ||
| 177 | + return this.selected.map((item) => this.formatDate(item)) | ||
| 178 | + }, | ||
| 179 | + // 供渲染期做O(1)的选中判断,取代原先每个格子都要遍历selected的dayjs比较 | ||
| 180 | + selectedDateMap() { | ||
| 181 | + const map = {} | ||
| 182 | + this.selectedDates.forEach((date) => { | ||
| 183 | + map[date] = true | ||
| 184 | + }) | ||
| 185 | + return map | ||
| 186 | + }, | ||
| 187 | + // 区间模式的起止日期 | ||
| 188 | + selectedStart() { | ||
| 189 | + return this.selectedDates[0] | ||
| 190 | + }, | ||
| 191 | + selectedEnd() { | ||
| 192 | + return this.selectedDates[this.selectedDates.length - 1] | ||
| 193 | + }, | ||
| 194 | + normalizedTodayDate() { | ||
| 195 | + return this.todayDate ? this.formatDate(this.todayDate) : '' | ||
| 196 | + }, | ||
| 197 | + dayStyle(index1, index2, item) { | ||
| 198 | + return (index1, index2, item) => { | ||
| 199 | + const style = {} | ||
| 200 | + let week = item.week | ||
| 201 | + // 隐藏挂载场景下节点宽度可能拿到0,兜底使用窗口宽度避免首日偏移归零 | ||
| 202 | + const wrapperWidth = this.width > 0 ? this.width : (getWindowInfo().windowWidth || 0) | ||
| 203 | + // 不进行四舍五入的形式保留2位小数 | ||
| 204 | + const dayWidth = Number(parseFloat(wrapperWidth / 7).toFixed(3).slice(0, -1)) | ||
| 205 | + // 得出每个日期的宽度 | ||
| 206 | + // #ifdef APP-NVUE | ||
| 207 | + style.width = addUnit(dayWidth, 'px') | ||
| 208 | + // #endif | ||
| 209 | + style.height = addUnit(this.rowHeight, 'px') | ||
| 210 | + if (index2 === 0) { | ||
| 211 | + // 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数 | ||
| 212 | + week = (week === 0 ? 7 : week) - 1 | ||
| 213 | + // #ifdef APP-NVUE | ||
| 214 | + style.marginLeft = addUnit(week * dayWidth, 'px') | ||
| 215 | + // #endif | ||
| 216 | + // #ifndef APP-NVUE | ||
| 217 | + style.marginLeft = `${(week / 7) * 100}%` | ||
| 218 | + // #endif | ||
| 219 | + } | ||
| 220 | + if (this.mode === 'range') { | ||
| 221 | + // 之所以需要这么写,是因为DCloud公司的iOS客户端导致的bug | ||
| 222 | + style.paddingLeft = 0 | ||
| 223 | + style.paddingRight = 0 | ||
| 224 | + style.paddingBottom = 0 | ||
| 225 | + style.paddingTop = 0 | ||
| 226 | + } | ||
| 227 | + return style | ||
| 228 | + } | ||
| 229 | + }, | ||
| 230 | + daySelectStyle() { | ||
| 231 | + return (index1, index2, item) => { | ||
| 232 | + let date = this.dateOf(item), | ||
| 233 | + style = {} | ||
| 234 | + // selected已归一化为YYYY-MM-DD(月份可能需要补0),故可直接比较字符串 | ||
| 235 | + if (this.selectedDateMap[date]) { | ||
| 236 | + style.backgroundColor = this.color | ||
| 237 | + } | ||
| 238 | + if (this.normalizedTodayDate && date === this.normalizedTodayDate) { | ||
| 239 | + style.border = `1px solid ${this.resolvedTodayColor}` | ||
| 240 | + style.boxSizing = 'border-box' | ||
| 241 | + } | ||
| 242 | + if (this.mode === 'single') { | ||
| 243 | + if (date === this.selectedStart) { | ||
| 244 | + // 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等 | ||
| 245 | + style.borderTopLeftRadius = '3px' | ||
| 246 | + style.borderBottomLeftRadius = '3px' | ||
| 247 | + style.borderTopRightRadius = '3px' | ||
| 248 | + style.borderBottomRightRadius = '3px' | ||
| 249 | + } | ||
| 250 | + } else if (this.mode === 'range') { | ||
| 251 | + if (this.selectedDates.length >= 2) { | ||
| 252 | + // 第一个日期设置左上角和左下角的圆角 | ||
| 253 | + if (date === this.selectedStart) { | ||
| 254 | + style.borderTopLeftRadius = '3px' | ||
| 255 | + style.borderBottomLeftRadius = '3px' | ||
| 256 | + } | ||
| 257 | + // 最后一个日期设置右上角和右下角的圆角 | ||
| 258 | + if (date === this.selectedEnd) { | ||
| 259 | + style.borderTopRightRadius = '3px' | ||
| 260 | + style.borderBottomRightRadius = '3px' | ||
| 261 | + } | ||
| 262 | + // 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值 | ||
| 263 | + if (date > this.selectedStart && date < this.selectedEnd) { | ||
| 264 | + style.backgroundColor = this.rangeMiddleColor | ||
| 265 | + // 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符 | ||
| 266 | + style.opacity = this.upThemeIsDark ? 0.85 : 0.7 | ||
| 267 | + } | ||
| 268 | + } else if (this.selectedDates.length === 1) { | ||
| 269 | + // 之所以需要这么写,是因为uni-app的iOS客户端的bug | ||
| 270 | + // 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现 | ||
| 271 | + style.borderTopLeftRadius = '3px' | ||
| 272 | + style.borderBottomLeftRadius = '3px' | ||
| 273 | + } | ||
| 274 | + } else { | ||
| 275 | + if (this.selectedDateMap[date]) { | ||
| 276 | + style.borderTopLeftRadius = '3px' | ||
| 277 | + style.borderBottomLeftRadius = '3px' | ||
| 278 | + style.borderTopRightRadius = '3px' | ||
| 279 | + style.borderBottomRightRadius = '3px' | ||
| 280 | + } | ||
| 281 | + } | ||
| 282 | + return style | ||
| 283 | + } | ||
| 284 | + }, | ||
| 285 | + // 区间中间日期的背景色与格子无关,按整体算一次即可, | ||
| 286 | + // 否则区间内每个格子都要做一遍100等分的颜色渐变计算 | ||
| 287 | + rangeMiddleColor() { | ||
| 288 | + const rangeEndColor = this.upThemeVar('--up-card-bg-color', this.upThemeIsDark ? '#1c1c1e' : '#ffffff') | ||
| 289 | + return colorGradient(this.color, rangeEndColor, 100)[90] | ||
| 290 | + }, | ||
| 291 | + resolvedTodayColor() { | ||
| 292 | + return this.todayColor || this.color | ||
| 293 | + }, | ||
| 294 | + // 某个日期是否被选中 | ||
| 295 | + textStyle() { | ||
| 296 | + return (item) => { | ||
| 297 | + const date = this.dateOf(item), | ||
| 298 | + style = {} | ||
| 299 | + // 选中的日期,提示文字设置白色 | ||
| 300 | + if (this.selectedDateMap[date]) { | ||
| 301 | + style.color = '#ffffff' | ||
| 302 | + } | ||
| 303 | + if (this.mode === 'range') { | ||
| 304 | + // 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色 | ||
| 305 | + if (date > this.selectedStart && date < this.selectedEnd) { | ||
| 306 | + style.color = this.color | ||
| 307 | + } | ||
| 308 | + } | ||
| 309 | + if (this.normalizedTodayDate && date === this.normalizedTodayDate && !this.selectedDateMap[date]) { | ||
| 310 | + style.color = this.resolvedTodayColor | ||
| 311 | + } | ||
| 312 | + return style | ||
| 313 | + } | ||
| 314 | + }, | ||
| 315 | + // 获取底部的提示文字 | ||
| 316 | + getBottomInfo() { | ||
| 317 | + return (index1, index2, item) => { | ||
| 318 | + const date = this.dateOf(item) | ||
| 319 | + const bottomInfo = item.bottomInfo | ||
| 320 | + // 当为日期范围模式时,且选择的日期个数大于0时 | ||
| 321 | + if (this.mode === 'range' && this.selectedDates.length > 0) { | ||
| 322 | + if (this.selectedDates.length === 1) { | ||
| 323 | + // 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始” | ||
| 324 | + if (date === this.selectedStart) return this.startText | ||
| 325 | + else return bottomInfo | ||
| 326 | + } else { | ||
| 327 | + // 如果数组中的日期大于2个时,第一个和最后一个显示为开始和结束日期 | ||
| 328 | + if (this.selectedDates.length === 2 && date === this.selectedStart && date === this | ||
| 329 | + .selectedEnd) { | ||
| 330 | + // 如果长度为2,且第一个等于第二个日期,则提示语放在同一个item中 | ||
| 331 | + return `${this.startText}/${this.endText}` | ||
| 332 | + } else if (date === this.selectedStart) { | ||
| 333 | + return this.startText | ||
| 334 | + } else if (date === this.selectedEnd) { | ||
| 335 | + return this.endText | ||
| 336 | + } else { | ||
| 337 | + return bottomInfo | ||
| 338 | + } | ||
| 339 | + } | ||
| 340 | + } else { | ||
| 341 | + return bottomInfo | ||
| 342 | + } | ||
| 343 | + } | ||
| 344 | + } | ||
| 345 | + }, | ||
| 346 | + mounted() { | ||
| 347 | + this.init() | ||
| 348 | + }, | ||
| 349 | + emits: ['monthSelected', 'updateMonthTop'], | ||
| 350 | + methods: { | ||
| 351 | + init() { | ||
| 352 | + // 初始化默认选中 | ||
| 353 | + this.$emit('monthSelected', this.selected) | ||
| 354 | + this.$nextTick(() => { | ||
| 355 | + // 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度 | ||
| 356 | + // 因为nvue下,$nextTick并不是100%可靠的 | ||
| 357 | + sleep(10).then(() => { | ||
| 358 | + this.getWrapperWidth() | ||
| 359 | + this.getMonthRect() | ||
| 360 | + }) | ||
| 361 | + }) | ||
| 362 | + }, | ||
| 363 | + monthTitle(item) { | ||
| 364 | + if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') { | ||
| 365 | + return item.year + '年' + (item.month < 10 ? '0' + item.month : item.month) + '月' | ||
| 366 | + } else { | ||
| 367 | + return (item.month < 10 ? '0' + item.month : item.month) + '/' + item.year | ||
| 368 | + } | ||
| 369 | + }, | ||
| 370 | + isForbid(item) { | ||
| 371 | + if (this.mode === 'range' || !this.forbidDays.length) { | ||
| 372 | + return false | ||
| 373 | + } | ||
| 374 | + return this.forbidDays.includes(this.dateOf(item)) | ||
| 375 | + }, | ||
| 376 | + // 归一化为YYYY-MM-DD,已经是该形式的字符串则直接返回,省掉一次dayjs解析 | ||
| 377 | + formatDate(value) { | ||
| 378 | + if (typeof value === 'string' && DATE_STR_REG.test(value)) { | ||
| 379 | + return value | ||
| 380 | + } | ||
| 381 | + return dayjs(value).format("YYYY-MM-DD") | ||
| 382 | + }, | ||
| 383 | + // 取日期格子的日期字符串,父组件生成数据时已经算好; | ||
| 384 | + // 兼容自定义formatter返回了不带dateStr的新对象的情况 | ||
| 385 | + dateOf(item) { | ||
| 386 | + return item.dateStr || dayjs(item.date).format("YYYY-MM-DD") | ||
| 387 | + }, | ||
| 388 | + // 判断两个日期是否相等 | ||
| 389 | + dateSame(date1, date2) { | ||
| 390 | + return dayjs(date1).isSame(dayjs(date2)) | ||
| 391 | + }, | ||
| 392 | + isSelectedDate(date) { | ||
| 393 | + return !!this.selectedDateMap[this.formatDate(date)] | ||
| 394 | + }, | ||
| 395 | + // 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度 | ||
| 396 | + getWrapperWidth() { | ||
| 397 | + // #ifdef APP-NVUE | ||
| 398 | + dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => { | ||
| 399 | + const width = res && res.size ? Number(res.size.width) : 0 | ||
| 400 | + this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0) | ||
| 401 | + }) | ||
| 402 | + // #endif | ||
| 403 | + // #ifndef APP-NVUE | ||
| 404 | + this.$uGetRect('.u-calendar-month-wrapper').then(size => { | ||
| 405 | + const width = size ? Number(size.width) : 0 | ||
| 406 | + this.width = width > 0 ? width : (getWindowInfo().windowWidth || 0) | ||
| 407 | + }) | ||
| 408 | + // #endif | ||
| 409 | + }, | ||
| 410 | + getMonthRect() { | ||
| 411 | + // 获取每个月份数据的尺寸,用于父组件在scroll-view滚动事件中,监听当前滚动到了第几个月份 | ||
| 412 | + const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise( | ||
| 413 | + `u-calendar-month-${index}`)) | ||
| 414 | + // 一次性返回 | ||
| 415 | + Promise.all(promiseAllArr).then( | ||
| 416 | + sizes => { | ||
| 417 | + let height = 1 | ||
| 418 | + const topArr = [] | ||
| 419 | + for (let i = 0; i < this.months.length; i++) { | ||
| 420 | + // 添加到months数组中,供scroll-view滚动事件中,判断当前滚动到哪个月份 | ||
| 421 | + topArr[i] = height | ||
| 422 | + height += sizes[i].height | ||
| 423 | + } | ||
| 424 | + // 由于微信下,无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值,所以使用事件形式对外发出 | ||
| 425 | + this.$emit('updateMonthTop', topArr) | ||
| 426 | + }) | ||
| 427 | + }, | ||
| 428 | + // 获取每个月份区域的尺寸 | ||
| 429 | + getMonthRectByPromise(el) { | ||
| 430 | + // #ifndef APP-NVUE | ||
| 431 | + // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://uview-plus.jiangruyi.com/js/getRect.html | ||
| 432 | + // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同 | ||
| 433 | + return new Promise(resolve => { | ||
| 434 | + this.$uGetRect(`.${el}`).then(size => { | ||
| 435 | + resolve(size) | ||
| 436 | + }) | ||
| 437 | + }) | ||
| 438 | + // #endif | ||
| 439 | + | ||
| 440 | + // #ifdef APP-NVUE | ||
| 441 | + // nvue下,使用dom模块查询元素高度 | ||
| 442 | + // 返回一个promise,让调用此方法的主体能使用then回调 | ||
| 443 | + return new Promise(resolve => { | ||
| 444 | + dom.getComponentRect(this.$refs[el][0], res => { | ||
| 445 | + resolve(res.size) | ||
| 446 | + }) | ||
| 447 | + }) | ||
| 448 | + // #endif | ||
| 449 | + }, | ||
| 450 | + // 点击某一个日期 | ||
| 451 | + clickHandler(index1, index2, item) { | ||
| 452 | + if (this.readonly) { | ||
| 453 | + return; | ||
| 454 | + } | ||
| 455 | + this.item = item | ||
| 456 | + const date = dayjs(item.date).format("YYYY-MM-DD") | ||
| 457 | + if (item.disabled) return | ||
| 458 | + if (this.isForbid(item)) { | ||
| 459 | + uni.showToast({ | ||
| 460 | + title: this.forbidDaysToast | ||
| 461 | + }) | ||
| 462 | + return | ||
| 463 | + } | ||
| 464 | + // 对上一次选择的日期数组进行深度克隆 | ||
| 465 | + let selected = deepClone(this.selected) | ||
| 466 | + if (this.mode === 'single') { | ||
| 467 | + // 单选情况下,让数组中的元素为当前点击的日期 | ||
| 468 | + selected = [date] | ||
| 469 | + } else if (this.mode === 'multiple') { | ||
| 470 | + if (selected.some(item => this.dateSame(item, date))) { | ||
| 471 | + // 如果点击的日期已在数组中,则进行移除操作,也就是达到反选的效果 | ||
| 472 | + const itemIndex = selected.findIndex(item => item === date) | ||
| 473 | + selected.splice(itemIndex, 1) | ||
| 474 | + } else { | ||
| 475 | + // 如果点击的日期不在数组中,且已有的长度小于总可选长度时,则添加到数组中去 | ||
| 476 | + if (selected.length < this.maxCount) selected.push(date) | ||
| 477 | + } | ||
| 478 | + } else { | ||
| 479 | + // 选择区间形式 | ||
| 480 | + if (selected.length === 0 || selected.length >= 2) { | ||
| 481 | + // 如果原来就为0或者大于2的长度,则当前点击的日期,就是开始日期 | ||
| 482 | + selected = [date] | ||
| 483 | + } else if (selected.length === 1) { | ||
| 484 | + // 如果已经选择了开始日期 | ||
| 485 | + const existsDate = selected[0] | ||
| 486 | + // 如果当前选择的日期小于上一次选择的日期,则当前的日期定为开始日期 | ||
| 487 | + if (dayjs(date).isBefore(existsDate)) { | ||
| 488 | + selected = [date] | ||
| 489 | + } else if (dayjs(date).isAfter(existsDate)) { | ||
| 490 | + // 当前日期减去最大可选的日期天数,如果大于起始时间,则进行提示 | ||
| 491 | + if(dayjs(dayjs(date).subtract(this.maxRange, 'day')).isAfter(dayjs(selected[0])) && this.showRangePrompt) { | ||
| 492 | + if(this.rangePrompt) { | ||
| 493 | + toast(this.rangePrompt) | ||
| 494 | + } else { | ||
| 495 | + toast(t("up.calendar.daysExceed", { days: this.maxRange })) | ||
| 496 | + } | ||
| 497 | + return | ||
| 498 | + } | ||
| 499 | + // 如果当前日期大于已有日期,将当前的添加到数组尾部 | ||
| 500 | + selected.push(date) | ||
| 501 | + const startDate = selected[0] | ||
| 502 | + const endDate = selected[1] | ||
| 503 | + const arr = [] | ||
| 504 | + let i = 0 | ||
| 505 | + do { | ||
| 506 | + // 将开始和结束日期之间的日期添加到数组中 | ||
| 507 | + arr.push(dayjs(startDate).add(i, 'day').format("YYYY-MM-DD")) | ||
| 508 | + i++ | ||
| 509 | + // 累加的日期小于结束日期时,继续下一次的循环 | ||
| 510 | + } while (dayjs(startDate).add(i, 'day').isBefore(dayjs(endDate))) | ||
| 511 | + // 为了一次性修改数组,避免computed中多次触发,这里才用arr变量一次性赋值的方式,同时将最后一个日期添加近来 | ||
| 512 | + arr.push(endDate) | ||
| 513 | + selected = arr | ||
| 514 | + } else { | ||
| 515 | + // 选择区间时,只有一个日期的情况下,且不允许选择起止为同一天的话,不允许选择自己 | ||
| 516 | + if (selected[0] === date && !this.allowSameDay) return | ||
| 517 | + selected.push(date) | ||
| 518 | + } | ||
| 519 | + } | ||
| 520 | + } | ||
| 521 | + this.setSelected(selected) | ||
| 522 | + }, | ||
| 523 | + // 设置默认日期 | ||
| 524 | + setDefaultDate() { | ||
| 525 | + if (!this.defaultDate) { | ||
| 526 | + // 如果没有设置默认日期,则将当天日期设置为默认选中的日期 | ||
| 527 | + const selected = [dayjs().format("YYYY-MM-DD")] | ||
| 528 | + return this.setSelected(selected, false) | ||
| 529 | + } | ||
| 530 | + let defaultDate = [] | ||
| 531 | + const minDate = this.minDate || dayjs().format("YYYY-MM-DD") | ||
| 532 | + const maxDate = this.maxDate || dayjs(minDate).add(this.maxMonth - 1, 'month').format("YYYY-MM-DD") | ||
| 533 | + if (this.mode === 'single') { | ||
| 534 | + // 单选模式,可以是字符串或数组,Date对象等 | ||
| 535 | + if (!test.array(this.defaultDate)) { | ||
| 536 | + defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")] | ||
| 537 | + } else { | ||
| 538 | + defaultDate = [this.defaultDate[0]] | ||
| 539 | + } | ||
| 540 | + } else { | ||
| 541 | + // 如果为非数组,则不执行 | ||
| 542 | + if (!test.array(this.defaultDate)) return | ||
| 543 | + defaultDate = this.defaultDate | ||
| 544 | + } | ||
| 545 | + // 过滤用户传递的默认数组,取出只在可允许最大值与最小值之间的元素 | ||
| 546 | + defaultDate = defaultDate.filter(item => { | ||
| 547 | + return dayjs(item).isAfter(dayjs(minDate).subtract(1, 'day')) && dayjs(item).isBefore(dayjs( | ||
| 548 | + maxDate).add(1, 'day')) | ||
| 549 | + }) | ||
| 550 | + this.setSelected(defaultDate, false) | ||
| 551 | + }, | ||
| 552 | + setSelected(selected, event = true) { | ||
| 553 | + this.selected = selected | ||
| 554 | + event && this.$emit('monthSelected', this.selected,'tap') | ||
| 555 | + }, | ||
| 556 | + selectDate(date) { | ||
| 557 | + const targetDate = dayjs(date).format("YYYY-MM-DD") | ||
| 558 | + for (let monthIndex = 0; monthIndex < this.months.length; monthIndex++) { | ||
| 559 | + const dayIndex = this.months[monthIndex].date.findIndex(item => { | ||
| 560 | + return this.dateSame(item.date, targetDate) | ||
| 561 | + }) | ||
| 562 | + if (dayIndex !== -1) { | ||
| 563 | + this.clickHandler(monthIndex, dayIndex, this.months[monthIndex].date[dayIndex]) | ||
| 564 | + return | ||
| 565 | + } | ||
| 566 | + } | ||
| 567 | + } | ||
| 568 | + } | ||
| 569 | + } | ||
| 570 | +</script> | ||
| 571 | + | ||
| 572 | +<style lang="scss" scoped> | ||
| 573 | + | ||
| 574 | + .u-calendar-month-wrapper { | ||
| 575 | + margin-top: 4px; | ||
| 576 | + } | ||
| 577 | + | ||
| 578 | + .u-calendar-month { | ||
| 579 | + | ||
| 580 | + &__title { | ||
| 581 | + display: flex; | ||
| 582 | + flex-direction: column; | ||
| 583 | + font-size: 14px; | ||
| 584 | + line-height: 42px; | ||
| 585 | + height: 42px; | ||
| 586 | + color: var(--up-main-color, $u-main-color); | ||
| 587 | + text-align: center; | ||
| 588 | + font-weight: bold; | ||
| 589 | + } | ||
| 590 | + | ||
| 591 | + &__days { | ||
| 592 | + position: relative; | ||
| 593 | + @include flex; | ||
| 594 | + flex-wrap: wrap; | ||
| 595 | + | ||
| 596 | + &__month-mark-wrapper { | ||
| 597 | + position: absolute; | ||
| 598 | + top: 0; | ||
| 599 | + bottom: 0; | ||
| 600 | + left: 0; | ||
| 601 | + right: 0; | ||
| 602 | + @include flex; | ||
| 603 | + justify-content: center; | ||
| 604 | + align-items: center; | ||
| 605 | + | ||
| 606 | + &__text { | ||
| 607 | + font-size: 155px; | ||
| 608 | + color: var(--up-calendar-month-mark-color, rgba(231, 232, 234, 0.83)); | ||
| 609 | + } | ||
| 610 | + } | ||
| 611 | + | ||
| 612 | + &__day { | ||
| 613 | + @include flex; | ||
| 614 | + padding: 2px; | ||
| 615 | + /* #ifndef APP-NVUE */ | ||
| 616 | + // vue下使用css进行宽度计算,因为某些安卓机会无法进行js获取父元素宽度进行计算得出,会有偏移 | ||
| 617 | + width: calc(100% / 7); | ||
| 618 | + box-sizing: border-box; | ||
| 619 | + /* #endif */ | ||
| 620 | + | ||
| 621 | + &__select { | ||
| 622 | + flex: 1; | ||
| 623 | + @include flex; | ||
| 624 | + align-items: center; | ||
| 625 | + justify-content: center; | ||
| 626 | + position: relative; | ||
| 627 | + | ||
| 628 | + &__dot { | ||
| 629 | + width: 7px; | ||
| 630 | + height: 7px; | ||
| 631 | + border-radius: 100px; | ||
| 632 | + background-color: $u-error; | ||
| 633 | + position: absolute; | ||
| 634 | + top: 12px; | ||
| 635 | + right: 7px; | ||
| 636 | + } | ||
| 637 | + | ||
| 638 | + &__buttom-info { | ||
| 639 | + color: var(--up-content-color, $u-content-color); | ||
| 640 | + text-align: center; | ||
| 641 | + position: absolute; | ||
| 642 | + bottom: 5px; | ||
| 643 | + font-size: 10px; | ||
| 644 | + text-align: center; | ||
| 645 | + left: 0; | ||
| 646 | + right: 0; | ||
| 647 | + | ||
| 648 | + &--selected { | ||
| 649 | + color: #ffffff; | ||
| 650 | + } | ||
| 651 | + | ||
| 652 | + &--disabled { | ||
| 653 | + color: var(--up-disabled-color, #cacbcd); | ||
| 654 | + } | ||
| 655 | + } | ||
| 656 | + | ||
| 657 | + &__info { | ||
| 658 | + text-align: center; | ||
| 659 | + font-size: 16px; | ||
| 660 | + color: var(--up-main-color, $u-main-color); | ||
| 661 | + | ||
| 662 | + &--selected { | ||
| 663 | + color: #ffffff; | ||
| 664 | + } | ||
| 665 | + | ||
| 666 | + &--disabled { | ||
| 667 | + color: var(--up-disabled-color, #cacbcd); | ||
| 668 | + } | ||
| 669 | + } | ||
| 670 | + | ||
| 671 | + &--selected { | ||
| 672 | + background-color: $u-primary; | ||
| 673 | + @include flex; | ||
| 674 | + justify-content: center; | ||
| 675 | + align-items: center; | ||
| 676 | + flex: 1; | ||
| 677 | + border-radius: 3px; | ||
| 678 | + } | ||
| 679 | + | ||
| 680 | + &--range-selected { | ||
| 681 | + opacity: 0.3; | ||
| 682 | + border-radius: 0; | ||
| 683 | + } | ||
| 684 | + | ||
| 685 | + &--range-start-selected { | ||
| 686 | + border-top-right-radius: 0; | ||
| 687 | + border-bottom-right-radius: 0; | ||
| 688 | + } | ||
| 689 | + | ||
| 690 | + &--range-end-selected { | ||
| 691 | + border-top-left-radius: 0; | ||
| 692 | + border-bottom-left-radius: 0; | ||
| 693 | + } | ||
| 694 | + } | ||
| 695 | + } | ||
| 696 | + } | ||
| 697 | + } | ||
| 698 | + | ||
| 699 | +</style> |
uni_modules/uview-plus/components/u-cascader/props.js
0 → 100644
| 1 | +import { defineMixin } from '../../libs/vue.js' | ||
| 2 | +import CalendarDefaultProps from './calendar.js' | ||
| 3 | +import { registerComponentProps } from '../../libs/config/props.js' | ||
| 4 | + | ||
| 5 | +const defProps = registerComponentProps(CalendarDefaultProps) | ||
| 6 | +export const props = defineMixin({ | ||
| 7 | + props: { | ||
| 8 | + // 日历顶部标题 | ||
| 9 | + title: { | ||
| 10 | + type: String, | ||
| 11 | + default: () => defProps.calendar.title | ||
| 12 | + }, | ||
| 13 | + // 是否显示标题 | ||
| 14 | + showTitle: { | ||
| 15 | + type: Boolean, | ||
| 16 | + default: () => defProps.calendar.showTitle | ||
| 17 | + }, | ||
| 18 | + // 是否显示副标题 | ||
| 19 | + showSubtitle: { | ||
| 20 | + type: Boolean, | ||
| 21 | + default: () => defProps.calendar.showSubtitle | ||
| 22 | + }, | ||
| 23 | + // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 | ||
| 24 | + mode: { | ||
| 25 | + type: String, | ||
| 26 | + default: () => defProps.calendar.mode | ||
| 27 | + }, | ||
| 28 | + // mode=range时,第一个日期底部的提示文字 | ||
| 29 | + startText: { | ||
| 30 | + type: String, | ||
| 31 | + default: () => defProps.calendar.startText | ||
| 32 | + }, | ||
| 33 | + // mode=range时,最后一个日期底部的提示文字 | ||
| 34 | + endText: { | ||
| 35 | + type: String, | ||
| 36 | + default: () => defProps.calendar.endText | ||
| 37 | + }, | ||
| 38 | + // 自定义列表 | ||
| 39 | + customList: { | ||
| 40 | + type: Array, | ||
| 41 | + default: () => defProps.calendar.customList | ||
| 42 | + }, | ||
| 43 | + // 主题色,对底部按钮和选中日期有效 | ||
| 44 | + color: { | ||
| 45 | + type: String, | ||
| 46 | + default: () => defProps.calendar.color | ||
| 47 | + }, | ||
| 48 | + // 最小的可选日期 | ||
| 49 | + minDate: { | ||
| 50 | + type: [String, Number], | ||
| 51 | + default: () => defProps.calendar.minDate | ||
| 52 | + }, | ||
| 53 | + // 最大可选日期 | ||
| 54 | + maxDate: { | ||
| 55 | + type: [String, Number], | ||
| 56 | + default: () => defProps.calendar.maxDate | ||
| 57 | + }, | ||
| 58 | + // 默认选中的日期,mode为multiple或range是必须为数组格式 | ||
| 59 | + defaultDate: { | ||
| 60 | + type: [Array, String, Date, null], | ||
| 61 | + default: () => defProps.calendar.defaultDate | ||
| 62 | + }, | ||
| 63 | + // mode=multiple时,最多可选多少个日期 | ||
| 64 | + maxCount: { | ||
| 65 | + type: [String, Number], | ||
| 66 | + default: () => defProps.calendar.maxCount | ||
| 67 | + }, | ||
| 68 | + // 日期行高 | ||
| 69 | + rowHeight: { | ||
| 70 | + type: [String, Number], | ||
| 71 | + default: () => defProps.calendar.rowHeight | ||
| 72 | + }, | ||
| 73 | + // 日期格式化函数 | ||
| 74 | + formatter: { | ||
| 75 | + type: [Function, null], | ||
| 76 | + default: () => defProps.calendar.formatter | ||
| 77 | + }, | ||
| 78 | + // 是否显示农历 | ||
| 79 | + showLunar: { | ||
| 80 | + type: Boolean, | ||
| 81 | + default: () => defProps.calendar.showLunar | ||
| 82 | + }, | ||
| 83 | + // 是否显示月份背景色 | ||
| 84 | + showMark: { | ||
| 85 | + type: Boolean, | ||
| 86 | + default: () => defProps.calendar.showMark | ||
| 87 | + }, | ||
| 88 | + // 确定按钮的文字 | ||
| 89 | + confirmText: { | ||
| 90 | + type: String, | ||
| 91 | + default: () => defProps.calendar.confirmText | ||
| 92 | + }, | ||
| 93 | + // 确认按钮处于禁用状态时的文字 | ||
| 94 | + confirmDisabledText: { | ||
| 95 | + type: String, | ||
| 96 | + default: () => defProps.calendar.confirmDisabledText | ||
| 97 | + }, | ||
| 98 | + // 是否显示日历弹窗 | ||
| 99 | + show: { | ||
| 100 | + type: Boolean, | ||
| 101 | + default: () => defProps.calendar.show | ||
| 102 | + }, | ||
| 103 | + // 是否显示遮罩 | ||
| 104 | + overlay: { | ||
| 105 | + type: Boolean, | ||
| 106 | + default: () => defProps.calendar.overlay | ||
| 107 | + }, | ||
| 108 | + // 动画时长,单位ms | ||
| 109 | + duration: { | ||
| 110 | + type: [String, Number], | ||
| 111 | + default: () => defProps.calendar.duration | ||
| 112 | + }, | ||
| 113 | + // 自定义遮罩的样式 | ||
| 114 | + overlayStyle: { | ||
| 115 | + type: [Object, String], | ||
| 116 | + default: () => defProps.calendar.overlayStyle | ||
| 117 | + }, | ||
| 118 | + // 遮罩的透明度,0-1之间 | ||
| 119 | + overlayOpacity: { | ||
| 120 | + type: [Number, String], | ||
| 121 | + default: () => defProps.calendar.overlayOpacity | ||
| 122 | + }, | ||
| 123 | + // 层级 | ||
| 124 | + zIndex: { | ||
| 125 | + type: [String, Number], | ||
| 126 | + default: () => defProps.calendar.zIndex | ||
| 127 | + }, | ||
| 128 | + // 是否为iPhoneX留出底部安全距离 | ||
| 129 | + safeAreaInsetBottom: { | ||
| 130 | + type: Boolean, | ||
| 131 | + default: () => defProps.calendar.safeAreaInsetBottom | ||
| 132 | + }, | ||
| 133 | + // 是否留出顶部安全距离(状态栏高度) | ||
| 134 | + safeAreaInsetTop: { | ||
| 135 | + type: Boolean, | ||
| 136 | + default: () => defProps.calendar.safeAreaInsetTop | ||
| 137 | + }, | ||
| 138 | + // 弹窗背景色,设置为transparent可去除白色背景 | ||
| 139 | + bgColor: { | ||
| 140 | + type: String, | ||
| 141 | + default: () => defProps.calendar.bgColor | ||
| 142 | + }, | ||
| 143 | + // 是否允许点击遮罩关闭日历 | ||
| 144 | + closeOnClickOverlay: { | ||
| 145 | + type: Boolean, | ||
| 146 | + default: () => defProps.calendar.closeOnClickOverlay | ||
| 147 | + }, | ||
| 148 | + // 是否为只读状态,只读状态下禁止选择日期 | ||
| 149 | + readonly: { | ||
| 150 | + type: Boolean, | ||
| 151 | + default: () => defProps.calendar.readonly | ||
| 152 | + }, | ||
| 153 | + // 是否展示确认按钮 | ||
| 154 | + showConfirm: { | ||
| 155 | + type: Boolean, | ||
| 156 | + default: () => defProps.calendar.showConfirm | ||
| 157 | + }, | ||
| 158 | + // 日期区间最多可选天数,默认无限制,mode = range时有效 | ||
| 159 | + maxRange: { | ||
| 160 | + type: [Number, String], | ||
| 161 | + default: () => defProps.calendar.maxRange | ||
| 162 | + }, | ||
| 163 | + // 范围选择超过最多可选天数时的提示文案,mode = range时有效 | ||
| 164 | + rangePrompt: { | ||
| 165 | + type: String, | ||
| 166 | + default: () => defProps.calendar.rangePrompt | ||
| 167 | + }, | ||
| 168 | + // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 | ||
| 169 | + showRangePrompt: { | ||
| 170 | + type: Boolean, | ||
| 171 | + default: () => defProps.calendar.showRangePrompt | ||
| 172 | + }, | ||
| 173 | + // 是否允许日期范围的起止时间为同一天,mode = range时有效 | ||
| 174 | + allowSameDay: { | ||
| 175 | + type: Boolean, | ||
| 176 | + default: () => defProps.calendar.allowSameDay | ||
| 177 | + }, | ||
| 178 | + // 区间模式下确认返回值格式,all-返回区间内所有日期,boundary-仅返回起止日期 | ||
| 179 | + rangeResultMode: { | ||
| 180 | + type: String, | ||
| 181 | + default: () => defProps.calendar.rangeResultMode | ||
| 182 | + }, | ||
| 183 | + // 是否开启时分秒选择 | ||
| 184 | + enableTime: { | ||
| 185 | + type: Boolean, | ||
| 186 | + default: () => defProps.calendar.enableTime | ||
| 187 | + }, | ||
| 188 | + // 时间精度:hour-仅时,minute-时分,second-时分秒 | ||
| 189 | + timePrecision: { | ||
| 190 | + type: String, | ||
| 191 | + default: () => defProps.calendar.timePrecision | ||
| 192 | + }, | ||
| 193 | + // 默认时间,格式支持 HH / HH:mm / HH:mm:ss | ||
| 194 | + defaultTime: { | ||
| 195 | + type: String, | ||
| 196 | + default: () => defProps.calendar.defaultTime | ||
| 197 | + }, | ||
| 198 | + // 圆角值 | ||
| 199 | + round: { | ||
| 200 | + type: [Boolean, String, Number], | ||
| 201 | + default: () => defProps.calendar.round | ||
| 202 | + }, | ||
| 203 | + // 最多展示月份数量 | ||
| 204 | + monthNum: { | ||
| 205 | + type: [Number, String], | ||
| 206 | + default: 3 | ||
| 207 | + }, | ||
| 208 | + // 是否启用非滚动的单月切换模式 | ||
| 209 | + monthSwitch: { | ||
| 210 | + type: Boolean, | ||
| 211 | + default: () => defProps.calendar.monthSwitch | ||
| 212 | + }, | ||
| 213 | + // 是否显示今天按钮 | ||
| 214 | + showToday: { | ||
| 215 | + type: Boolean, | ||
| 216 | + default: () => defProps.calendar.showToday | ||
| 217 | + }, | ||
| 218 | + // 今天日期的独立高亮颜色,默认跟随主题色 | ||
| 219 | + todayColor: { | ||
| 220 | + type: String, | ||
| 221 | + default: () => defProps.calendar.todayColor | ||
| 222 | + }, | ||
| 223 | + // 星期文案 | ||
| 224 | + weekText: { | ||
| 225 | + type: Array, | ||
| 226 | + default: defProps.calendar.weekText | ||
| 227 | + }, | ||
| 228 | + forbidDays: { | ||
| 229 | + type: Array, | ||
| 230 | + default: defProps.calendar.forbidDays | ||
| 231 | + }, | ||
| 232 | + forbidDaysToast:{ | ||
| 233 | + type: String, | ||
| 234 | + default: defProps.calendar.forbidDaysToast | ||
| 235 | + }, | ||
| 236 | + monthFormat:{ | ||
| 237 | + type: String, | ||
| 238 | + default: defProps.calendar.monthFormat | ||
| 239 | + }, | ||
| 240 | + // 是否页面内展示 | ||
| 241 | + pageInline:{ | ||
| 242 | + type: Boolean, | ||
| 243 | + default: defProps.calendar.pageInline | ||
| 244 | + } | ||
| 245 | + } | ||
| 246 | +}) |
uni_modules/uview-plus/components/u-cascader/u-cascader.vue
| 1 | <template> | 1 | <template> |
| 2 | - <up-popup | ||
| 3 | - :show="popupShow" | ||
| 4 | - mode="bottom" | ||
| 5 | - :popup="false" | ||
| 6 | - :mask="true" | ||
| 7 | - :closeable="closeable" | ||
| 8 | - :safe-area-inset-bottom="true" | ||
| 9 | - :close-icon-color="closeIconColor" | ||
| 10 | - :z-index="uZIndex" | ||
| 11 | - :maskCloseAble="maskCloseAble" | 2 | + <u-popup |
| 3 | + :show="show" | ||
| 4 | + mode="bottom" | ||
| 5 | + :overlay="overlay" | ||
| 6 | + :duration="duration" | ||
| 7 | + :overlayStyle="overlayStyle" | ||
| 8 | + :overlayOpacity="overlayOpacity" | ||
| 9 | + :zIndex="zIndex" | ||
| 10 | + :safeAreaInsetBottom="safeAreaInsetBottom" | ||
| 11 | + :safeAreaInsetTop="safeAreaInsetTop" | ||
| 12 | + :bgColor="bgColor" | ||
| 13 | + :closeable="!pageInline" | ||
| 12 | @close="close" | 14 | @close="close" |
| 15 | + @closed="$emit('closed')" | ||
| 16 | + :round="round" | ||
| 17 | + :pageInline="pageInline" | ||
| 18 | + :closeOnClickOverlay="closeOnClickOverlay" | ||
| 13 | > | 19 | > |
| 14 | - <view class="up-p-t-30 up-p-l-20 up-m-b-10" v-if="headerDirection ==='column'"> | ||
| 15 | - <up-steps v-if="popupShow" dot direction="column" v-model:current="tabsIndex"> | ||
| 16 | - <view v-for="(item, index) in genTabsList" @click="toFatherIndex(index)"> | ||
| 17 | - <up-steps-item :title="item.name"/> | ||
| 18 | - </view> | ||
| 19 | - </up-steps> | ||
| 20 | - </view> | ||
| 21 | - <view class="up-p-t-20 up-m-b-10" v-else> | ||
| 22 | - <up-tabs | ||
| 23 | - v-if="popupShow" | ||
| 24 | - :list="genTabsList" | ||
| 25 | - :scrollable="true" | ||
| 26 | - v-model:current="tabsIndex" | ||
| 27 | - @change="tabsChange" | ||
| 28 | - ref="tabs" | ||
| 29 | - ></up-tabs> | ||
| 30 | - </view> | ||
| 31 | - <view class="area-box"> | ||
| 32 | - <view class="u-flex" :class="{ 'change':isChange }" | ||
| 33 | - :style="{transform: optionsCols == 2 && isChange ? 'translateX(-33.3333333%)' : ''}"> | ||
| 34 | - <template v-for="(levelData, levelIndex) in levelList" :key="levelIndex"> | ||
| 35 | - <view | ||
| 36 | - v-if="optionsCols == 2 || levelIndex == tabsIndex" | ||
| 37 | - class="area-item" | ||
| 38 | - :style="{ width: optionsCols == 2 ? '33.33333%' : '750rpx'}" | ||
| 39 | - > | ||
| 40 | - <view class="u-padding-10" :style="[levelPaneStyle, { height: '100%' }]"> | ||
| 41 | - <scroll-view :scroll-y="true" style="height: 100%"> | ||
| 42 | - <up-cell-group v-if="levelIndex === 0 || selectedValueIndexs[levelIndex - 1] !== undefined"> | ||
| 43 | - <up-cell | ||
| 44 | - v-for="(item,index) in levelData" | ||
| 45 | - :title="item[labelKey]" | ||
| 46 | - :arrow="false" | ||
| 47 | - :index="index" | ||
| 48 | - :key="index" | ||
| 49 | - @click="levelChange(levelIndex, index)" | ||
| 50 | - > | ||
| 51 | - <template v-slot:right-icon> | ||
| 52 | - <up-icon | ||
| 53 | - v-if="selectedValueIndexs[levelIndex] === index" | ||
| 54 | - size="17" | ||
| 55 | - name="checkbox-mark" | ||
| 56 | - ></up-icon> | ||
| 57 | - </template> | ||
| 58 | - </up-cell> | ||
| 59 | - </up-cell-group> | ||
| 60 | - </scroll-view> | 20 | + <view class="u-calendar"> |
| 21 | + <uHeader | ||
| 22 | + :title="title" | ||
| 23 | + :subtitle="subtitle" | ||
| 24 | + :showSubtitle="showSubtitle" | ||
| 25 | + :showTitle="showTitle" | ||
| 26 | + :weekText="weekText" | ||
| 27 | + :showSwitch="monthSwitch" | ||
| 28 | + :showToday="showToday" | ||
| 29 | + :todayText="todayText" | ||
| 30 | + :todayDisabled="todayDisabled" | ||
| 31 | + :prevDisabled="switchPrevDisabled" | ||
| 32 | + :nextDisabled="switchNextDisabled" | ||
| 33 | + :prevYearDisabled="switchPrevYearDisabled" | ||
| 34 | + :nextYearDisabled="switchNextYearDisabled" | ||
| 35 | + @prev="prevMonth" | ||
| 36 | + @next="nextMonth" | ||
| 37 | + @prevYear="prevYear" | ||
| 38 | + @nextYear="nextYear" | ||
| 39 | + @today="jumpToToday" | ||
| 40 | + ></uHeader> | ||
| 41 | + <view v-if="showTimePanel" class="u-calendar__time-panel"> | ||
| 42 | + <view v-if="mode === 'single'" class="u-calendar__time-row"> | ||
| 43 | + <text class="u-calendar__time-date">{{ singleDateLabel }}</text> | ||
| 44 | + <view class="u-calendar__time-trigger" @click="openTimePicker('single')"> | ||
| 45 | + <text class="u-calendar__time-text">{{ singleTime }}</text> | ||
| 46 | + </view> | ||
| 47 | + </view> | ||
| 48 | + <view v-else-if="mode === 'range' && rangeResultMode === 'boundary'"> | ||
| 49 | + <view class="u-calendar__time-row"> | ||
| 50 | + <text class="u-calendar__time-date">{{ rangeStartDateLabel }}</text> | ||
| 51 | + <view class="u-calendar__time-trigger" @click="openTimePicker('start')"> | ||
| 52 | + <text class="u-calendar__time-text">{{ rangeStartTime }}</text> | ||
| 53 | + </view> | ||
| 54 | + </view> | ||
| 55 | + <view class="u-calendar__time-row"> | ||
| 56 | + <text class="u-calendar__time-date">{{ rangeEndDateLabel }}</text> | ||
| 57 | + <view class="u-calendar__time-trigger" @click="openTimePicker('end')"> | ||
| 58 | + <text class="u-calendar__time-text">{{ rangeEndTime }}</text> | ||
| 61 | </view> | 59 | </view> |
| 62 | </view> | 60 | </view> |
| 63 | - </template> | 61 | + </view> |
| 64 | </view> | 62 | </view> |
| 65 | - </view> | ||
| 66 | - <!-- 按钮区域 --> | ||
| 67 | - <view class="u-cascader-action up-flex up-flex-between"> | ||
| 68 | - <view class="u-padding-20 up-flex-fill"> | ||
| 69 | - <up-button @click="handleCancel" type="default">{{ t("up.common.cancel") }}</up-button> | 63 | + <scroll-view |
| 64 | + v-if="!monthSwitch" | ||
| 65 | + :style="{ | ||
| 66 | + height: addUnit(listHeight, 'px') | ||
| 67 | + }" | ||
| 68 | + scroll-y | ||
| 69 | + @scroll="onScroll" | ||
| 70 | + :scroll-top="scrollTop" | ||
| 71 | + :scrollIntoView="scrollIntoView" | ||
| 72 | + > | ||
| 73 | + <uMonth | ||
| 74 | + :color="color" | ||
| 75 | + :rowHeight="rowHeight" | ||
| 76 | + :showMark="showMark" | ||
| 77 | + :months="months" | ||
| 78 | + :mode="mode" | ||
| 79 | + :maxCount="maxCount" | ||
| 80 | + :startText="startText" | ||
| 81 | + :endText="endText" | ||
| 82 | + :defaultDate="defaultDate" | ||
| 83 | + :minDate="innerMinDate" | ||
| 84 | + :maxDate="innerMaxDate" | ||
| 85 | + :maxMonth="monthNum" | ||
| 86 | + :readonly="readonly" | ||
| 87 | + :maxRange="maxRange" | ||
| 88 | + :rangePrompt="rangePrompt" | ||
| 89 | + :showRangePrompt="showRangePrompt" | ||
| 90 | + :allowSameDay="allowSameDay" | ||
| 91 | + :forbidDays="forbidDays" | ||
| 92 | + :forbidDaysToast="forbidDaysToast" | ||
| 93 | + :monthFormat="monthFormat" | ||
| 94 | + :todayDate="todayDate" | ||
| 95 | + :todayColor="todayColor" | ||
| 96 | + ref="month" | ||
| 97 | + @monthSelected="monthSelected" | ||
| 98 | + @updateMonthTop="onUpdateMonthTop" | ||
| 99 | + ></uMonth> | ||
| 100 | + </scroll-view> | ||
| 101 | + <view | ||
| 102 | + v-else | ||
| 103 | + :style="{ | ||
| 104 | + height: addUnit(listHeight, 'px') | ||
| 105 | + }" | ||
| 106 | + > | ||
| 107 | + <uMonth | ||
| 108 | + :color="color" | ||
| 109 | + :rowHeight="rowHeight" | ||
| 110 | + :showMark="showMark" | ||
| 111 | + :months="currentMonths" | ||
| 112 | + :mode="mode" | ||
| 113 | + :maxCount="maxCount" | ||
| 114 | + :startText="startText" | ||
| 115 | + :endText="endText" | ||
| 116 | + :defaultDate="defaultDate" | ||
| 117 | + :minDate="innerMinDate" | ||
| 118 | + :maxDate="innerMaxDate" | ||
| 119 | + :maxMonth="monthNum" | ||
| 120 | + :readonly="readonly" | ||
| 121 | + :maxRange="maxRange" | ||
| 122 | + :rangePrompt="rangePrompt" | ||
| 123 | + :showRangePrompt="showRangePrompt" | ||
| 124 | + :allowSameDay="allowSameDay" | ||
| 125 | + :forbidDays="forbidDays" | ||
| 126 | + :forbidDaysToast="forbidDaysToast" | ||
| 127 | + :monthFormat="monthFormat" | ||
| 128 | + :todayDate="todayDate" | ||
| 129 | + :todayColor="todayColor" | ||
| 130 | + ref="month" | ||
| 131 | + @monthSelected="monthSelected" | ||
| 132 | + @updateMonthTop="onUpdateMonthTop" | ||
| 133 | + ></uMonth> | ||
| 70 | </view> | 134 | </view> |
| 71 | - <view class="u-padding-20 up-flex-fill"> | ||
| 72 | - <up-button @click="handleConfirm" type="primary">{{ t("up.common.confirm") }}</up-button> | 135 | + <slot name="footer" v-if="showConfirm"> |
| 136 | + <view class="u-calendar__confirm"> | ||
| 137 | + <u-button | ||
| 138 | + shape="circle" | ||
| 139 | + :text=" | ||
| 140 | + buttonDisabled ? confirmDisabledText : confirmText | ||
| 141 | + " | ||
| 142 | + :color="color" | ||
| 143 | + @click="confirm" | ||
| 144 | + :disabled="buttonDisabled" | ||
| 145 | + ></u-button> | ||
| 146 | + </view> | ||
| 147 | + </slot> | ||
| 148 | + </view> | ||
| 149 | + </u-popup> | ||
| 150 | + <u-popup | ||
| 151 | + :show="timePickerShow" | ||
| 152 | + mode="center" | ||
| 153 | + :round="8" | ||
| 154 | + :closeOnClickOverlay="true" | ||
| 155 | + @close="closeTimePicker" | ||
| 156 | + > | ||
| 157 | + <view class="u-calendar__time-picker"> | ||
| 158 | + <view class="u-calendar__time-picker__header"> | ||
| 159 | + <text class="u-calendar__time-picker__cancel" @click="closeTimePicker">取消</text> | ||
| 160 | + <text class="u-calendar__time-picker__title">选择时间</text> | ||
| 161 | + <text class="u-calendar__time-picker__confirm" @click="confirmTimePicker">确定</text> | ||
| 73 | </view> | 162 | </view> |
| 163 | + <picker-view class="u-calendar__time-picker__body" :value="timePickerValue" @change="onTimePickerChange"> | ||
| 164 | + <picker-view-column> | ||
| 165 | + <view v-for="(item, index) in hourOptions" :key="`h-${index}`" class="u-calendar__time-picker__item">{{ item }}</view> | ||
| 166 | + </picker-view-column> | ||
| 167 | + <picker-view-column v-if="timePrecision !== 'hour'"> | ||
| 168 | + <view v-for="(item, index) in minuteOptions" :key="`m-${index}`" class="u-calendar__time-picker__item">{{ item }}</view> | ||
| 169 | + </picker-view-column> | ||
| 170 | + <picker-view-column v-if="timePrecision === 'second'"> | ||
| 171 | + <view v-for="(item, index) in secondOptions" :key="`s-${index}`" class="u-calendar__time-picker__item">{{ item }}</view> | ||
| 172 | + </picker-view-column> | ||
| 173 | + </picker-view> | ||
| 74 | </view> | 174 | </view> |
| 75 | - </up-popup> | 175 | + </u-popup> |
| 76 | </template> | 176 | </template> |
| 77 | 177 | ||
| 78 | <script> | 178 | <script> |
| 179 | +import uHeader from './header.vue' | ||
| 180 | +import uMonth from './month.vue' | ||
| 181 | +import { props } from './props.js' | ||
| 182 | +import util from './util.js' | ||
| 183 | +import dayjs from '../u-datetime-picker/dayjs.esm.min.js'; | ||
| 184 | +import Calendar from '../../libs/util/calendar.js' | ||
| 185 | +import { mpMixin } from '../../libs/mixin/mpMixin.js' | ||
| 186 | +import { mixin } from '../../libs/mixin/mixin.js' | ||
| 187 | +import { addUnit, getPx, range, error, padZero } from '../../libs/function/index'; | ||
| 188 | +import test from '../../libs/function/test'; | ||
| 79 | import { t } from '../../libs/i18n' | 189 | import { t } from '../../libs/i18n' |
| 190 | +/** | ||
| 191 | + * Calendar 日历 | ||
| 192 | + * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中. | ||
| 193 | + * @tutorial https://uview-plus.jiangruyi.com/components/calendar.html | ||
| 194 | + * | ||
| 195 | + * @property {String} title 标题内容 (默认 日期选择 ) | ||
| 196 | + * @property {Boolean} showTitle 是否显示标题 (默认 true ) | ||
| 197 | + * @property {Boolean} showSubtitle 是否显示副标题 (默认 true ) | ||
| 198 | + * @property {String} mode 日期类型选择 single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 ( 默认 'single' ) | ||
| 199 | + * @property {String} startText mode=range时,第一个日期底部的提示文字 (默认 '开始' ) | ||
| 200 | + * @property {String} endText mode=range时,最后一个日期底部的提示文字 (默认 '结束' ) | ||
| 201 | + * @property {Array} customList 自定义列表 | ||
| 202 | + * @property {String} color 主题色,对底部按钮和选中日期有效 (默认 ‘#3c9cff' ) | ||
| 203 | + * @property {String | Number} minDate 最小的可选日期 (默认 0 ) | ||
| 204 | + * @property {String | Number} maxDate 最大可选日期 (默认 0 ) | ||
| 205 | + * @property {Array | String| Date} defaultDate 默认选中的日期,mode为multiple或range是必须为数组格式 | ||
| 206 | + * @property {String | Number} maxCount mode=multiple时,最多可选多少个日期 (默认 Number.MAX_SAFE_INTEGER ) | ||
| 207 | + * @property {String | Number} rowHeight 日期行高 (默认 56 ) | ||
| 208 | + * @property {Function} formatter 日期格式化函数 | ||
| 209 | + * @property {Boolean} showLunar 是否显示农历 (默认 false ) | ||
| 210 | + * @property {Boolean} showMark 是否显示月份背景色 (默认 true ) | ||
| 211 | + * @property {String} confirmText 确定按钮的文字 (默认 '确定' ) | ||
| 212 | + * @property {String} confirmDisabledText 确认按钮处于禁用状态时的文字 (默认 '确定' ) | ||
| 213 | + * @property {Boolean} show 是否显示日历弹窗 (默认 false ) | ||
| 214 | + * @property {Boolean} overlay 是否显示遮罩 (默认 true ) | ||
| 215 | + * @property {String | Number} duration 动画时长,单位ms (默认 300 ) | ||
| 216 | + * @property {Object | String} overlayStyle 自定义遮罩的样式 | ||
| 217 | + * @property {Number | String} overlayOpacity 遮罩透明度,0-1之间 (默认 0.5 ) | ||
| 218 | + * @property {String | Number} zIndex 弹出层的z-index值 (默认 10075 ) | ||
| 219 | + * @property {Boolean} safeAreaInsetBottom 是否留出底部安全距离 (默认 true ) | ||
| 220 | + * @property {Boolean} safeAreaInsetTop 是否留出顶部安全距离(状态栏高度) (默认 false ) | ||
| 221 | + * @property {String} bgColor 弹窗背景色,设置为transparent可去除白色背景 | ||
| 222 | + * @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false ) | ||
| 223 | + * @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false ) | ||
| 224 | + * @property {String | Number} maxRange 日期区间最多可选天数,默认无限制,mode = range时有效 | ||
| 225 | + * @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案,mode = range时有效 | ||
| 226 | + * @property {Boolean} showRangePrompt 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true ) | ||
| 227 | + * @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false ) | ||
| 228 | + * @property {String} rangeResultMode 区间模式下确认返回值格式,all-返回区间内所有日期,boundary-仅返回起止日期 (默认 'all' ) | ||
| 229 | + * @property {Boolean} enableTime 是否开启时分秒选择 (默认 false ) | ||
| 230 | + * @property {String} timePrecision 时间精度,hour-仅时,minute-时分,second-时分秒 (默认 'minute' ) | ||
| 231 | + * @property {String} defaultTime 默认时间,支持HH/HH:mm/HH:mm:ss,不传则按00:00补齐 | ||
| 232 | + * @property {Number|String} round 圆角值,默认无圆角 (默认 0 ) | ||
| 233 | + * @property {Number|String} monthNum 最多展示的月份数量 (默认 3 ) | ||
| 234 | + * @property {Boolean} monthSwitch 是否启用非滚动的单月切换模式 (默认 false ) | ||
| 235 | + * @property {String} todayColor 今天日期的独立高亮颜色,默认跟随主题色 | ||
| 236 | + * @property {Array} weekText 星期文案 (默认 ['一', '二', '三', '四', '五', '六', '日'] ) | ||
| 237 | + * | ||
| 238 | + * @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数 | ||
| 239 | + * @event {Function()} close 日历关闭时触发 可定义页面关闭时的回调事件 | ||
| 240 | + * @event {Function()} closed 日历已关闭(离场动画结束、弹窗真正消失后)触发 | ||
| 241 | + * @example <u-calendar :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm"> | ||
| 242 | + </u-calendar> | ||
| 243 | + * */ | ||
| 80 | export default { | 244 | export default { |
| 81 | - name: 'up-cascader', | ||
| 82 | - props: { | 245 | + name: 'u-calendar', |
| 246 | + mixins: [mpMixin, mixin, props], | ||
| 247 | + components: { | ||
| 248 | + uHeader, | ||
| 249 | + uMonth | ||
| 250 | + }, | ||
| 251 | + data() { | ||
| 252 | + return { | ||
| 253 | + // 需要显示的月份的数组 | ||
| 254 | + months: [], | ||
| 255 | + // 在月份滚动区域中,当前视图中月份的index索引 | ||
| 256 | + monthIndex: 0, | ||
| 257 | + // 月份滚动区域的高度 | ||
| 258 | + listHeight: 0, | ||
| 259 | + // month组件中选择的日期数组 | ||
| 260 | + selected: [], | ||
| 261 | + scrollIntoView: '', | ||
| 262 | + scrollIntoViewScroll: '', | ||
| 263 | + scrollTop:0, | ||
| 264 | + timePickerShow: false, | ||
| 265 | + timePickerTarget: 'single', | ||
| 266 | + timePickerValue: [0, 0, 0], | ||
| 267 | + hourOptions: [], | ||
| 268 | + minuteOptions: [], | ||
| 269 | + secondOptions: [], | ||
| 270 | + singleTime: '00:00', | ||
| 271 | + rangeStartTime: '00:00', | ||
| 272 | + rangeEndTime: '00:00', | ||
| 273 | + // 过滤处理方法 | ||
| 274 | + innerFormatter: (value) => value | ||
| 275 | + } | ||
| 276 | + }, | ||
| 277 | + watch: { | ||
| 278 | + scrollIntoView: { | ||
| 279 | + immediate: true, | ||
| 280 | + handler(n) { | ||
| 281 | + // console.log('scrollIntoView', n) | ||
| 282 | + } | ||
| 283 | + }, | ||
| 284 | + selectedChange: { | ||
| 285 | + immediate: true, | ||
| 286 | + handler(n) { | ||
| 287 | + this.setMonth() | ||
| 288 | + } | ||
| 289 | + }, | ||
| 290 | + timePrecision() { | ||
| 291 | + this.initTimeOptions() | ||
| 292 | + this.initTimeValues() | ||
| 293 | + }, | ||
| 294 | + defaultTime() { | ||
| 295 | + this.initTimeValues() | ||
| 296 | + }, | ||
| 297 | + // 打开弹窗时,设置月份数据 | ||
| 83 | show: { | 298 | show: { |
| 84 | - type: Boolean, | ||
| 85 | - default: false | 299 | + immediate: true, |
| 300 | + handler(n) { | ||
| 301 | + if (n) { | ||
| 302 | + this.setMonth() | ||
| 303 | + // 弹层每次打开都会重新挂载并滚动回默认月份,副标题指向的月份索引要一起复位, | ||
| 304 | + // 否则会沿用上一次关闭前滚动到的月份,导致副标题与日历内容对应不上。 | ||
| 305 | + // 页面内模式的内容一直挂载着,滚动位置不会重置,所以不做处理 | ||
| 306 | + if (!this.pageInline) { | ||
| 307 | + this.monthIndex = this.getDefaultMonthIndex() | ||
| 308 | + } | ||
| 309 | + } else { | ||
| 310 | + // 关闭时重置scrollIntoView,否则会出现二次打开日历,当前月份数据显示不正确。 | ||
| 311 | + // scrollIntoView需要有一个值变动过程,才会产生作用。 | ||
| 312 | + this.scrollIntoView = '' | ||
| 313 | + } | ||
| 314 | + } | ||
| 315 | + } | ||
| 316 | + }, | ||
| 317 | + computed: { | ||
| 318 | + // 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳),但是dayjs如果接受字符串形式的时间戳会有问题,这里进行处理 | ||
| 319 | + innerMaxDate() { | ||
| 320 | + return test.number(this.maxDate) | ||
| 321 | + ? Number(this.maxDate) | ||
| 322 | + : this.maxDate | ||
| 86 | }, | 323 | }, |
| 87 | - data: { | ||
| 88 | - type: Array, | ||
| 89 | - default() { | ||
| 90 | - return []; | 324 | + innerMinDate() { |
| 325 | + return test.number(this.minDate) | ||
| 326 | + ? Number(this.minDate) | ||
| 327 | + : this.minDate | ||
| 328 | + }, | ||
| 329 | + todayDate() { | ||
| 330 | + return dayjs().format('YYYY-MM-DD') | ||
| 331 | + }, | ||
| 332 | + todayText() { | ||
| 333 | + return t('up.calendar.today') | ||
| 334 | + }, | ||
| 335 | + todayDisabled() { | ||
| 336 | + const today = dayjs(this.todayDate) | ||
| 337 | + const minDate = this.innerMinDate | ||
| 338 | + ? dayjs(this.innerMinDate) | ||
| 339 | + : null | ||
| 340 | + const maxDate = this.innerMaxDate | ||
| 341 | + ? dayjs(this.innerMaxDate) | ||
| 342 | + : null | ||
| 343 | + if (minDate && today.isBefore(minDate, 'day')) { | ||
| 344 | + return true | ||
| 345 | + } | ||
| 346 | + if (maxDate && today.isAfter(maxDate, 'day')) { | ||
| 347 | + return true | ||
| 348 | + } | ||
| 349 | + return false | ||
| 350 | + }, | ||
| 351 | + // 多个条件的变化,会引起选中日期的变化,这里统一管理监听 | ||
| 352 | + selectedChange() { | ||
| 353 | + return [this.innerMinDate, this.innerMaxDate, this.defaultDate] | ||
| 354 | + }, | ||
| 355 | + subtitle() { | ||
| 356 | + // 初始化时,this.months为空数组,所以需要特别判断处理 | ||
| 357 | + if (this.months.length) { | ||
| 358 | + if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') { | ||
| 359 | + return this.months[this.monthIndex].year + '年' + (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '月' | ||
| 360 | + } else { | ||
| 361 | + return (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '/' + this.months[this.monthIndex].year | ||
| 362 | + } | ||
| 363 | + } else { | ||
| 364 | + return '' | ||
| 91 | } | 365 | } |
| 92 | }, | 366 | }, |
| 93 | - modelValue: { | ||
| 94 | - type: Array, | ||
| 95 | - default() { | ||
| 96 | - return []; | 367 | + currentMonths() { |
| 368 | + if (this.monthSwitch && this.months.length) { | ||
| 369 | + return [this.months[this.monthIndex]] | ||
| 97 | } | 370 | } |
| 371 | + return this.months | ||
| 98 | }, | 372 | }, |
| 99 | - valueKey: { | ||
| 100 | - type: String, | ||
| 101 | - default: 'value' | 373 | + switchPrevDisabled() { |
| 374 | + return this.monthIndex <= 0 | ||
| 102 | }, | 375 | }, |
| 103 | - labelKey: { | ||
| 104 | - type: String, | ||
| 105 | - default: 'label' | 376 | + switchNextDisabled() { |
| 377 | + return this.monthIndex >= this.months.length - 1 | ||
| 106 | }, | 378 | }, |
| 107 | - childrenKey: { | ||
| 108 | - type: String, | ||
| 109 | - default: 'children' | 379 | + switchPrevYearDisabled() { |
| 380 | + return this.monthIndex - 12 < 0 | ||
| 110 | }, | 381 | }, |
| 111 | - maskCloseAble: { | ||
| 112 | - type: Boolean, | ||
| 113 | - default: true | 382 | + switchNextYearDisabled() { |
| 383 | + return this.monthIndex + 12 > this.months.length - 1 | ||
| 114 | }, | 384 | }, |
| 115 | - zIndex: { | ||
| 116 | - type: [String, Number], | ||
| 117 | - default: 0 | 385 | + showTimePanel() { |
| 386 | + if (!this.enableTime) return false | ||
| 387 | + if (this.mode === 'single') return true | ||
| 388 | + if (this.mode === 'range' && this.rangeResultMode === 'boundary') { | ||
| 389 | + return true | ||
| 390 | + } | ||
| 391 | + return false | ||
| 118 | }, | 392 | }, |
| 119 | - autoClose: { | ||
| 120 | - type: Boolean, | ||
| 121 | - default: false | 393 | + singleDateLabel() { |
| 394 | + return this.selected[0] || '--' | ||
| 122 | }, | 395 | }, |
| 123 | - headerDirection: { | ||
| 124 | - type: String, | ||
| 125 | - default: 'row' | 396 | + rangeStartDateLabel() { |
| 397 | + return this.selected[0] || '--' | ||
| 126 | }, | 398 | }, |
| 127 | - optionsCols: { | ||
| 128 | - type: [Number], | ||
| 129 | - default: 2 | 399 | + rangeEndDateLabel() { |
| 400 | + if (this.selected.length >= 2) { | ||
| 401 | + return this.selected[this.selected.length - 1] | ||
| 402 | + } | ||
| 403 | + return '--' | ||
| 130 | }, | 404 | }, |
| 131 | - closeable: { | ||
| 132 | - type: Boolean, | ||
| 133 | - default: true | 405 | + buttonDisabled() { |
| 406 | + // 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态 | ||
| 407 | + if (this.mode === 'range') { | ||
| 408 | + if (this.selected.length <= 1) { | ||
| 409 | + return true | ||
| 410 | + } else { | ||
| 411 | + return false | ||
| 412 | + } | ||
| 413 | + } else { | ||
| 414 | + return false | ||
| 415 | + } | ||
| 134 | } | 416 | } |
| 135 | }, | 417 | }, |
| 136 | - data() { | ||
| 137 | - return { | ||
| 138 | - levelList: [], | ||
| 139 | - selectedValueIndexs: [], | ||
| 140 | - tabsIndex: 0, | ||
| 141 | - popupShow: false, | ||
| 142 | - confirmValues: [] | ||
| 143 | - } | 418 | + mounted() { |
| 419 | + this.start = Date.now() | ||
| 420 | + this.init() | ||
| 144 | }, | 421 | }, |
| 145 | - watch: { | ||
| 146 | - data: { | ||
| 147 | - handler() { | ||
| 148 | - this.initLevelList(); | ||
| 149 | - this.setDefaultValue(); | ||
| 150 | - }, | ||
| 151 | - immediate: true | 422 | + emits: ["confirm", "close", "closed"], |
| 423 | + methods: { | ||
| 424 | + addUnit, | ||
| 425 | + padTime(num) { | ||
| 426 | + return String(num).padStart(2, '0') | ||
| 152 | }, | 427 | }, |
| 153 | - show: { | ||
| 154 | - handler(val) { | ||
| 155 | - this.popupShow = val; | ||
| 156 | - if (val) { | ||
| 157 | - this.tabsIndex = 0; | ||
| 158 | - this.initLevelList(); | ||
| 159 | - this.setDefaultValue(); | ||
| 160 | - } | ||
| 161 | - }, | ||
| 162 | - immediate: true | ||
| 163 | - }, | ||
| 164 | - modelValue: { | ||
| 165 | - handler() { | ||
| 166 | - this.setDefaultValue(); | ||
| 167 | - }, | ||
| 168 | - immediate: true | ||
| 169 | - } | ||
| 170 | - }, | ||
| 171 | - computed: { | ||
| 172 | - isChange() { | ||
| 173 | - return this.tabsIndex > 1; | ||
| 174 | - }, | ||
| 175 | - genTabsList() { | ||
| 176 | - let tabsList = [{ name: "请选择" }]; | ||
| 177 | - for (let i = 0; i < this.selectedValueIndexs.length; i++) { | ||
| 178 | - if (this.selectedValueIndexs[i] !== undefined && this.levelList[i]) { | ||
| 179 | - const selectedItem = this.levelList[i][this.selectedValueIndexs[i]]; | ||
| 180 | - if (selectedItem) { | ||
| 181 | - tabsList[i] = { name: selectedItem[this.labelKey] }; | ||
| 182 | - if (i === this.selectedValueIndexs.length - 1 && | ||
| 183 | - selectedItem[this.childrenKey] && | ||
| 184 | - selectedItem[this.childrenKey].length > 0) { | ||
| 185 | - tabsList.push({ name: "请选择" }); | ||
| 186 | - } | ||
| 187 | - } | ||
| 188 | - } | 428 | + initTimeOptions() { |
| 429 | + this.hourOptions = Array.from({ length: 24 }, (_, i) => this.padTime(i)) | ||
| 430 | + this.minuteOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i)) | ||
| 431 | + this.secondOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i)) | ||
| 432 | + }, | ||
| 433 | + parseTimeValue(value = '') { | ||
| 434 | + const raw = String(value || '').trim() | ||
| 435 | + const parts = raw.length ? raw.split(':') : [] | ||
| 436 | + const getNumber = (index, max) => { | ||
| 437 | + const current = Number(parts[index] || 0) | ||
| 438 | + if (Number.isNaN(current)) return 0 | ||
| 439 | + return Math.max(0, Math.min(max, current)) | ||
| 189 | } | 440 | } |
| 190 | - return tabsList; | 441 | + const hour = getNumber(0, 23) |
| 442 | + const minute = getNumber(1, 59) | ||
| 443 | + const second = getNumber(2, 59) | ||
| 444 | + return [hour, minute, second] | ||
| 191 | }, | 445 | }, |
| 192 | - uZIndex() { | ||
| 193 | - return this.zIndex ? this.zIndex : this.$u.zIndex.popup; | 446 | + getDefaultTimeValue() { |
| 447 | + const [hour, minute, second] = this.parseTimeValue(this.defaultTime) | ||
| 448 | + if (this.timePrecision === 'hour') { | ||
| 449 | + return this.padTime(hour) | ||
| 450 | + } | ||
| 451 | + if (this.timePrecision === 'second') { | ||
| 452 | + return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}` | ||
| 453 | + } | ||
| 454 | + return `${this.padTime(hour)}:${this.padTime(minute)}` | ||
| 194 | }, | 455 | }, |
| 195 | - closeIconColor() { | ||
| 196 | - return 'var(--up-main-color, #303133)'; | 456 | + initTimeValues() { |
| 457 | + const value = this.getDefaultTimeValue() | ||
| 458 | + this.singleTime = value | ||
| 459 | + this.rangeStartTime = value | ||
| 460 | + this.rangeEndTime = value | ||
| 197 | }, | 461 | }, |
| 198 | - levelPaneStyle() { | ||
| 199 | - return { | ||
| 200 | - backgroundColor: 'var(--up-bg-color, #f7f7f7)' | ||
| 201 | - }; | ||
| 202 | - } | ||
| 203 | - }, | ||
| 204 | - emits: ['update:modelValue', 'update:show', 'change', 'confirm', 'cancel'], | ||
| 205 | - methods: { | ||
| 206 | - t, | ||
| 207 | - // 获取选中值数组 | ||
| 208 | - getSelectedValues() { | ||
| 209 | - const result = []; | ||
| 210 | - for (let i = 0; i < this.selectedValueIndexs.length; i++) { | ||
| 211 | - const selectedIndex = this.selectedValueIndexs[i]; | ||
| 212 | - if (selectedIndex === undefined) continue; | ||
| 213 | - const list = this.levelList[i]; | ||
| 214 | - if (!list || !list[selectedIndex]) continue; | ||
| 215 | - result.push(list[selectedIndex][this.valueKey]); | ||
| 216 | - } | ||
| 217 | - return result; | ||
| 218 | - }, | ||
| 219 | - // 判断当前选中项是否为最后一级(叶子节点) | ||
| 220 | - isCurrentLeafNode() { | ||
| 221 | - const len = this.selectedValueIndexs.length; | ||
| 222 | - if (len === 0) return false; | ||
| 223 | - const lastLevel = len - 1; | ||
| 224 | - const lastIndex = this.selectedValueIndexs[lastLevel]; | ||
| 225 | - const lastItem = this.levelList[lastLevel]?.[lastIndex]; | ||
| 226 | - if (!lastItem) return false; | ||
| 227 | - // 无子级 = 最后一级 | ||
| 228 | - return !lastItem[this.childrenKey] || lastItem[this.childrenKey].length === 0; | ||
| 229 | - }, | ||
| 230 | - initLevelList() { | ||
| 231 | - if (this.data && this.data.length > 0) { | ||
| 232 | - this.levelList = [this.data]; | ||
| 233 | - this.selectedValueIndexs = []; | ||
| 234 | - } else { | ||
| 235 | - this.levelList = []; | ||
| 236 | - this.selectedValueIndexs = []; | 462 | + timeToPickerValue(value = '') { |
| 463 | + const [hour, minute, second] = this.parseTimeValue(value) | ||
| 464 | + return [hour, minute, second] | ||
| 465 | + }, | ||
| 466 | + pickerValueToTime(value = []) { | ||
| 467 | + const hour = Number(value[0] || 0) | ||
| 468 | + const minute = Number(value[1] || 0) | ||
| 469 | + const second = Number(value[2] || 0) | ||
| 470 | + if (this.timePrecision === 'hour') { | ||
| 471 | + return this.padTime(hour) | ||
| 237 | } | 472 | } |
| 473 | + if (this.timePrecision === 'second') { | ||
| 474 | + return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}` | ||
| 475 | + } | ||
| 476 | + return `${this.padTime(hour)}:${this.padTime(minute)}` | ||
| 477 | + }, | ||
| 478 | + openTimePicker(target) { | ||
| 479 | + this.timePickerTarget = target | ||
| 480 | + let currentValue = this.singleTime | ||
| 481 | + if (target === 'start') currentValue = this.rangeStartTime | ||
| 482 | + if (target === 'end') currentValue = this.rangeEndTime | ||
| 483 | + this.timePickerValue = this.timeToPickerValue(currentValue) | ||
| 484 | + this.timePickerShow = true | ||
| 485 | + }, | ||
| 486 | + onTimePickerChange(e) { | ||
| 487 | + this.timePickerValue = e.detail.value | ||
| 488 | + }, | ||
| 489 | + closeTimePicker() { | ||
| 490 | + this.timePickerShow = false | ||
| 491 | + }, | ||
| 492 | + confirmTimePicker() { | ||
| 493 | + const value = this.pickerValueToTime(this.timePickerValue) | ||
| 494 | + if (this.timePickerTarget === 'single') this.singleTime = value | ||
| 495 | + if (this.timePickerTarget === 'start') this.rangeStartTime = value | ||
| 496 | + if (this.timePickerTarget === 'end') this.rangeEndTime = value | ||
| 497 | + this.timePickerShow = false | ||
| 238 | }, | 498 | }, |
| 239 | - setDefaultValue() { | ||
| 240 | - if (!this.data || this.data.length === 0) { | ||
| 241 | - this.confirmValues = []; | ||
| 242 | - return; | 499 | + timeToSecond(timeText = '') { |
| 500 | + const [hour, minute, second] = this.parseTimeValue(timeText) | ||
| 501 | + return hour * 3600 + minute * 60 + second | ||
| 502 | + }, | ||
| 503 | + validateSameDayRangeTime() { | ||
| 504 | + if (!this.enableTime || this.mode !== 'range' || this.rangeResultMode !== 'boundary') { | ||
| 505 | + return true | ||
| 243 | } | 506 | } |
| 244 | - if (!this.modelValue || this.modelValue.length === 0) { | ||
| 245 | - this.confirmValues = []; | ||
| 246 | - return; | 507 | + if (this.selected.length < 2) return true |
| 508 | + const startDate = this.selected[0] | ||
| 509 | + const endDate = this.selected[this.selected.length - 1] | ||
| 510 | + if (startDate !== endDate) return true | ||
| 511 | + const startSeconds = this.timeToSecond(this.rangeStartTime) | ||
| 512 | + const endSeconds = this.timeToSecond(this.rangeEndTime) | ||
| 513 | + if (endSeconds < startSeconds) { | ||
| 514 | + uni.showToast({ | ||
| 515 | + title: '结束时间不能早于开始时间', | ||
| 516 | + icon: 'none' | ||
| 517 | + }) | ||
| 518 | + return false | ||
| 247 | } | 519 | } |
| 248 | - this.selectedValueIndexs = []; | ||
| 249 | - this.levelList = []; | ||
| 250 | - let currentLevelData = this.data; | ||
| 251 | - | ||
| 252 | - for (let i = 0; i < this.modelValue.length; i++) { | ||
| 253 | - const value = this.modelValue[i]; | ||
| 254 | - const index = currentLevelData.findIndex(item => item[this.valueKey] === value); | ||
| 255 | - this.levelList[i] = currentLevelData; | ||
| 256 | - | ||
| 257 | - if (index !== -1) { | ||
| 258 | - this.selectedValueIndexs.push(index); | ||
| 259 | - if (currentLevelData[index][this.childrenKey]) { | ||
| 260 | - currentLevelData = currentLevelData[index][this.childrenKey]; | ||
| 261 | - } else { | ||
| 262 | - break; | ||
| 263 | - } | ||
| 264 | - } else { | ||
| 265 | - break; | 520 | + return true |
| 521 | + }, | ||
| 522 | + appendTime(dateText, timeText) { | ||
| 523 | + return `${dateText} ${timeText}` | ||
| 524 | + }, | ||
| 525 | + getConfirmValue(selected = this.selected) { | ||
| 526 | + let result = selected | ||
| 527 | + if ( | ||
| 528 | + this.mode === 'range' && | ||
| 529 | + this.rangeResultMode === 'boundary' && | ||
| 530 | + selected.length >= 2 | ||
| 531 | + ) { | ||
| 532 | + const len = selected.length - 1 | ||
| 533 | + result = [selected[0], selected[len]] | ||
| 534 | + } | ||
| 535 | + if (!this.showTimePanel || !this.enableTime) { | ||
| 536 | + return result | ||
| 537 | + } | ||
| 538 | + if (this.mode === 'single' && result.length >= 1) { | ||
| 539 | + return [this.appendTime(result[0], this.singleTime)] | ||
| 540 | + } | ||
| 541 | + if (this.mode === 'range' && this.rangeResultMode === 'boundary' && result.length >= 2) { | ||
| 542 | + return [ | ||
| 543 | + this.appendTime(result[0], this.rangeStartTime), | ||
| 544 | + this.appendTime(result[1], this.rangeEndTime) | ||
| 545 | + ] | ||
| 546 | + } | ||
| 547 | + return result | ||
| 548 | + }, | ||
| 549 | + // 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用 | ||
| 550 | + setFormatter(e) { | ||
| 551 | + this.innerFormatter = e | ||
| 552 | + }, | ||
| 553 | + // month组件内部选择日期后,通过事件通知给父组件 | ||
| 554 | + monthSelected(e,scene ='init') { | ||
| 555 | + this.selected = e | ||
| 556 | + if (!this.showConfirm) { | ||
| 557 | + // 在不需要确认按钮的情况下,如果为单选,或者范围多选且已选长度大于2,则直接进行返还 | ||
| 558 | + if ( | ||
| 559 | + this.mode === 'multiple' || | ||
| 560 | + this.mode === 'single' || | ||
| 561 | + (this.mode === 'range' && this.selected.length >= 2) | ||
| 562 | + ) { | ||
| 563 | + if( scene === 'init'){ | ||
| 564 | + return | ||
| 565 | + } | ||
| 566 | + if( scene === 'tap') { | ||
| 567 | + if (!this.validateSameDayRangeTime()) return | ||
| 568 | + this.$emit('confirm', this.getConfirmValue()) | ||
| 569 | + } | ||
| 266 | } | 570 | } |
| 267 | } | 571 | } |
| 268 | - this.confirmValues = this.getSelectedValues(); | ||
| 269 | }, | 572 | }, |
| 270 | - | 573 | + init() { |
| 574 | + // 校验maxDate,不能小于minDate。 | ||
| 575 | + if ( | ||
| 576 | + this.innerMaxDate && | ||
| 577 | + this.innerMinDate && | ||
| 578 | + new Date(this.innerMaxDate).getTime() < new Date(this.innerMinDate).getTime() | ||
| 579 | + ) { | ||
| 580 | + return error('maxDate不能小于minDate时间') | ||
| 581 | + } | ||
| 582 | + // 滚动区域的高度 | ||
| 583 | + let bottomPadding = 0; | ||
| 584 | + if (this.pageInline) { | ||
| 585 | + bottomPadding = 0 | ||
| 586 | + } else { | ||
| 587 | + bottomPadding = 30 | ||
| 588 | + } | ||
| 589 | + this.listHeight = this.rowHeight * (this.monthSwitch ? 6 : 5) + bottomPadding | ||
| 590 | + this.initTimeOptions() | ||
| 591 | + this.initTimeValues() | ||
| 592 | + this.setMonth() | ||
| 593 | + }, | ||
| 271 | close() { | 594 | close() { |
| 272 | - this.popupShow = false; | ||
| 273 | - this.levelList = []; | ||
| 274 | - this.selectedValueIndexs = []; | ||
| 275 | - this.tabsIndex = 0; | ||
| 276 | - this.confirmValues = []; | ||
| 277 | - this.$emit('update:show', false); | ||
| 278 | - this.$emit('cancel'); | 595 | + this.$emit('close') |
| 279 | }, | 596 | }, |
| 280 | - | ||
| 281 | - tabsChange(item) {}, | ||
| 282 | - toFatherIndex(index) { | ||
| 283 | - this.tabsIndex = index; | ||
| 284 | - }, | ||
| 285 | - levelChange(levelIndex, index) { | ||
| 286 | - this.$set(this.selectedValueIndexs, levelIndex, index); | ||
| 287 | - this.selectedValueIndexs.splice(levelIndex + 1); | ||
| 288 | - this.tabsIndex = Math.min(this.tabsIndex, levelIndex); | ||
| 289 | - this.levelList.splice(levelIndex + 1); | ||
| 290 | - | ||
| 291 | - const currentItem = this.levelList[levelIndex][index]; | ||
| 292 | - if (currentItem && currentItem[this.childrenKey] && currentItem[this.childrenKey].length > 0) { | ||
| 293 | - if (this.levelList.length <= levelIndex + 1) { | ||
| 294 | - this.levelList.push(currentItem[this.childrenKey]); | ||
| 295 | - } else { | ||
| 296 | - this.$set(this.levelList, levelIndex + 1, currentItem[this.childrenKey]); | 597 | + // 点击确定按钮 |
| 598 | + confirm() { | ||
| 599 | + if (!this.buttonDisabled) { | ||
| 600 | + if (!this.validateSameDayRangeTime()) return | ||
| 601 | + this.$emit('confirm', this.getConfirmValue()) | ||
| 602 | + } | ||
| 603 | + }, | ||
| 604 | + // 获得两个日期之间的月份数 | ||
| 605 | + getMonths(minDate, maxDate) { | ||
| 606 | + const minYear = dayjs(minDate).year() | ||
| 607 | + const minMonth = dayjs(minDate).month() + 1 | ||
| 608 | + const maxYear = dayjs(maxDate).year() | ||
| 609 | + const maxMonth = dayjs(maxDate).month() + 1 | ||
| 610 | + return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1 | ||
| 611 | + }, | ||
| 612 | + // 设置月份数据 | ||
| 613 | + setMonth() { | ||
| 614 | + // 最小日期的毫秒数 | ||
| 615 | + const minDate = this.innerMinDate || dayjs().valueOf() | ||
| 616 | + // 如果没有指定最大日期,则往后推3个月 | ||
| 617 | + const maxDate = | ||
| 618 | + this.innerMaxDate || | ||
| 619 | + dayjs(minDate) | ||
| 620 | + .add(this.monthNum - 1, 'month') | ||
| 621 | + .valueOf() | ||
| 622 | + // 最大最小月份之间的共有多少个月份, | ||
| 623 | + const months = range( | ||
| 624 | + 1, | ||
| 625 | + this.monthNum, | ||
| 626 | + this.getMonths(minDate, maxDate) | ||
| 627 | + ) | ||
| 628 | + // 记录上一次测量到的各月份top值,月份未发生变化时高度也不会变, | ||
| 629 | + // 复用后可避免重新生成数据后onScroll没有判断依据 | ||
| 630 | + const topCache = {} | ||
| 631 | + this.months.forEach(item => { | ||
| 632 | + if (typeof item.top === 'number') { | ||
| 633 | + topCache[`${item.year}-${item.month}`] = item.top | ||
| 297 | } | 634 | } |
| 298 | - this.tabsIndex = levelIndex + 1; | ||
| 299 | - // 有子级,不自动确认 | ||
| 300 | - if (this.autoClose) return; | ||
| 301 | - } else { | ||
| 302 | - // 已选到最后一级,自动确认 | ||
| 303 | - if (this.autoClose) { | ||
| 304 | - this.handleConfirm(); | 635 | + }) |
| 636 | + // 先清空数组 | ||
| 637 | + this.months = [] | ||
| 638 | + // 最大最小日期只需格式化一次,YYYY-MM-DD的字典序即时间序,可直接比较字符串 | ||
| 639 | + const minDateStr = dayjs(minDate).format('YYYY-MM-DD') | ||
| 640 | + const maxDateStr = dayjs(maxDate).format('YYYY-MM-DD') | ||
| 641 | + const formatter = this.formatter || this.innerFormatter | ||
| 642 | + for (let i = 0; i < months; i++) { | ||
| 643 | + // 每个月只构造一个dayjs对象,当月的日期数据都由它派生, | ||
| 644 | + // 避免按天重复解析(monthNum较大时这里是主要开销) | ||
| 645 | + const monthStart = dayjs(minDate).add(i, 'month') | ||
| 646 | + const monthValue = monthStart.month() + 1 | ||
| 647 | + this.months.push({ | ||
| 648 | + date: new Array(monthStart.daysInMonth()) | ||
| 649 | + .fill(1) | ||
| 650 | + .map((item, index) => { | ||
| 651 | + // 日期,取值1-31 | ||
| 652 | + let day = index + 1 | ||
| 653 | + const current = monthStart.date(day) | ||
| 654 | + // 星期,0-6,0为周日 | ||
| 655 | + const week = current.day() | ||
| 656 | + const date = current.format('YYYY-MM-DD') | ||
| 657 | + let bottomInfo = '' | ||
| 658 | + if (this.showLunar) { | ||
| 659 | + // 将日期转为农历格式 | ||
| 660 | + const lunar = Calendar.solar2lunar( | ||
| 661 | + current.year(), | ||
| 662 | + monthValue, | ||
| 663 | + day | ||
| 664 | + ) | ||
| 665 | + bottomInfo = lunar.IDayCn | ||
| 666 | + } | ||
| 667 | + let config = { | ||
| 668 | + day, | ||
| 669 | + week, | ||
| 670 | + // 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态 | ||
| 671 | + disabled: | ||
| 672 | + date < minDateStr || date > maxDateStr, | ||
| 673 | + // 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理 | ||
| 674 | + date: new Date(date), | ||
| 675 | + // 已格式化好的日期字符串,供month组件比较使用, | ||
| 676 | + // 省掉渲染期对每个日期格子的重复解析 | ||
| 677 | + dateStr: date, | ||
| 678 | + bottomInfo, | ||
| 679 | + dot: false, | ||
| 680 | + month: monthValue | ||
| 681 | + } | ||
| 682 | + return formatter(config) | ||
| 683 | + }), | ||
| 684 | + // 当前所属的月份 | ||
| 685 | + month: monthValue, | ||
| 686 | + // 当前年份 | ||
| 687 | + year: monthStart.year() | ||
| 688 | + }) | ||
| 689 | + } | ||
| 690 | + this.months.forEach(item => { | ||
| 691 | + const cachedTop = topCache[`${item.year}-${item.month}`] | ||
| 692 | + if (typeof cachedTop === 'number') { | ||
| 693 | + item.top = cachedTop | ||
| 694 | + } | ||
| 695 | + }) | ||
| 696 | + if (this.monthSwitch) { | ||
| 697 | + this.monthIndex = this.getDefaultMonthIndex() | ||
| 698 | + } | ||
| 699 | + }, | ||
| 700 | + getDefaultMonthIndex() { | ||
| 701 | + let selected = dayjs().format('YYYY-MM') | ||
| 702 | + if (this.defaultDate) { | ||
| 703 | + if (!test.array(this.defaultDate)) { | ||
| 704 | + selected = dayjs(this.defaultDate).format('YYYY-MM') | ||
| 705 | + } else if (this.defaultDate.length) { | ||
| 706 | + selected = dayjs(this.defaultDate[0]).format('YYYY-MM') | ||
| 707 | + } | ||
| 708 | + } | ||
| 709 | + const index = this.months.findIndex(({ year, month }) => { | ||
| 710 | + return `${year}-${padZero(month)}` === selected | ||
| 711 | + }) | ||
| 712 | + return index === -1 ? 0 : index | ||
| 713 | + }, | ||
| 714 | + prevMonth() { | ||
| 715 | + if (!this.switchPrevDisabled) { | ||
| 716 | + this.monthIndex -= 1 | ||
| 717 | + } | ||
| 718 | + }, | ||
| 719 | + nextMonth() { | ||
| 720 | + if (!this.switchNextDisabled) { | ||
| 721 | + this.monthIndex += 1 | ||
| 722 | + } | ||
| 723 | + }, | ||
| 724 | + prevYear() { | ||
| 725 | + if (!this.switchPrevYearDisabled) { | ||
| 726 | + this.monthIndex -= 12 | ||
| 727 | + } | ||
| 728 | + }, | ||
| 729 | + nextYear() { | ||
| 730 | + if (!this.switchNextYearDisabled) { | ||
| 731 | + this.monthIndex += 12 | ||
| 732 | + } | ||
| 733 | + }, | ||
| 734 | + jumpToToday() { | ||
| 735 | + if (this.todayDisabled) { | ||
| 736 | + return | ||
| 737 | + } | ||
| 738 | + const targetMonth = dayjs(this.todayDate).format('YYYY-MM') | ||
| 739 | + const selectToday = () => { | ||
| 740 | + if (this.mode === 'range') { | ||
| 741 | + return | ||
| 742 | + } | ||
| 743 | + this.$refs.month && this.$refs.month.selectDate(this.todayDate) | ||
| 744 | + } | ||
| 745 | + if (this.monthSwitch) { | ||
| 746 | + const todayMonthIndex = this.months.findIndex(({ year, month }) => { | ||
| 747 | + return `${year}-${padZero(month)}` === targetMonth | ||
| 748 | + }) | ||
| 749 | + if (todayMonthIndex !== -1) { | ||
| 750 | + this.monthIndex = todayMonthIndex | ||
| 751 | + this.$nextTick(selectToday) | ||
| 305 | } | 752 | } |
| 753 | + return | ||
| 306 | } | 754 | } |
| 755 | + this.scrollIntoDefaultMonth(targetMonth) | ||
| 756 | + this.$nextTick(selectToday) | ||
| 307 | }, | 757 | }, |
| 308 | - emitChange(closePopup = true) { | ||
| 309 | - const result = this.getSelectedValues(); | ||
| 310 | - this.confirmValues = [...result]; | ||
| 311 | - this.$emit('change', this.confirmValues); | ||
| 312 | - if (closePopup) { | ||
| 313 | - this.close(); | 758 | + // 滚动到默认设置的月份 |
| 759 | + scrollIntoDefaultMonth(selected) { | ||
| 760 | + // 查询默认日期在可选列表的下标 | ||
| 761 | + const _index = this.months.findIndex(({ | ||
| 762 | + year, | ||
| 763 | + month | ||
| 764 | + }) => { | ||
| 765 | + month = padZero(month) | ||
| 766 | + return `${year}-${month}` === selected | ||
| 767 | + }) | ||
| 768 | + if (_index !== -1) { | ||
| 769 | + // 滚动位置是程序主动设置的,同步更新副标题指向的月份索引。 | ||
| 770 | + // 只依赖scroll事件的话,滚动位置没有真正变化时收不到事件,副标题就会停留在上一次的月份 | ||
| 771 | + this.monthIndex = _index | ||
| 772 | + // #ifndef MP-WEIXIN | ||
| 773 | + this.$nextTick(() => { | ||
| 774 | + this.scrollIntoView = '' | ||
| 775 | + this.scrollIntoView = `month-${_index}` | ||
| 776 | + this.scrollIntoViewScroll = this.scrollIntoView | ||
| 777 | + }) | ||
| 778 | + // #endif | ||
| 779 | + // #ifdef MP-WEIXIN | ||
| 780 | + // scrollTop与scrollIntoView同理,需要有一个值变动过程才会生效, | ||
| 781 | + // 否则二次打开日历时沿用上次的数值,scroll-view不会滚动到默认月份 | ||
| 782 | + const _top = this.months[_index].top || 0; | ||
| 783 | + this.scrollTop = 0; | ||
| 784 | + if (_top !== 0) { | ||
| 785 | + this.$nextTick(() => { | ||
| 786 | + this.scrollTop = _top; | ||
| 787 | + }) | ||
| 788 | + } | ||
| 789 | + // #endif | ||
| 314 | } | 790 | } |
| 315 | }, | 791 | }, |
| 316 | - handleCancel() { | ||
| 317 | - this.close(); | 792 | + // scroll-view滚动监听 |
| 793 | + onScroll(event) { | ||
| 794 | + // 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值 | ||
| 795 | + const scrollTop = Math.max(0, event.detail.scrollTop) | ||
| 796 | + // 月份的top值尚未测量出来时不做判断,否则所有月份的判断阈值相同, | ||
| 797 | + // 会把副标题固定到最后一个月份上 | ||
| 798 | + if (!this.months.some(item => typeof item.top === 'number')) return | ||
| 799 | + // 将当前滚动条数值,与各月份的top值比较,可以得出当前滚动到了哪一个月份的索引 | ||
| 800 | + let monthIndex = 0 | ||
| 801 | + for (let i = 0; i < this.months.length; i++) { | ||
| 802 | + const top = this.months[i].top | ||
| 803 | + if (typeof top !== 'number') continue | ||
| 804 | + if (scrollTop >= top) { | ||
| 805 | + monthIndex = i | ||
| 806 | + } else { | ||
| 807 | + break | ||
| 808 | + } | ||
| 809 | + } | ||
| 810 | + this.monthIndex = monthIndex | ||
| 811 | + this.scrollIntoViewScroll = `month-${monthIndex}` | ||
| 812 | + }, | ||
| 813 | + // 更新月份的top值 | ||
| 814 | + onUpdateMonthTop(topArr = []) { | ||
| 815 | + if (this.monthSwitch) { | ||
| 816 | + return | ||
| 817 | + } | ||
| 818 | + this.updateMonthTop(topArr) | ||
| 318 | }, | 819 | }, |
| 319 | - handleConfirm() { | ||
| 320 | - // 校验:必须选择到最后一级 | ||
| 321 | - if (!this.isCurrentLeafNode()) { | ||
| 322 | - uni.showToast({ title: '请选择到最后一级', icon: 'none' }); | ||
| 323 | - return; | 820 | + updateMonthTop(topArr = []) { |
| 821 | + // 设置对应月份的top值,用于onScroll方法更新月份 | ||
| 822 | + topArr.map((item, index) => { | ||
| 823 | + this.months[index].top = item | ||
| 824 | + }) | ||
| 825 | + | ||
| 826 | + // 获取默认日期的下标 | ||
| 827 | + if (!this.defaultDate) { | ||
| 828 | + // 如果没有设置默认日期,则将当天日期设置为默认选中的日期 | ||
| 829 | + const selected = dayjs().format("YYYY-MM") | ||
| 830 | + this.scrollIntoDefaultMonth(selected) | ||
| 831 | + return | ||
| 832 | + } | ||
| 833 | + let selected = dayjs().format("YYYY-MM"); | ||
| 834 | + // 单选模式,可以是字符串或数组,Date对象等 | ||
| 835 | + if (!test.array(this.defaultDate)) { | ||
| 836 | + selected = dayjs(this.defaultDate).format("YYYY-MM") | ||
| 837 | + } else { | ||
| 838 | + selected = dayjs(this.defaultDate[0]).format("YYYY-MM"); | ||
| 324 | } | 839 | } |
| 325 | - const values = this.getSelectedValues(); | ||
| 326 | - this.confirmValues = [...values]; | ||
| 327 | - // 向外输出 选中值数组 | ||
| 328 | - this.$emit('update:modelValue', values); | ||
| 329 | - this.$emit('confirm', values); | ||
| 330 | - this.close(); | 840 | + this.scrollIntoDefaultMonth(selected) |
| 331 | } | 841 | } |
| 332 | } | 842 | } |
| 333 | } | 843 | } |
| 334 | </script> | 844 | </script> |
| 335 | 845 | ||
| 336 | -<style lang="scss"> | ||
| 337 | - .area-box { | ||
| 338 | - width: 100%; | 846 | +<style lang="scss" scoped> |
| 847 | +.u-calendar { | ||
| 848 | + color: var(--up-main-color, $u-main-color); | ||
| 849 | + background-color: var(--up-card-bg-color, #ffffff); | ||
| 850 | + | ||
| 851 | + &__time-panel { | ||
| 852 | + padding: 8px 16px 0; | ||
| 853 | + } | ||
| 854 | + | ||
| 855 | + &__time-row { | ||
| 856 | + height: 34px; | ||
| 857 | + display: flex; | ||
| 858 | + align-items: center; | ||
| 859 | + justify-content: space-between; | ||
| 860 | + margin-bottom: 6px; | ||
| 861 | + } | ||
| 862 | + | ||
| 863 | + &__time-date { | ||
| 864 | + font-size: 13px; | ||
| 865 | + color: #303133; | ||
| 866 | + } | ||
| 867 | + | ||
| 868 | + &__time-trigger { | ||
| 869 | + min-width: 92px; | ||
| 870 | + height: 30px; | ||
| 871 | + padding: 0 10px; | ||
| 872 | + border-radius: 15px; | ||
| 873 | + border: 1px solid #dcdfe6; | ||
| 874 | + display: flex; | ||
| 875 | + align-items: center; | ||
| 876 | + justify-content: center; | ||
| 877 | + } | ||
| 878 | + | ||
| 879 | + &__time-text { | ||
| 880 | + font-size: 14px; | ||
| 881 | + color: #303133; | ||
| 882 | + } | ||
| 883 | + | ||
| 884 | + &__time-picker { | ||
| 885 | + width: 320px; | ||
| 886 | + background-color: #ffffff; | ||
| 887 | + border-radius: 8px; | ||
| 339 | overflow: hidden; | 888 | overflow: hidden; |
| 340 | - height: 800rpx; | 889 | + } |
| 341 | 890 | ||
| 342 | - >view { | ||
| 343 | - width: 150%; | ||
| 344 | - transition: transform 0.3s ease-in-out 0s; | ||
| 345 | - transform: translateX(0); | 891 | + &__time-picker__header { |
| 892 | + height: 44px; | ||
| 893 | + padding: 0 14px; | ||
| 894 | + display: flex; | ||
| 895 | + align-items: center; | ||
| 896 | + justify-content: space-between; | ||
| 897 | + border-bottom: 1px solid #f0f0f0; | ||
| 898 | + } | ||
| 346 | 899 | ||
| 347 | - &.change { | ||
| 348 | - transform: translateX(-33.3333333%); | ||
| 349 | - } | ||
| 350 | - } | 900 | + &__time-picker__cancel { |
| 901 | + font-size: 14px; | ||
| 902 | + color: #909399; | ||
| 903 | + } | ||
| 351 | 904 | ||
| 352 | - .area-item { | ||
| 353 | - height: 800rpx; | ||
| 354 | - } | 905 | + &__time-picker__title { |
| 906 | + font-size: 15px; | ||
| 907 | + color: #303133; | ||
| 908 | + font-weight: 600; | ||
| 355 | } | 909 | } |
| 356 | 910 | ||
| 357 | - .u-cascader-action { | ||
| 358 | - border-top: 1px solid var(--up-border-color, #eee); | 911 | + &__time-picker__confirm { |
| 912 | + font-size: 14px; | ||
| 913 | + color: #3c9cff; | ||
| 359 | } | 914 | } |
| 360 | -</style> | ||
| 361 | \ No newline at end of file | 915 | \ No newline at end of file |
| 916 | + | ||
| 917 | + &__time-picker__body { | ||
| 918 | + width: 320px; | ||
| 919 | + height: 200px; | ||
| 920 | + } | ||
| 921 | + | ||
| 922 | + &__time-picker__item { | ||
| 923 | + height: 40px; | ||
| 924 | + line-height: 40px; | ||
| 925 | + text-align: center; | ||
| 926 | + font-size: 16px; | ||
| 927 | + color: #303133; | ||
| 928 | + } | ||
| 929 | + | ||
| 930 | + &__confirm { | ||
| 931 | + padding: 7px 18px; | ||
| 932 | + } | ||
| 933 | +} | ||
| 934 | +</style> |
uni_modules/uview-plus/components/u-cascader/util.js
0 → 100644
| 1 | +import dayjs from '../u-datetime-picker/dayjs.esm.min.js'; | ||
| 2 | +export default { | ||
| 3 | + methods: { | ||
| 4 | + // 设置月份数据 | ||
| 5 | + setMonth() { | ||
| 6 | + // 月初是周几 | ||
| 7 | + const day = dayjs(this.date).date(1).day() | ||
| 8 | + const start = day == 0 ? 6 : day - 1 | ||
| 9 | + | ||
| 10 | + // 本月天数 | ||
| 11 | + const days = dayjs(this.date).endOf('month').format('D') | ||
| 12 | + | ||
| 13 | + // 上个月天数 | ||
| 14 | + const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D') | ||
| 15 | + | ||
| 16 | + // 日期数据 | ||
| 17 | + const arr = [] | ||
| 18 | + // 清空表格 | ||
| 19 | + this.month = [] | ||
| 20 | + | ||
| 21 | + // 添加上月数据 | ||
| 22 | + arr.push( | ||
| 23 | + ...new Array(start).fill(1).map((e, i) => { | ||
| 24 | + const day = prevDays - start + i + 1 | ||
| 25 | + | ||
| 26 | + return { | ||
| 27 | + value: day, | ||
| 28 | + disabled: true, | ||
| 29 | + date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD') | ||
| 30 | + } | ||
| 31 | + }) | ||
| 32 | + ) | ||
| 33 | + | ||
| 34 | + // 添加本月数据 | ||
| 35 | + arr.push( | ||
| 36 | + ...new Array(days - 0).fill(1).map((e, i) => { | ||
| 37 | + const day = i + 1 | ||
| 38 | + | ||
| 39 | + return { | ||
| 40 | + value: day, | ||
| 41 | + date: dayjs(this.date).date(day).format('YYYY-MM-DD') | ||
| 42 | + } | ||
| 43 | + }) | ||
| 44 | + ) | ||
| 45 | + | ||
| 46 | + // 添加下个月 | ||
| 47 | + arr.push( | ||
| 48 | + ...new Array(42 - days - start).fill(1).map((e, i) => { | ||
| 49 | + const day = i + 1 | ||
| 50 | + | ||
| 51 | + return { | ||
| 52 | + value: day, | ||
| 53 | + disabled: true, | ||
| 54 | + date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD') | ||
| 55 | + } | ||
| 56 | + }) | ||
| 57 | + ) | ||
| 58 | + | ||
| 59 | + // 分割数组 | ||
| 60 | + for (let n = 0; n < arr.length; n += 7) { | ||
| 61 | + this.month.push( | ||
| 62 | + arr.slice(n, n + 7).map((e, i) => { | ||
| 63 | + e.index = i + n | ||
| 64 | + | ||
| 65 | + // 自定义信息 | ||
| 66 | + const custom = this.customList.find((c) => c.date == e.date) | ||
| 67 | + | ||
| 68 | + // 农历 | ||
| 69 | + if (this.lunar) { | ||
| 70 | + const { | ||
| 71 | + IDayCn, | ||
| 72 | + IMonthCn | ||
| 73 | + } = this.getLunar(e.date) | ||
| 74 | + e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + return { | ||
| 78 | + ...e, | ||
| 79 | + ...custom | ||
| 80 | + } | ||
| 81 | + }) | ||
| 82 | + ) | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | +} |
uni_modules/uview-plus/libs/config/props.js
| @@ -188,3 +188,11 @@ export default { | @@ -188,3 +188,11 @@ export default { | ||
| 188 | ...Transition, | 188 | ...Transition, |
| 189 | ...Upload | 189 | ...Upload |
| 190 | } | 190 | } |
| 191 | + | ||
| 192 | +export function registerComponentProps(defaultProps = {}) { | ||
| 193 | + Object.keys(defaultProps || {}).forEach((key) => { | ||
| 194 | + const componentProps = ensureComponentProps(key) | ||
| 195 | + mergeDefaults(componentProps, defaultProps[key]) | ||
| 196 | + }) | ||
| 197 | + return props | ||
| 198 | +} | ||
| 191 | \ No newline at end of file | 199 | \ No newline at end of file |