Commit 9441e28e2c0927c2244657b7fb5eefca19dabefa

Authored by 刘淇
1 parent 3c3d6799

考勤打卡

api/index/index.js
... ... @@ -48,5 +48,27 @@ export const taskDetailsWithSmartSort = (params) => {
48 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
... ... @@ -54,7 +54,7 @@
54 54 },
55 55 /* 小程序特有相关 */
56 56 "mp-weixin" : {
57   - "appid" : "wx4ba1e0a1b7075371",
  57 + "appid" : "wxcb4cd34066b97d82",
58 58 "setting" : {
59 59 "urlCheck" : false
60 60 },
... ...
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 2 <view class="page-container">
3 3 <view class="work-order-form-content commonPageLRpadding">
4 4 <up-form label-position="left" :model="workOrderForm" ref="workOrderFormRef" labelWidth="200rpx">
5   - <!-- 业务线单选框:移入form内,作为第一个表单项(核心调整) -->
  5 + <!-- 业务线单选框 -->
6 6 <up-form-item label="业务线" prop="busiLineCn" border-bottom required>
7 7 <up-radio-group v-model="workOrderForm.busiLineCn" placement="row" @change="handleBusiLineChange">
8 8 <up-radio v-for="item in busiLineOptions" :key="item.name" :label="item.name" :name="item.name"></up-radio>
9 9 </up-radio-group>
10   -
11 10 </up-form-item>
12   -
13 11 <!-- 1. 工单位置(地图选择) -->
14 12 <up-form-item label="工单位置" prop="workLocation" border-bottom required
15 13 @click="chooseWorkLocation(); hideKeyboard()">
16 14 <up-input v-model="workOrderForm.workLocation" border="none" readonly suffix-icon="map-fill"
17 15 placeholder="点击选择工单位置"></up-input>
18 16 </up-form-item>
19   -
20   - <!-- 2. 道路名称(下拉框) -->
  17 + <!-- 2. 道路名称(下拉框action-sheet,保留不变) -->
21 18 <up-form-item label="道路名称" prop="roadName" border-bottom required
22 19 @click="handleActionSheetOpen('roadName'); hideKeyboard()">
23 20 <up-input v-model="workOrderForm.roadName" readonly disabled-color="#ffffff" placeholder="请先选择工单位置"
... ... @@ -26,17 +23,14 @@
26 23 <up-icon name="arrow-right" size="16"></up-icon>
27 24 </template>
28 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 28 <up-input v-model="workOrderForm.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型"
34 29 border="none"></up-input>
35 30 <template #right>
36 31 <up-icon name="arrow-right" size="16"></up-icon>
37 32 </template>
38 33 </up-form-item>
39   -
40 34 <!-- 新增:紧急程度选择 -->
41 35 <up-form-item label="紧急程度" prop="pressingTypeName" border-bottom required
42 36 @click="handleActionSheetOpen('pressingType'); hideKeyboard()">
... ... @@ -46,61 +40,60 @@
46 40 <up-icon name="arrow-right" size="16"></up-icon>
47 41 </template>
48 42 </up-form-item>
49   -
50 43 <!-- 4. 情况描述(文本域) -->
51 44 <up-form-item label="情况描述" prop="problemDesc" required>
52 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 47 </up-form-item>
55   -
56   - <!-- 问题照片(核心修复:绑定纯数组) -->
  48 + <!-- 问题照片 -->
57 49 <up-form-item label="问题照片" prop="problemImgs" border-bottom required>
58 50 <up-upload :file-list="problemImgs.imgList.value || []" @after-read="problemImgs.uploadImgs"
59 51 @delete="problemImgs.deleteImg" multiple :width="70" :height="70"
60 52 :max-count="problemImgs.uploadConfig.maxCount" :upload-text="problemImgs.uploadConfig.uploadText"
61 53 :size-type="problemImgs.uploadConfig.sizeType"></up-upload>
62 54 </up-form-item>
63   -
64 55 <!-- 完成时间 -->
65 56 <up-form-item label="希望完成时间" prop="expectedFinishDate" @click="show = true; hideKeyboard()">
66 57 <up-input v-model="workOrderForm.expectedFinishDate" border="none" readonly placeholder="点击选择时间"></up-input>
67 58 <template #right>
68   - <!-- 核心修复:用view包裹图标,添加双重@click.stop -->
69 59 <view v-if="workOrderForm.expectedFinishDate" @click.stop>
70 60 <up-icon name="close" size="20" @click.stop="clearExpectedFinishDate"></up-icon>
71 61 </view>
72 62 <up-icon name="arrow-right" size="16" v-else></up-icon>
73 63 </template>
74 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 78 </up-form>
92 79 </view>
93   -
94 80 <!-- 底部提交按钮 -->
95 81 <view class="fixed-bottom-btn-wrap">
96 82 <up-button type="primary" text="提交工单" @click="submitWorkOrder"></up-button>
97 83 </view>
98   -
99   - <!-- 合并后的通用下拉弹窗 -->
  84 + <!-- 通用下拉弹窗:道路名称、紧急程度保留 -->
100 85 <up-action-sheet :show="showActionSheet" :actions="currentActionSheetData.list"
101 86 :title="currentActionSheetData.title" @close="handleActionSheetClose"
102 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 98 <up-datetime-picker :show="show" v-model="expectedFinishDate" mode="datetime" :min-date="new Date()"
106 99 @cancel="show = false" @confirm="expectedFinishDateConfirm"></up-datetime-picker>
... ... @@ -111,74 +104,37 @@
111 104 import { ref, reactive } from 'vue'
112 105 import { onReady, onShow, onLoad } from '@dcloudio/uni-app';
113 106 import { useUploadImgs } from '@/common/utils/useUploadImgs'
114   -import { getRoadListByLatLng } from '@/api/common'
  107 +import { getRoadListByLatLng, getProblemListByType } from '@/api/common'
115 108 import { universalApproval, workorderCreate } from '@/api/work-order-manage/work-order-manage'
116 109 import { timeFormat } from '@/uni_modules/uview-plus'
117 110 import { nextStepMap } from '@/common/utils/common'
118 111 import { useUserStore } from '@/pinia/user';
119 112 import { getDictLabel, getDictValue } from '@/common/utils/dict';
120   -
121   -// ========== 状态管理 ==========
122 113 const userStore = useUserStore();
123   -
124   -// ========== 业务线相关状态(核心改造:已整合到workOrderForm中,删除冗余selectedBusiLine) ==========
125   -// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码
126 114 const BUSI_LINE_DICT_TYPE = 'business_line';
127   -// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」
128 115 const CJK_RE = /[一-龥]/;
129   -
130   -// 业务线选项列表(仅保留name字段=中文名称,适配单选框配置)
131 116 const busiLineOptions = ref([]);
132   -// 格式化业务线选项(兼容1个/2个/3个的情况,生成中文名称列表)
133 117 const formatBusiLineOptions = () => {
134 118 if (!userStore.userInfo?.user?.busiLine) {
135 119 busiLineOptions.value = [];
136 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 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 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 131 const groupChange = (n) => {
164 132 console.log('groupChange', n);
165 133 };
166   -
167   -const radioChange = (n) => {
168   - console.log('radioChange', n);
169   -};
170   -
171   -// ========== 工具方法 ==========
172   -// 编码 → 中文名称。字典未拉到时(登录时字典请求是容错的)兜底显示原始编码,
173   -// 保证选项不为空、页面仍可用
174 134 const getBusiLineCnByEn = (busiLineEn) => {
175 135 if (!busiLineEn) return '';
176 136 return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn;
177 137 };
178   -
179   -// ========== 工具方法:通过中文名称获取对应的英文标识 ==========
180   -// 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回;
181   -// 中文名解析不到仍返回空串,沿用调用方原有的异常提示
182 138 const getBusiLineEnByCn = (cnName) => {
183 139 if (!cnName) return '';
184 140 const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName);
... ... @@ -186,10 +142,75 @@ const getBusiLineEnByCn = (cnName) =&gt; {
186 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 214 const problemImgs = useUploadImgs({
194 215 maxCount: 3,
195 216 uploadText: '选择问题照片',
... ... @@ -197,53 +218,41 @@ const problemImgs = useUploadImgs({
197 218 formRef: workOrderFormRef,
198 219 fieldName: 'problemImgs'
199 220 })
200   -
201   -// 无需再手动初始化普通数组,直接使用 ref 响应式数据
202 221 if (!Array.isArray(problemImgs.rawImgList.value)) {
203 222 problemImgs.rawImgList.value = [];
204 223 }
205   -
206   -// ========== 页面状态 ==========
207   -// 通用弹窗控制
208 224 const showActionSheet = ref(false)
209   -// 当前弹窗配置
210 225 const currentActionSheetData = reactive({
211 226 type: '',
212 227 list: [],
213 228 title: ''
214 229 })
215   -// 完成时间选择器控制
216 230 const show = ref(false)
217 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 234 const roadNameList = ref([])
225   -const orderNameList = ref([])
226 235 const pressingTypeList = ref([])
227 236  
228   -// ========== 工单表单数据(核心调整:新增busiLineCn字段,绑定业务线) ==========
  237 +// ========== 表单数据 ==========
229 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 251 keyInspection: '否'
242 252 })
243   -
244   -// ========== 表单校验规则(核心调整:新增busiLineCn校验规则) ==========
  253 +// 表单校验规则:prop改成 orderName,对齐参考页面
245 254 const workOrderFormRules = reactive({
246   - busiLineCn: [ // 新增:业务线必选校验
  255 + busiLineCn: [
247 256 { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] }
248 257 ],
249 258 workLocation: [
... ... @@ -253,7 +262,7 @@ const workOrderFormRules = reactive({
253 262 { type: 'string', required: true, message: '请选择道路名称', trigger: ['change', 'blur'] }
254 263 ],
255 264 orderName: [
256   - { type: 'string', required: true, message: '请选择问题类型', trigger: ['change', 'blur'] }
  265 + { type: 'string', required: true, message: '请选择问题类型', trigger: 'change' }
257 266 ],
258 267 pressingTypeName: [
259 268 { type: 'string', required: true, message: '请选择紧急程度', trigger: ['change'] }
... ... @@ -262,38 +271,27 @@ const workOrderFormRules = reactive({
262 271 { type: 'string', required: true, message: '请输入情况描述', trigger: ['change', 'blur'] },
263 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 277 onLoad((options) => {
271   - // 初始化业务线选项
272 278 formatBusiLineOptions();
273   - // 默认选中第一个业务线(赋值给workOrderForm.busiLineCn,核心调整)
274 279 if (busiLineOptions.value.length > 0) {
275 280 workOrderForm.busiLineCn = busiLineOptions.value[0].name;
  281 + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn)
  282 + loadProblemCascader(busiLineEn)
276 283 }
277   -
278   - // 判断是否为重新提交状态
279 284 console.log(options)
280 285 if (options.isRenew == 1 && options.tempKey) {
281 286 isRenew.value = true;
282 287 const tempKey = options.tempKey;
283   -
284   - // 1. 从本地同步存储中读取完整工单数据
285 288 try {
286 289 const orderData = uni.getStorageSync(tempKey);
287 290 if (orderData && typeof orderData === 'object') {
288 291 renewOrderData.value = orderData;
289   -
290   - console.log('123213')
291   - console.log(orderData)
292   - // 2. 回显工单数据到表单
293 292 echoOrderData(renewOrderData.value);
294 293 } else {
295 294 uni.showToast({ title: '工单数据不存在,无法重新提交', icon: 'none' });
296   - // 跳转回列表页
297 295 setTimeout(() => uni.navigateBack(), 1000);
298 296 return;
299 297 }
... ... @@ -303,106 +301,114 @@ onLoad((options) =&gt; {
303 301 setTimeout(() => uni.navigateBack(), 1000);
304 302 return;
305 303 } finally {
306   - // 3. 关键:用完即删,避免本地冗余存储和数据泄露
307 304 uni.removeStorageSync(tempKey);
308 305 }
309 306 }
310 307 });
311   -
312 308 onReady(() => {
313   - // 设置表单校验规则
314 309 if (workOrderFormRef.value) {
315 310 workOrderFormRef.value.setRules(workOrderFormRules)
316 311 }
317 312 console.log('工单表单规则初始化完成')
318 313 })
319   -
320 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 315 pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('workorder_pressing_type'))
330 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 325 workOrderForm.roadId = orderItem.roadId || 0;
342 326 workOrderForm.roadName = orderItem.roadName || '';
343 327 workOrderForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || '';
344 328 workOrderForm.orderName = orderItem.orderName || '';
  329 + workOrderForm.orderCode = orderItem.orderCode || '';
345 330 workOrderForm.pressingType = orderItem.pressingType || '';
346 331 workOrderForm.pressingTypeName = uni.$dict.getDictLabel('workorder_pressing_type', orderItem.pressingType) || '';
347 332 workOrderForm.problemDesc = orderItem.remark || '';
348 333 workOrderForm.lat = orderItem.lat || 0;
349 334 workOrderForm.lon = orderItem.lon || 0;
350 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 339 if (orderItem.problemsImgs && Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length > 0) {
356 340 const imgList = orderItem.problemsImgs.map((imgUrl, index) => ({
357 341 url: imgUrl,
358 342 name: `renew_img_${index}`,
359   - status: 'success' // 标记为已上传状态
  343 + status: 'success'
360 344 }));
361 345 problemImgs.imgList.value = imgList;
362 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 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 382 const handleBusiLineChange = () => {
376   - // 1. 清空已渲染的道路名称和表单字段
  383 + // 清空道路
377 384 workOrderForm.roadName = '';
378 385 workOrderForm.roadId = 0;
379   - // 2. 清空道路名称列表数据
380 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 394 if (workOrderForm.workLocation) {
383 395 getRoadListByBusiLine();
384 396 }
385 397 };
386 398  
387   -/**
388   - * 封装:根据当前选中业务线(中文)获取英文标识,再请求道路列表
389   - */
390 399 const getRoadListByBusiLine = async () => {
391 400 if (!workOrderForm.lat || !workOrderForm.lon) {
392 401 return;
393 402 }
394   - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整)
395 403 const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn);
396 404 if (!busiLineEn) {
397 405 uni.showToast({ title: '业务线标识异常', icon: 'none' });
398 406 return;
399 407 }
400   -
401 408 try {
402 409 uni.showLoading({ title: '获取道路名称中...' });
403 410 const roadRes = await getRoadListByLatLng({
404   - busiLine: busiLineEn, // 传递英文标识(yl/sz/wy),接口正常使用
405   - // companyCode: 'sls', // 传递英文标识(yl/sz/wy),接口正常使用
  411 + busiLine: busiLineEn,
406 412 latitude: workOrderForm.lat,
407 413 longitude: workOrderForm.lon
408 414 });
... ... @@ -425,115 +431,72 @@ const getRoadListByBusiLine = async () =&gt; {
425 431 }
426 432 };
427 433  
428   -// ========== 通用弹窗方法 ==========
429   -/**
430   - * 打开通用下拉弹窗
431   - */
  434 +// 通用action-sheet方法
432 435 const handleActionSheetOpen = (type) => {
433   - // 道路名称需先校验工单位置是否选择
434 436 if (type === 'roadName' && !workOrderForm.workLocation) {
435 437 uni.showToast({ title: '请先选择工单位置', icon: 'none' })
436 438 return
437 439 }
438   -
439   - // 配置当前弹窗参数
440 440 const configMap = {
441 441 roadName: {
442 442 title: '请选择道路名称',
443 443 list: roadNameList.value
444 444 },
445   - orderName: {
446   - title: '请选择问题类型',
447   - list: orderNameList.value
448   - },
449 445 pressingType: {
450 446 title: '请选择紧急程度',
451 447 list: pressingTypeList.value
452 448 }
453 449 }
454   -
455 450 currentActionSheetData.type = type
456 451 currentActionSheetData.title = configMap[type].title
457 452 currentActionSheetData.list = configMap[type].list
458 453 showActionSheet.value = true
459 454 }
460   -
461   -/**
462   - * 关闭通用下拉弹窗
463   - */
464 455 const handleActionSheetClose = () => {
465 456 showActionSheet.value = false
466   - // 重置当前弹窗配置
467 457 currentActionSheetData.type = ''
468 458 currentActionSheetData.list = []
469 459 currentActionSheetData.title = ''
470 460 }
471   -
472   -/**
473   - * 通用下拉弹窗选择事件
474   - */
475 461 const handleActionSheetSelect = (e) => {
476 462 const { type } = currentActionSheetData
477   - // 根据类型处理不同的选择逻辑
478 463 switch (type) {
479 464 case 'roadName':
480 465 workOrderForm.roadName = e.name
481 466 workOrderForm.roadId = e.code
482 467 workOrderFormRef.value?.validateField('roadName')
483 468 break
484   - case 'orderName':
485   - workOrderForm.orderName = e.name
486   - workOrderFormRef.value?.validateField('orderName')
487   - break
488 469 case 'pressingType':
489   - console.log(e)
490 470 workOrderForm.pressingType = e.value
491 471 workOrderForm.pressingTypeName = e.name
492 472 workOrderFormRef.value?.validateField('pressingTypeName')
493 473 break
494 474 }
495   - // 关闭弹窗
496 475 showActionSheet.value = false
497 476 }
498 477  
499   -/**
500   - * 返回上一页
501   - */
502 478 const navigateBack = () => {
503 479 uni.reLaunch({
504   - url: '/pages-sub/problem/work-order-manage/index', // 列表页路径
  480 + url: '/pages-sub/problem/work-order-manage/index',
505 481 fail: () => {
506   - // 兜底:若 reLaunch 失败,返回2层(跳过提交页)
507 482 uni.navigateBack({ delta: 2 });
508 483 }
509 484 });
510 485 }
511   -
512   -// 新增:清除希望完成时间的方法
513 486 const clearExpectedFinishDate = () => {
514 487 workOrderForm.expectedFinishDate = ''
515   - // 清除后触发一次字段校验,确保校验状态更新
516   - // this.$refs.inspectFormRef.validateField('expectedFinishDate')
517 488 }
518   -
519   -/**
520   - * 选择工单位置
521   - */
522 489 const chooseWorkLocation = () => {
523 490 uni.chooseLocation({
524 491 success: async (res) => {
525 492 workOrderForm.roadName = ''
526 493 workOrderForm.roadId = 0
527 494 roadNameList.value = []
528   -
529 495 workOrderForm.workLocation = res.name
530 496 workOrderForm.lat = res.latitude
531 497 workOrderForm.lon = res.longitude
532   -
533 498 workOrderFormRef.value?.validateField('workLocation')
534 499 workOrderFormRef.value?.validateField('roadName')
535   -
536   - // 改用封装后的方法,自动携带选中的业务线英文标识
537 500 await getRoadListByBusiLine();
538 501 },
539 502 fail: (err) => {
... ... @@ -542,44 +505,28 @@ const chooseWorkLocation = () =&gt; {
542 505 }
543 506 })
544 507 }
545   -
546   -/**
547   - * 完成时间确认
548   - */
549 508 const expectedFinishDateConfirm = (e) => {
550 509 console.log('选择的完成时间:', e)
551 510 workOrderForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss')
552 511 show.value = false
553 512 }
554   -
555   -/**
556   - * 隐藏键盘
557   - */
558 513 const hideKeyboard = () => {
559 514 uni.hideKeyboard()
560 515 }
561 516  
562   -/**
563   - * 提交工单(区分新增/重新提交,接口隔离:从workOrderForm.busiLineCn取值)
564   - */
565 517 const submitWorkOrder = async () => {
566 518 try {
567   - // 先执行表单校验(包含业务线校验)
568 519 await workOrderFormRef.value.validate()
569   -
570   - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整)
571 520 const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn);
572 521 if (!busiLineEn) {
573 522 uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' });
574 523 return;
575 524 }
576   - console.log('完成时间:' + workOrderForm.expectedFinishDate)
577 525 const keyInspectionVal = workOrderForm.keyInspection === '是' ? 1 : 0
578   - // 构建公共提交数据
579 526 const commonSubmitData = {
580 527 roadId: workOrderForm.roadId,
581 528 roadName: workOrderForm.roadName,
582   - problemsImgs: problemImgs.getSuccessImgUrls(), // 复用上传逻辑的URL获取方法
  529 + problemsImgs: problemImgs.getSuccessImgUrls(),
583 530 remark: workOrderForm.problemDesc.trim(),
584 531 latLonType: 2,
585 532 lat: workOrderForm.lat,
... ... @@ -587,62 +534,47 @@ const submitWorkOrder = async () =&gt; {
587 534 lonLatAddress: workOrderForm.workLocation,
588 535 pressingType: workOrderForm.pressingType,
589 536 orderName: workOrderForm.orderName,
  537 + orderCode: workOrderForm.orderCode,
590 538 keyInspection: keyInspectionVal,
591 539 expectedFinishDate: workOrderForm.expectedFinishDate == "" ? "" : new Date(workOrderForm.expectedFinishDate).getTime(),
592 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 544 console.log(commonSubmitData)
599   - // 显示加载中
600 545 uni.showLoading({ title: '提交中...' })
601 546 let res
602   -
603   - // 核心:根据状态区分接口调用
604 547 if (isRenew.value) {
605   - // 重新提交:调用 universalApproval 接口
606 548 const renewSubmitData = {
607   - // 原有工单必要参数
608 549 workerDataId: renewOrderData.value.id,
609 550 taskKey: renewOrderData.value.taskKey,
610 551 taskId: renewOrderData.value.taskId,
611 552 operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '',
612 553 agree: 0,
613 554 reason: workOrderForm.problemDesc.trim(),
614   - // 新编辑的工单数据
615 555 ...commonSubmitData
616 556 }
617 557 res = await universalApproval(renewSubmitData)
618 558 } else {
619   - // 新增工单:调用原有 workorderCreate 接口(不影响原有功能)
620 559 res = await workorderCreate(commonSubmitData)
621 560 }
622   -
623 561 uni.hideLoading()
624 562 uni.showToast({
625 563 title: isRenew.value ? '重新提交成功' : '工单提交成功',
626 564 icon: 'success',
627 565 duration: 1000
628 566 })
629   -
630   - // 延迟跳转回列表页
631 567 setTimeout(() => {
632 568 uni.reLaunch({
633 569 url: '/pages-sub/problem/work-order-manage/index'
634 570 })
635 571 }, 1000)
636 572 } catch (error) {
637   - // 隐藏加载框
638 573 uni.hideLoading()
639   -
640   - // 区分是表单校验失败还是接口调用失败
641 574 if (!Array.isArray(error)) {
642   - // 接口调用失败
643 575 console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error)
644 576 uni.showToast({
645   - title: isRenew.value ? error.msg : error.msg,
  577 + title: error.msg || '请求异常',
646 578 icon: 'none',
647 579 duration: 2000
648 580 })
... ... @@ -652,15 +584,11 @@ const submitWorkOrder = async () =&gt; {
652 584 </script>
653 585  
654 586 <style lang="scss" scoped>
655   -// 全局页面样式
656 587 .page-container {
657 588 min-height: 100vh;
658   - padding-bottom: 100rpx; // 给底部按钮留空间
  589 + padding-bottom: 100rpx;
659 590 }
660   -
661   -
662   -// 工单表单内容容器
663 591 .work-order-form-content {
664 592 background: #fff;
665 593 }
666   -</style>
667 594 \ No newline at end of file
  595 +</style>
... ...
pages.json
... ... @@ -41,6 +41,13 @@
41 41 "navigationBarTitleText": "巡查计划"
42 42 }
43 43 },
  44 +{
  45 + "path": "attendanceRecord",
  46 + "style": {
  47 + "navigationBarTitleText": "考勤记录"
  48 + }
  49 + },
  50 +
44 51 {
45 52 "path": "patrol-manage/pending-plan-detail",
46 53 "style": {
... ...
pages/index/index.vue
... ... @@ -60,12 +60,12 @@
60 60 v-for="(baseListItem,baseListIndex) in baseList"
61 61 :key="baseListIndex"
62 62 >
63   - <up-icon
  63 + <!-- <up-icon
64 64 :customStyle="{paddingTop:20+'rpx'}"
65 65 :name="baseListItem.name"
66 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 69 <text class="grid-text">{{baseListItem.title}}</text>
70 70 </up-grid-item>
71 71 </up-grid>
... ... @@ -140,113 +140,64 @@
140 140 </template>
141 141  
142 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 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 147 import uniCharts from '@/components/uni-charts/uni-charts.vue';
148 148 import { getUnreadCount } from "@/api/user";
149 149 import {
150 150 getTaskCompletionSummary,
151 151 taskDetailsWithDistance,
152   - taskDetailsWithSmartSort
  152 + taskDetailsWithSmartSort,
  153 + getCurrentOrg,
  154 + orgTaskIndicator
153 155 } from "@/api/index/index";
154   -
155 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 160 type GroupItem = {
160   - deptIds: string[]; // 多个部门ID数组
  161 + deptIds: string[];
161 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 166 const groupConfig: GroupItem[] = [
206   - // 组2 A1B2 纯效率·有提醒
207 167 {
208 168 deptIds: ['361', '363', '279', '277', '270'],
209   - label: '[组2 A1B2 纯效率·有提醒]',
  169 + label: '[组2 A1B2 纯效率·有提醒]',
210 170 showTipHeader: true,
211 171 useWalkText: true
212 172 },
213   - // 组3 A2B1 个性化·无提醒
214 173 {
215 174 deptIds: ['252', '273', '274', '337', '359', '360'],
216   - label: '[组3 A2B1 个性化·无提醒]',
  175 + label: '[组3 A2B1 个性化·无提醒]',
217 176 showTipHeader: false,
218 177 useWalkText: false
219 178 },
220   - // 组4 A2B2 个性化·有提醒
221 179 {
222 180 deptIds: ['281', '272', '271', '338', '280'],
223   - label: '[组4 A2B2 个性化·有提醒]',
  181 + label: '[组4 A2B2 个性化·有提醒]',
224 182 showTipHeader: true,
225 183 useWalkText: false
226 184 },
227 185 ];
228   -// 默认组1:未匹配到的所有部门
229 186 const defaultGroup: GroupItem = {
230 187 deptIds: [],
231   - label: '[组1 A1B1 纯效率·无提醒]',
  188 + label: '[组1 A1B1 纯效率·无提醒]',
232 189 showTipHeader: false,
233 190 useWalkText: true
234 191 };
235   -
236   -// 获取当前部门所属分组
237 192 const currentGroup = computed(() => {
238 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 199 return defaultGroup;
247 200 });
248   -
249   -// 自动读取分组配置,无需修改其他业务代码
250 201 const tipText = computed(() => currentGroup.value.label);
251 202 const showTipHeader = computed(() => currentGroup.value.showTipHeader);
252 203 const isWalkTextMode = computed(() => currentGroup.value.useWalkText);
... ... @@ -257,13 +208,11 @@ const finishNum = ref(0);
257 208 const avgNum = ref(0);
258 209 const msgCount = ref(0);
259 210 const currentTabValue = ref(0);
260   -
261 211 const dateRange = ref<string[]>([]);
262 212 const yesterday = new Date();
263 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 215 const minDate = '2026-01-06';
266   -
267 216 const isDatePickerOpen = ref(false);
268 217 const handlePopupShow = () => {
269 218 isDatePickerOpen.value = true;
... ... @@ -279,17 +228,15 @@ const doneFullList = ref&lt;any[]&gt;([]);
279 228 const taskList = computed(() => {
280 229 return currentTabValue.value === 0 ? todoFullList.value : doneFullList.value;
281 230 });
282   -
283 231 const chooseOptions = computed(() => [
284 232 { title: `待办事项(${todoTotal.value})`, id: 'todo' },
285   - { title: `已办事项(${doneTotal.value})`, id: 'done' }
  233 + { title: `已办事项(${doneTotal.value})`, id: 'done' },
286 234 ]);
287 235  
288 236 const currentLon = ref<string>('');
289 237 const currentLat = ref<string>('');
290 238 const chartWidth = ref(0);
291 239 const chartHeight = ref(300);
292   -
293 240 const klineCategories = ref<string[]>([]);
294 241 const klineChartData = ref([
295 242 {
... ... @@ -303,12 +250,11 @@ const klineChartData = ref([
303 250 color: '#B34C17'
304 251 }
305 252 ]);
306   -
307 253 const klineOption = ref({
308 254 grid: {
309 255 top: '25%',
310 256 left: '12%',
311   - right: '8%',
  257 + right: '12%',
312 258 bottom: '16%'
313 259 },
314 260 xAxis: {
... ... @@ -323,8 +269,8 @@ const klineOption = ref({
323 269 trigger: 'axis',
324 270 formatter: (params: any[]) => {
325 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 276 legend: {
... ... @@ -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 300 name: opt.label,
344 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 322 const rpx2px = (rpx: number) => {
369 323 const systemInfo = uni.getSystemInfoSync();
370 324 return Math.floor(systemInfo.screenWidth / 750 * rpx);
371 325 };
372 326  
373   -// ========== 滚动逻辑 终极优化 ==========
  327 +// ========== 滚动逻辑 ==========
374 328 const showChartArea = ref(true);
375   -// 隐藏阈值:超过80rpx收起
376 329 const scrollHideThreshold = rpx2px(80);
377   -// 安全显示区间:≤80rpx一律显示,覆盖所有回弹残留(70/80/90)
378 330 const safeShowThreshold = rpx2px(130);
379 331 let scrollTimer: number | null = null;
380   -
381   -// 下拉触顶强制展示
382 332 const handleTouchTop = () => {
383 333 showChartArea.value = true;
384 334 };
385   -
386 335 const onListScroll = (e: any) => {
387 336 const scrollTop = e.detail.scrollTop;
388   - console.log('scrollTop', scrollTop)
389 337 if (scrollTimer) clearTimeout(scrollTimer);
390   -
391   - // 实时:只要在安全区间立刻显示
392 338 if (scrollTop <= safeShowThreshold) {
393 339 showChartArea.value = true;
394 340 } else if (scrollTop > scrollHideThreshold) {
395 341 showChartArea.value = false;
396 342 }
397   -
398   - // 滚动停止兜底二次校验
399 343 scrollTimer = setTimeout(() => {
400 344 if (scrollTop <= safeShowThreshold) {
401 345 showChartArea.value = true;
402 346 }
403 347 }, 100);
404 348 };
405   -
406   -// 切换tab重置图表显示
407 349 const handleTabClick = (tabIndex: number) => {
408 350 if (currentTabValue.value === tabIndex) return;
409 351 currentTabValue.value = tabIndex;
410   - // 切换tab强制显示图表
411 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 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 401 initChartData(dateRange.value[0], dateRange.value[1]);
504 402 };
... ... @@ -508,8 +406,18 @@ const getUnread = async () =&gt; {
508 406 const res = await getUnreadCount();
509 407 msgCount.value = res || 0;
510 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) =&gt; {
519 427 const res = await getTaskCompletionSummary(postData);
520 428 klineCategories.value = res.map((item: any) => item.currentDate);
521 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 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) =&gt; {
542 450 await nextTick();
543 451 } catch (err) {
544 452 uni.showToast({ title: '加载任务失败', icon: 'none' });
545   - console.error('加载distance接口任务报错', err);
  453 + console.error('loadDistanceApiTask error', err);
546 454 todoFullList.value = [];
547 455 doneFullList.value = [];
548 456 todoTotal.value = 0;
549 457 doneTotal.value = 0;
550 458 }
551 459 };
552   -
553 460 const loadSmartSortTask = async (lon: string, lat: string) => {
554 461 try {
555 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 466 todoFullList.value = todoRes?.sortedTasks || [];
568 467 doneFullList.value = doneRes?.sortedTasks || [];
... ... @@ -573,14 +472,13 @@ const loadSmartSortTask = async (lon: string, lat: string) =&gt; {
573 472 await nextTick();
574 473 } catch (err) {
575 474 uni.showToast({ title: '加载任务失败', icon: 'none' });
576   - console.error('加载smartSort接口任务报错', err);
  475 + console.error('loadSmartSortTask error', err);
577 476 todoFullList.value = [];
578 477 doneFullList.value = [];
579 478 todoTotal.value = 0;
580 479 doneTotal.value = 0;
581 480 }
582 481 };
583   -
584 482 const loadAllTaskByDept = async () => {
585 483 const loc = await getCurrentLocation();
586 484 currentLon.value = loc.lon;
... ... @@ -592,37 +490,119 @@ const loadAllTaskByDept = async () =&gt; {
592 490 }
593 491 };
594 492  
595   -const handleDateConfirm = (e: string[]) => {
  493 +// 日期确认回调:更新图表 + 重新请求指标orgTaskIndicator
  494 +const handleDateConfirm = async (e: string[]) => {
596 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 564 const handleTaskClick = (item: any) => {
605 565 if (item.taskType == "maintain") {
606 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 576 if (item.taskType == "inspection") {
616 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 602 </script>
625 603  
  604 +
  605 +
626 606 <style scoped lang="scss">
627 607 $primary-color: #1677ff;
628 608 $danger-color: #E53935;
... ...
pages/mine/index.vue
... ... @@ -26,6 +26,7 @@
26 26 <up-cell title="账号" :value="userInfo.username"></up-cell>
27 27 <up-cell title="电话" :value="userInfo.mobile"></up-cell>
28 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 30 </up-cell-group>
30 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 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 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 59 </view>
62 60 </view>
63   - </template>
  61 + </view>
64 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 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 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 174 </view>
75   - </up-popup>
  175 + </u-popup>
76 176 </template>
77 177  
78 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 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 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 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 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 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 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 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 188 ...Transition,
189 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 199 \ No newline at end of file
... ...