Commit bc2154934a924f10abdab01f46174db881080bd9

Authored by 刘淇
1 parent 9441e28e

考勤记录

api/index/index.js
@@ -63,7 +63,7 @@ export const getCurrentOrg = (params) => { @@ -63,7 +63,7 @@ export const getCurrentOrg = (params) => {
63 * @returns {Promise} 63 * @returns {Promise}
64 */ 64 */
65 export const orgTaskIndicator = (params) => { 65 export const orgTaskIndicator = (params) => {
66 - return get('/app-api/homePage/summary/orgTaskIndicator', params); 66 + return post('/app-api/homePage/summary/orgTaskIndicator', params);
67 }; 67 };
68 68
69 69
api/user.js
@@ -85,6 +85,25 @@ export const getSmsCode = (data) => { @@ -85,6 +85,25 @@ export const getSmsCode = (data) => {
85 } 85 }
86 86
87 87
  88 +/**
  89 + * 查询月度考勤日历
  90 + * @returns {Promise}
  91 + */
  92 +export const attendanceDaily = (data) => {
  93 + return get('/app-api/garden/attendance-daily/calendar',data)
  94 +}
  95 +
  96 +/**
  97 + * 查询月度考勤日历
  98 + * @returns {Promise}
  99 + */
  100 +export const dayDetail = (data) => {
  101 + return get('/app-api/garden/attendance-daily/day-detail',data)
  102 +}
  103 +
  104 +
  105 +
  106 +
88 107
89 108
90 109
pages-sub/daily/attendanceRecord.vue
1 <template> 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> 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>
32 </view> 17 </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="{ 18 + <!-- 手写内联日历,无弹窗 -->
  19 + <view class="calendar-wrap">
  20 + <view class="calendar-header">
  21 + <!-- 左右横向排列:上一月 | 年月标题 | 下一月 -->
  22 + <view class="calendar-btn" @click="prevMonth">上一月</view>
  23 + <view class="calendar-title">{{ currentYear }}年{{ currentMonth }}月</view>
  24 + <!-- 当前是最新月份时,隐藏下个月按钮 -->
  25 + <view v-if="canNext" class="calendar-btn" @click="nextMonth">下一月</view>
  26 + <view v-else class="calendar-btn empty-btn"></view>
  27 + </view>
  28 + <!-- 星期头部 -->
  29 + <view class="week-row">
  30 + <view class="week-item" v-for="w in weekList" :key="w">{{ w }}</view>
  31 + </view>
  32 + <!-- 日期网格 -->
  33 + <view class="day-row" v-for="(row, rIdx) in dayRows" :key="rIdx">
  34 + <view class="day-item" v-for="(day, dIdx) in row" :key="dIdx" :class="{
40 'day-item--other': day.otherMonth, 35 'day-item--other': day.otherMonth,
41 'day-item--active': day.dateStr === selectDate, 36 '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> 37 + }" @click="onDayClick(day)">
  38 + <view class="day-text">{{ day.day }}</view>
  39 + <view v-if="day.dot" class="day-dot" :style="{ backgroundColor: day.dotColor }"></view>
  40 + </view>
47 </view> 41 </view>
48 </view> 42 </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> 43 + <!-- 打卡详情区域 -->
  44 + <view class="detail-card">
  45 + <view v-if="!currentAttendance">
  46 + <view class="empty-text">暂无考勤记录</view>
60 </view> 47 </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> 48 + <view v-else>
  49 + <view class="detail-row">
  50 + <view class="detail-title">上班打卡时间:</view>
  51 + <view class="detail-text">{{ currentAttendance.onDutyTime || '缺少打卡记录' }}</view>
  52 + </view>
  53 + <view class="detail-row">
  54 + <view class="detail-title">上班打卡地点:</view>
  55 + <view class="detail-text">{{ currentAttendance.onDutyAddress || '-' }}</view>
  56 + </view>
  57 + <view class="divider"></view>
  58 + <view class="detail-row">
  59 + <view class="detail-title">下班打卡时间:</view>
  60 + <view class="detail-text">{{ currentAttendance.offDutyTime || '缺少打卡记录' }}</view>
  61 + </view>
  62 + <view class="detail-row">
  63 + <view class="detail-title">下班打卡地点:</view>
  64 + <view class="detail-text">{{ currentAttendance.offDutyAddress || '-' }}</view>
  65 + </view>
  66 + <!-- 可选:展示异常原因 -->
  67 + <view v-if="currentAttendance.abnormalReason" class="detail-row">
  68 + <view class="detail-title">异常原因:</view>
  69 + <view class="detail-text" style="color:#f56c6c">{{ currentAttendance.abnormalReason }}</view>
  70 + </view>
73 </view> 71 </view>
74 </view> 72 </view>
75 - </view>  
76 -</view> 73 + </view>
77 </template> 74 </template>
78 -  
79 <script setup lang="ts"> 75 <script setup lang="ts">
80 -import { ref, onMounted, computed } from 'vue';  
81 -  
82 -const weekList = ['日','一','二','三','四','五','六']; 76 +import { ref, onMounted, computed, watch } from 'vue';
  77 +import { attendanceDaily, dayDetail } from '@/api/user'
  78 +const weekList = ['日', '一', '二', '三', '四', '五', '六'];
83 const today = new Date(); 79 const today = new Date();
84 const todayTs = today.getTime(); 80 const todayTs = today.getTime();
85 // 当前显示月份 81 // 当前显示月份
@@ -87,65 +83,50 @@ const currentYear = ref(today.getFullYear()); @@ -87,65 +83,50 @@ const currentYear = ref(today.getFullYear());
87 const currentMonth = ref(today.getMonth() + 1); 83 const currentMonth = ref(today.getMonth() + 1);
88 const selectDate = ref(''); 84 const selectDate = ref('');
89 const dayRows = ref<any[]>([]); 85 const dayRows = ref<any[]>([]);
90 -  
91 // 是否允许点击下一月:判断当前展示月份是否小于本月 86 // 是否允许点击下一月:判断当前展示月份是否小于本月
92 const canNext = computed(() => { 87 const canNext = computed(() => {
93 - const showMonthDate = new Date(currentYear.value, currentMonth.value -1, 1); 88 + const showMonthDate = new Date(currentYear.value, currentMonth.value - 1, 1);
94 const nowMonthDate = new Date(today.getFullYear(), today.getMonth(), 1); 89 const nowMonthDate = new Date(today.getFullYear(), today.getMonth(), 1);
95 return showMonthDate < nowMonthDate; 90 return showMonthDate < nowMonthDate;
96 }); 91 });
97 92
98 -// ========== 写死静态统计数据 ========== 93 +// 统计数据
99 const statData = ref({ 94 const statData = ref({
100 - checkInDays: 28,  
101 - normalDays: 25,  
102 - errorDays: 3 95 + checkInDays: 0,
  96 + normalDays: 0,
  97 + errorDays: 0
103 }); 98 });
  99 +// 当月考勤打点字典 {dateStr: {dot:string, abnormalReason:string}}
  100 +const monthDayMap = ref<Record<string, { dot: string | null, abnormalReason: string | null }>>({});
104 101
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 -}); 102 +// 选中日期详情
  103 +const currentAttendance = ref<any>(null);
113 104
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: '' 105 +// 拉取月度考勤汇总(stat + 当月日期打点)
  106 +const getMonthAttendance = async () => {
  107 + try {
  108 + const month = `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}`;
  109 + const res = await attendanceDaily({ month });
  110 + // 赋值顶部统计
  111 + statData.value.checkInDays = res.stat.clockDays;
  112 + statData.value.normalDays = res.stat.normalDays;
  113 + statData.value.errorDays = res.stat.abnormalDays;
  114 + // 构建日期映射字典
  115 + const map: Record<string, { dot: string | null, abnormalReason: string | null }> = {};
  116 + res.days.forEach((item: any) => {
  117 + map[item.date] = {
  118 + dot: item.dot,
  119 + abnormalReason: item.abnormalReason
  120 + };
  121 + })
  122 + monthDayMap.value = map;
  123 + // 重新渲染日历
  124 + renderMonth();
  125 + } catch (err) {
  126 + console.error('获取月度考勤失败', err);
  127 + uni.showToast({ title: '加载月度考勤失败', icon: 'none' });
146 } 128 }
147 -});  
148 -const currentAttendance = ref<any>(null); 129 +};
149 130
150 // 生成月份日期网格 131 // 生成月份日期网格
151 const renderMonth = () => { 132 const renderMonth = () => {
@@ -159,46 +140,49 @@ const renderMonth = () =&gt; { @@ -159,46 +140,49 @@ const renderMonth = () =&gt; {
159 const prevLastDay = new Date(year, month - 1, 0).getDate(); 140 const prevLastDay = new Date(year, month - 1, 0).getDate();
160 // 当月第一天星期 0周日~6周六 141 // 当月第一天星期 0周日~6周六
161 const firstWeek = firstDay.getDay(); 142 const firstWeek = firstDay.getDay();
162 -  
163 const rows: any[] = []; 143 const rows: any[] = [];
164 let row: any[] = []; 144 let row: any[] = [];
165 // 填充上月尾部 145 // 填充上月尾部
166 - for(let i = 0; i < firstWeek; i++){ 146 + for (let i = 0; i < firstWeek; i++) {
167 const dayNum = prevLastDay - firstWeek + i + 1; 147 const dayNum = prevLastDay - firstWeek + i + 1;
168 row.push({ 148 row.push({
169 day: dayNum, 149 day: dayNum,
170 otherMonth: true, 150 otherMonth: true,
171 - dateStr: `${year}-${String(month-1).padStart(2,'0')}-${String(dayNum).padStart(2,'0')}` 151 + dateStr: `${year}-${String(month - 1).padStart(2, '0')}-${String(dayNum).padStart(2, '0')}`
172 }) 152 })
173 } 153 }
174 // 填充当月日期 154 // 填充当月日期
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 = { 155 + for (let d = 1; d <= lastDay.getDate(); d++) {
  156 + const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
  157 + const mark = monthDayMap.value[dateStr];
  158 + const item: any = {
179 day: d, 159 day: d,
180 otherMonth: false, 160 otherMonth: false,
181 dateStr, 161 dateStr,
182 dot: false, 162 dot: false,
183 dotColor: '' 163 dotColor: ''
184 }; 164 };
185 - if(mark){ 165 + if (mark?.dot) {
186 item.dot = true; 166 item.dot = true;
187 - item.dotColor = mark.type === 'error' ? '#f56c6c' : '#cccccc'; 167 + if (mark.dot === 'abnormal') {
  168 + item.dotColor = '#f56c6c';
  169 + } else if (mark.dot === 'partial') {
  170 + item.dotColor = '#cccccc';
  171 + }
188 } 172 }
189 row.push(item); 173 row.push(item);
190 - if(row.length ===7){ 174 + if (row.length === 7) {
191 rows.push(row); 175 rows.push(row);
192 row = []; 176 row = [];
193 } 177 }
194 } 178 }
195 // 填充下月开头 179 // 填充下月开头
196 let nextDay = 1; 180 let nextDay = 1;
197 - while(row.length <7){ 181 + while (row.length < 7) {
198 row.push({ 182 row.push({
199 day: nextDay, 183 day: nextDay,
200 - otherMonth:true,  
201 - dateStr: `${year}-${String(month+1).padStart(2,'0')}-${String(nextDay).padStart(2,'0')}` 184 + otherMonth: true,
  185 + dateStr: `${year}-${String(month + 1).padStart(2, '0')}-${String(nextDay).padStart(2, '0')}`
202 }) 186 })
203 nextDay++; 187 nextDay++;
204 } 188 }
@@ -208,180 +192,209 @@ const renderMonth = () =&gt; { @@ -208,180 +192,209 @@ const renderMonth = () =&gt; {
208 192
209 // 上一月 193 // 上一月
210 const prevMonth = () => { 194 const prevMonth = () => {
211 - if(currentMonth.value === 1){ 195 + if (currentMonth.value === 1) {
212 currentYear.value--; 196 currentYear.value--;
213 currentMonth.value = 12; 197 currentMonth.value = 12;
214 - }else{ 198 + } else {
215 currentMonth.value--; 199 currentMonth.value--;
216 } 200 }
217 - renderMonth(); 201 + getMonthAttendance();
218 }; 202 };
219 // 下一月 203 // 下一月
220 const nextMonth = () => { 204 const nextMonth = () => {
221 let nextY = currentYear.value; 205 let nextY = currentYear.value;
222 let nextM = currentMonth.value + 1; 206 let nextM = currentMonth.value + 1;
223 - if(nextM > 12){ 207 + if (nextM > 12) {
224 nextY++; 208 nextY++;
225 - nextM =1; 209 + nextM = 1;
226 } 210 }
227 currentYear.value = nextY; 211 currentYear.value = nextY;
228 currentMonth.value = nextM; 212 currentMonth.value = nextM;
229 - renderMonth(); 213 + getMonthAttendance();
230 }; 214 };
231 215
232 -// 点击日期  
233 -const onDayClick = (day:any) => { 216 +// 点击日期:获取当日详情
  217 +const onDayClick = async (day: any) => {
234 // 禁止点击其他月份 218 // 禁止点击其他月份
235 - if(day.otherMonth) return; 219 + if (day.otherMonth) return;
236 const selectDay = new Date(day.dateStr); 220 const selectDay = new Date(day.dateStr);
237 - if(selectDay > today){ 221 + if (selectDay > today) {
238 currentAttendance.value = null; 222 currentAttendance.value = null;
  223 + selectDate.value = day.dateStr;
239 return; 224 return;
240 } 225 }
241 selectDate.value = day.dateStr; 226 selectDate.value = day.dateStr;
242 - const data = dayAttendanceMap.value[day.dateStr];  
243 - currentAttendance.value = data ?? null; 227 + try {
  228 + const res = await dayDetail({ date: day.dateStr });
  229 + // 取第一条记录,大多数场景单条记录
  230 + const record = res.records?.[0] ?? null;
  231 + if (record) {
  232 + currentAttendance.value = {
  233 + ...record,
  234 + abnormalReason: res.abnormalReason
  235 + };
  236 + } else {
  237 + currentAttendance.value = null;
  238 + }
  239 + } catch (err) {
  240 + console.error('获取当日考勤详情失败', err);
  241 + uni.showToast({ title: '加载当日考勤失败', icon: 'none' });
  242 + currentAttendance.value = null;
  243 + }
244 }; 244 };
245 245
246 -onMounted(() => {  
247 - const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`; 246 +// 页面初始化
  247 +onMounted(async () => {
  248 + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
248 selectDate.value = todayStr; 249 selectDate.value = todayStr;
249 - fetchDayAttendance(todayStr);  
250 - renderMonth(); 250 + await getMonthAttendance();
  251 + // 默认选中今天,加载当日详情
  252 + await onDayClick({ dateStr: todayStr, otherMonth: false });
251 }); 253 });
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> 254 </script>
263 -  
264 <style scoped lang="scss"> 255 <style scoped lang="scss">
265 -.attendance-page{ 256 +.attendance-page {
266 padding: 20rpx; 257 padding: 20rpx;
267 background-color: #f5f5f7; 258 background-color: #f5f5f7;
268 min-height: 100vh; 259 min-height: 100vh;
269 } 260 }
270 -.stat-card{ 261 +
  262 +.stat-card {
271 display: flex; 263 display: flex;
272 background-color: #3c9cff; 264 background-color: #3c9cff;
273 border-radius: 16rpx; 265 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; 266 + padding: 30rpx 0;
  267 + margin-bottom: 20rpx;
  268 +
  269 + .stat-item {
  270 + flex: 1;
  271 + text-align: center;
  272 + color: #fff;
  273 +
  274 + .stat-label {
  275 + font-size: 28rpx;
  276 + opacity: 0.9;
283 } 277 }
284 - .stat-value{  
285 - font-size:36rpx;  
286 - margin-top:10rpx;  
287 - font-weight:bold; 278 +
  279 + .stat-value {
  280 + font-size: 36rpx;
  281 + margin-top: 10rpx;
  282 + font-weight: bold;
288 } 283 }
289 } 284 }
290 } 285 }
291 -.calendar-wrap{  
292 - background:#fff;  
293 - border-radius:12rpx;  
294 - padding:20rpx;  
295 - margin-bottom:20rpx; 286 +
  287 +.calendar-wrap {
  288 + background: #fff;
  289 + border-radius: 12rpx;
  290 + padding: 20rpx;
  291 + margin-bottom: 20rpx;
296 } 292 }
297 -.calendar-header{  
298 - display:flex; 293 +
  294 +.calendar-header {
  295 + display: flex;
299 justify-content: space-between; 296 justify-content: space-between;
300 - align-items:center;  
301 - margin-bottom:20rpx; 297 + align-items: center;
  298 + margin-bottom: 20rpx;
302 } 299 }
303 -.calendar-title{  
304 - font-size:32rpx;  
305 - font-weight:bold; 300 +
  301 +.calendar-title {
  302 + font-size: 32rpx;
  303 + font-weight: bold;
306 } 304 }
307 -.calendar-btn{  
308 - padding:8rpx 16rpx;  
309 - border:1rpx solid #eee;  
310 - border-radius:8rpx;  
311 - font-size:26rpx; 305 +
  306 +.calendar-btn {
  307 + padding: 8rpx 16rpx;
  308 + border: 1rpx solid #eee;
  309 + border-radius: 8rpx;
  310 + font-size: 26rpx;
312 } 311 }
313 -.empty-btn{  
314 - border-color:transparent;  
315 - color:transparent; 312 +
  313 +.empty-btn {
  314 + border-color: transparent;
  315 + color: transparent;
316 } 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; 317 +
  318 +.week-row {
  319 + display: flex;
  320 +
  321 + .week-item {
  322 + flex: 1;
  323 + text-align: center;
  324 + font-size: 26rpx;
  325 + color: #666;
  326 + padding: 10rpx 0;
325 } 327 }
326 } 328 }
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; 329 +
  330 +.day-row {
  331 + display: flex;
  332 +
  333 + .day-item {
  334 + flex: 1;
  335 + min-height: 70rpx;
  336 + display: flex;
  337 + flex-direction: column;
  338 + align-items: center;
  339 + justify-content: center;
  340 + padding: 8rpx 0;
  341 + position: relative;
  342 +
  343 + .day-text {
  344 + font-size: 28rpx;
340 } 345 }
341 - .day-dot{  
342 - width:10rpx;  
343 - height:10rpx;  
344 - border-radius:50%;  
345 - margin-top:4rpx; 346 +
  347 + .day-dot {
  348 + position: absolute;
  349 + bottom: 8rpx;
  350 + width: 10rpx;
  351 + height: 10rpx;
  352 + border-radius: 50%;
346 } 353 }
347 } 354 }
348 - .day-item--other{  
349 - color:#ccc; 355 +
  356 + .day-item--other {
  357 + color: #ccc;
350 } 358 }
351 - .day-item--active{  
352 - background:#3c9cff;  
353 - color:#fff;  
354 - border-radius:50%; 359 +
  360 + .day-item--active {
  361 + color: #f00;
355 } 362 }
356 } 363 }
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; 364 +
  365 +.detail-card {
  366 + background: #fff;
  367 + border-radius: 12rpx;
  368 + padding: 30rpx;
  369 +
  370 + .empty-text {
  371 + text-align: center;
  372 + color: #999;
  373 + font-size: 30rpx;
  374 + padding: 40rpx 0;
366 } 375 }
367 - .detail-row{  
368 - margin-bottom:20rpx;  
369 - display:flex;  
370 - .detail-title{  
371 - font-size:30rpx;  
372 - color:#333;  
373 - width:220rpx; 376 +
  377 + .detail-row {
  378 + margin-bottom: 20rpx;
  379 + display: flex;
  380 +
  381 + .detail-title {
  382 + font-size: 30rpx;
  383 + color: #333;
  384 + width: 220rpx;
374 } 385 }
375 - .detail-text{  
376 - flex:1;  
377 - font-size:30rpx;  
378 - color:#666; 386 +
  387 + .detail-text {
  388 + flex: 1;
  389 + font-size: 30rpx;
  390 + color: #666;
379 } 391 }
380 } 392 }
381 - .divider{  
382 - height:1px;  
383 - background:#eee;  
384 - margin:30rpx 0; 393 +
  394 + .divider {
  395 + height: 1px;
  396 + background: #eee;
  397 + margin: 30rpx 0;
385 } 398 }
386 } 399 }
387 </style> 400 </style>
pages-sub/daily/patrol-manage/add-patrol-record.vue
1 <template> 1 <template>
2 <view class="u-page"> 2 <view class="u-page">
3 - <!-- 原有模板内容不变,仅调整表单校验相关配置 -->  
4 - <view class="inspect-form-content commonPageLRpadding">  
5 - <up-form  
6 - label-position="left"  
7 - :model="inspectForm"  
8 - ref="inspectFormRef"  
9 - labelWidth="200rpx"  
10 - >  
11 - <!-- 1. 巡查描述 -->  
12 - <up-form-item  
13 - label="巡查描述"  
14 - prop="content"  
15 - class="form-item"  
16 - required  
17 - >  
18 - <up-textarea  
19 - v-model.trim="inspectForm.content"  
20 - placeholder="请输入巡查描述"  
21 - maxlength="200"  
22 - count  
23 - ></up-textarea>  
24 - </up-form-item>  
25 -  
26 - <!-- 2. 上传图片 -->  
27 - <up-form-item  
28 - label="上传图片"  
29 - prop="images"  
30 - required  
31 - border-bottom  
32 - class="form-item"  
33 - >  
34 - <up-upload  
35 - :file-list="imagesList"  
36 - @after-read="(event) => uploadImgs(event)"  
37 - @delete="(event) => deleteImg(event)"  
38 - @on-exceed="handleExceed"  
39 - multiple  
40 - :max-count="3"  
41 - upload-text="选择图片"  
42 - del-color="#ff4d4f"  
43 - class="upload-wrap"  
44 - width="70"  
45 - height="70"  
46 - ></up-upload>  
47 - </up-form-item>  
48 -  
49 - <!-- 3. 转为工单 -->  
50 - <up-form-item  
51 - label="转为工单"  
52 - prop="isWorkOrder"  
53 - required  
54 - class="form-item"  
55 - >  
56 - <up-radio-group  
57 - v-model="inspectForm.isWorkOrder"  
58 - active-color="#1989fa"  
59 - direction="row"  
60 - class="radio-group"  
61 - @change="handleWorkOrderChange"  
62 - >  
63 - <up-radio  
64 - :custom-style="{marginRight: '40rpx'}"  
65 - v-for="(item, index) in radioList"  
66 - :key="index"  
67 - :label="item.label"  
68 - :name="item.value"  
69 - > 3 +<!-- 原有模板内容不变,仅调整表单校验相关配置 -->
  4 +<view class="inspect-form-content commonPageLRpadding">
  5 +<up-form label-position="left" :model="inspectForm" ref="inspectFormRef" labelWidth="200rpx">
  6 +<!-- 1. 巡查描述 -->
  7 +<up-form-item label="巡查描述" prop="content" class="form-item" required>
  8 +<up-textarea v-model.trim="inspectForm.content" placeholder="请输入巡查描述" maxlength="200" count></up-textarea>
  9 +</up-form-item>
  10 +<!-- 2. 上传图片 -->
  11 +<up-form-item label="上传图片" prop="images" required border-bottom class="form-item">
  12 +<up-upload :file-list="imagesList" @after-read="(event) => uploadImgs(event)"
  13 + @delete="(event) => deleteImg(event)" @on-exceed="handleExceed" multiple :max-count="3" upload-text="选择图片"
  14 +del-color="#ff4d4f" class="upload-wrap" width="70" height="70"></up-upload>
  15 +</up-form-item>
  16 +<!-- 3. 转为工单 -->
  17 +<up-form-item label="转为工单" prop="isWorkOrder" required class="form-item">
  18 +<up-radio-group v-model="inspectForm.isWorkOrder" active-color="#1989fa" direction="row" class="radio-group"
  19 + @change="handleWorkOrderChange">
  20 +<up-radio :custom-style="{ marginRight: '40rpx' }" v-for="(item, index) in radioList" :key="index"
  21 + :label="item.label" :name="item.value">
70 {{ item.label }} 22 {{ item.label }}
71 - </up-radio>  
72 - </up-radio-group>  
73 - </up-form-item>  
74 -  
75 - <!-- 紧急程度(仍为必填) -->  
76 -<!-- v-if="inspectForm.isWorkOrder === '2'"-->  
77 - <up-form-item  
78 - v-if="inspectForm.isWorkOrder === '2'"  
79 - label="紧急程度"  
80 - prop="pressingTypeName"  
81 - border-bottom  
82 - required  
83 - class="form-item"  
84 - @click="handleActionSheetOpen('pressingType')"  
85 - >  
86 - <up-input  
87 - v-model="inspectForm.pressingTypeName"  
88 - disabled  
89 - disabled-color="#ffffff"  
90 - placeholder="请选择紧急程度"  
91 - border="none"  
92 -  
93 - ></up-input>  
94 - <template #right>  
95 - <up-icon name="arrow-right" size="16"></up-icon>  
96 - </template>  
97 - </up-form-item>  
98 -  
99 - <!-- 希望完成时间(移除 required 属性,改为非必填) -->  
100 -<!-- v-if="inspectForm.isWorkOrder === '2'"-->  
101 - <up-form-item  
102 - v-if="inspectForm.isWorkOrder === '2'"  
103 - label="希望完成时间"  
104 - prop="expectedFinishDate"  
105 - class="form-item"  
106 - @click="show = true; uni.hideKeyboard()"  
107 - >  
108 - <up-input  
109 - v-model="inspectForm.expectedFinishDate"  
110 - border="none"  
111 - readonly  
112 - placeholder="点击选择时间"  
113 - >  
114 -  
115 -  
116 - </up-input>  
117 - <template #right>  
118 - <!-- 核心修复:用view包裹图标,添加双重@click.stop -->  
119 - <view v-if="inspectForm.expectedFinishDate" @click.stop>  
120 - <up-icon  
121 - name="close"  
122 - size="16"  
123 - @click.stop="clearExpectedFinishDate"  
124 - ></up-icon>  
125 - </view>  
126 - <up-icon name="arrow-right" size="16" v-else></up-icon>  
127 - </template>  
128 - </up-form-item>  
129 -  
130 - </up-form>  
131 - </view>  
132 -  
133 - <!-- 底部提交按钮 -->  
134 - <view class="fixed-bottom-btn-wrap">  
135 - <up-button  
136 - type="primary"  
137 - text="提交"  
138 - @click="submitInspect"  
139 - :style="{ width: '100%', height: '88rpx', fontSize: '32rpx', borderRadius: 0 }"  
140 - ></up-button>  
141 - </view>  
142 -  
143 - <!-- 紧急程度下拉弹窗 -->  
144 - <up-action-sheet  
145 - :show="showActionSheet"  
146 - :actions="pressingTypeList"  
147 - title="请选择紧急程度"  
148 - @close="handleActionSheetClose"  
149 - @select="handlePressingTypeSelect"  
150 - ></up-action-sheet>  
151 -  
152 - <!-- 完成时间选择器 -->  
153 - <up-datetime-picker  
154 - :show="show"  
155 - v-model="datetimeValue"  
156 - mode="datetime"  
157 - :min-date="new Date()"  
158 - @cancel="show = false"  
159 - @confirm="expectedFinishDateConfirm"  
160 - ></up-datetime-picker>  
161 - </view> 23 +</up-radio>
  24 +</up-radio-group>
  25 +</up-form-item>
  26 +<!-- ========== 【新增】业务线(单选框,对齐参考页面) ========== -->
  27 +<up-form-item v-if="inspectForm.isWorkOrder === '2'" label="业务线" prop="busiLineCn" border-bottom required
  28 +class="form-item">
  29 +<up-radio-group v-model="inspectForm.busiLineCn" placement="row" @change="handleBusiLineChange">
  30 +<up-radio v-for="item in busiLineOptions" :key="item.name" :label="item.name" :name="item.name"></up-radio>
  31 +</up-radio-group>
  32 +</up-form-item>
  33 +<!-- ========== 【新增】问题类型(级联选择器,对齐参考页面) ========== -->
  34 +<up-form-item v-if="inspectForm.isWorkOrder === '2'" label="问题类型" prop="orderName" border-bottom required
  35 +class="form-item" @click="openCascader(); uni.hideKeyboard()">
  36 +<up-input v-model="inspectForm.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型"
  37 +border="none"></up-input>
  38 +<template #right>
  39 +<up-icon name="arrow-right" size="16"></up-icon>
  40 +</template>
  41 +</up-form-item>
  42 +<!-- 紧急程度(仍为必填) -->
  43 +<up-form-item v-if="inspectForm.isWorkOrder === '2'" label="紧急程度" prop="pressingTypeName" border-bottom required
  44 +class="form-item" @click="handleActionSheetOpen('pressingType')">
  45 +<up-input v-model="inspectForm.pressingTypeName" disabled disabled-color="#ffffff" placeholder="请选择紧急程度"
  46 +border="none"></up-input>
  47 +<template #right>
  48 +<up-icon name="arrow-right" size="16"></up-icon>
  49 +</template>
  50 +</up-form-item>
  51 +<!-- 希望完成时间(移除 required 属性,改为非必填) -->
  52 +<up-form-item v-if="inspectForm.isWorkOrder === '2'" label="希望完成时间" prop="expectedFinishDate" class="form-item"
  53 + @click="show = true; uni.hideKeyboard()">
  54 +<up-input v-model="inspectForm.expectedFinishDate" border="none" readonly placeholder="点击选择时间">
  55 +</up-input>
  56 +<template #right>
  57 +<!-- 核心修复:用view包裹图标,添加双重@click.stop -->
  58 +<view v-if="inspectForm.expectedFinishDate" @click.stop>
  59 +<up-icon name="close" size="16" @click.stop="clearExpectedFinishDate"></up-icon>
  60 +</view>
  61 +<up-icon name="arrow-right" size="16" v-else></up-icon>
  62 +</template>
  63 +</up-form-item>
  64 +</up-form>
  65 +</view>
  66 +<!-- 底部提交按钮 -->
  67 +<view class="fixed-bottom-btn-wrap">
  68 +<up-button type="primary" text="提交" @click="submitInspect"
  69 + :style="{ width: '100%', height: '88rpx', fontSize: '32rpx', borderRadius: 0 }"></up-button>
  70 +</view>
  71 +<!-- 紧急程度下拉弹窗 -->
  72 +<up-action-sheet :show="showActionSheet" :actions="pressingTypeList" title="请选择紧急程度" @close="handleActionSheetClose"
  73 + @select="handlePressingTypeSelect"></up-action-sheet>
  74 +<!-- ========== 新增级联选择器组件 ========== -->
  75 +<up-cascader v-model:show="showCascader" v-model="cascaderValue" :data="cascaderOptions" title="选择问题类型"
  76 + :auto-close="false" @confirm="cascaderConfirm"></up-cascader>
  77 +<!-- 完成时间选择器 -->
  78 +<up-datetime-picker :show="show" v-model="datetimeValue" mode="datetime" :min-date="new Date()"
  79 + @cancel="show = false" @confirm="expectedFinishDateConfirm"></up-datetime-picker>
  80 +</view>
162 </template> 81 </template>
163 -  
164 <script setup lang="ts"> 82 <script setup lang="ts">
165 -import { ref } from 'vue' 83 +import { ref, getCurrentInstance } from 'vue'
166 import type { UniFormRef } from '@/uni_modules/uview-plus/types' 84 import type { UniFormRef } from '@/uni_modules/uview-plus/types'
  85 +import { useUserStore } from '@/pinia/user';
  86 +import { getDictLabel, getDictValue } from '@/common/utils/dict';
  87 +import { getProblemListByType } from '@/api/common'
  88 +const { proxy } = getCurrentInstance()!
167 const inspectFormRef = ref<UniFormRef>(null) 89 const inspectFormRef = ref<UniFormRef>(null)
168 -</script> 90 +const userStore = useUserStore();
  91 +const BUSI_LINE_DICT_TYPE = 'business_line';
  92 +const CJK_RE = /[一-龥]/;
  93 +
  94 +// 级联选择器变量
  95 +const showCascader = ref(false);
  96 +const cascaderValue = ref([]);
  97 +const cascaderOptions = ref([]);
  98 +
  99 +// 打开级联弹窗
  100 +const openCascader = () => {
  101 +const busiLineEn = getBusiLineEnByCn(proxy.$data.inspectForm.busiLineCn)
  102 +if (!busiLineEn) {
  103 +uni.showToast({ title: '请先选择业务线', icon: 'none' })
  104 +return
  105 + }
  106 +if (!cascaderOptions.value || cascaderOptions.value.length === 0) {
  107 +uni.showToast({ title: '正在加载问题类型,请稍等', icon: 'none' });
  108 +loadProblemCascader(busiLineEn);
  109 +return;
  110 + }
  111 +cascaderValue.value = [];
  112 +showCascader.value = true;
  113 +};
  114 +
  115 +// 级联确认回调
  116 +const cascaderConfirm = (selectedArr) => {
  117 +const getLabelByValueList = (list, valList) => {
  118 +const labelArr = [];
  119 +let currentList = list;
  120 +for (const val of valList) {
  121 +const target = currentList.find(item => item.value === val);
  122 +if (!target) break;
  123 +labelArr.push(target.label);
  124 +currentList = target.children || [];
  125 + }
  126 +return labelArr.join('/');
  127 + };
  128 +const fullLabel = getLabelByValueList(cascaderOptions.value, selectedArr);
  129 +proxy.$data.inspectForm.orderName = fullLabel;
  130 +proxy.$data.inspectForm.orderCode = selectedArr.at(-1);
  131 +cascaderValue.value = selectedArr;
  132 +inspectFormRef.value?.validateField('orderName');
  133 +};
  134 +
  135 +// 加载级联树形数据
  136 +const loadProblemCascader = async (parentCode) => {
  137 +if (!parentCode) {
  138 +cascaderOptions.value = []
  139 +return
  140 + }
  141 +try {
  142 +uni.showLoading({ title: '加载类型中...' })
  143 +const res = await getProblemListByType({ parentCode })
  144 +uni.hideLoading()
  145 +cascaderOptions.value = res || []
  146 + } catch (err) {
  147 +uni.hideLoading()
  148 +console.error('获取问题类型失败', err)
  149 +uni.showToast({ title: '获取问题类型失败', icon: 'none' })
  150 +cascaderOptions.value = []
  151 + }
  152 +}
169 153
  154 +const getBusiLineCnByEn = (busiLineEn) => {
  155 +if (!busiLineEn) return '';
  156 +return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn;
  157 +};
  158 +const getBusiLineEnByCn = (cnName) => {
  159 +if (!cnName) return '';
  160 +const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName);
  161 +if (code) return code;
  162 +return CJK_RE.test(cnName) ? '' : cnName;
  163 +};
  164 +
  165 +// 暴露给选项式访问
  166 +proxy.$setupCtx = {
  167 + loadProblemCascader,
  168 + getBusiLineEnByCn,
  169 + cascaderOptions,
  170 + cascaderValue
  171 +}
  172 +</script>
170 <script lang="ts"> 173 <script lang="ts">
171 import { uploadImages } from '@/common/utils/upload'; 174 import { uploadImages } from '@/common/utils/upload';
172 -import { inspectionCreate } from "@/api/patrol-manage/patrol-plan"; 175 +import { inspectionCreate } from "@/api/patrol-manage/patrol-plan";
173 import { timeFormat } from '@/uni_modules/uview-plus' 176 import { timeFormat } from '@/uni_modules/uview-plus'
174 - 177 +import { useUserStore } from '@/pinia/user';
  178 +import { getDictLabel } from '@/common/utils/dict';
175 export default { 179 export default {
176 - data() {  
177 - return {  
178 - imagesList: [],  
179 - radioList: [ 180 +data() {
  181 +return {
  182 +imagesList: [],
  183 +radioList: [
180 { label: '否', value: '1' }, 184 { label: '否', value: '1' },
181 { label: '是', value: '2' } 185 { label: '是', value: '2' }
182 ], 186 ],
183 - pressingTypeList: [],  
184 - inspectForm: {  
185 - content: '',  
186 - isWorkOrder: '1',  
187 - pressingTypeName: '',  
188 - pressingType: '',  
189 - expectedFinishDate: '' // 非必填,可空 187 +pressingTypeList: [],
  188 +busiLineOptions: [], // 业务线移到data,不再放在setup
  189 +inspectForm: {
  190 +content: '',
  191 +isWorkOrder: '1',
  192 +pressingTypeName: '',
  193 +pressingType: '',
  194 +busiLineCn: '',
  195 +orderName: '',
  196 +orderCode: '',
  197 +expectedFinishDate: '' // 非必填,可空
190 }, 198 },
191 - paramsOptins: {},  
192 - showActionSheet: false,  
193 - show: false,  
194 - datetimeValue: Date.now(),  
195 - inspectFormRules: {  
196 - images: [ 199 +paramsOptins: {},
  200 +showActionSheet: false,
  201 +show: false,
  202 +datetimeValue: Date.now(),
  203 +inspectFormRules: {
  204 +images: [
197 { 205 {
198 - required: true,  
199 - message: '请上传图片',  
200 - trigger: 'change',  
201 - validator: (rule, value, callback) => {  
202 - const hasSuccessImg = this.imagesList.some(item => item.status === 'success')  
203 - const imgCount = this.imagesList.filter(item => item.status === 'success').length  
204 -  
205 - if (!hasSuccessImg || imgCount < 1) {  
206 - callback(new Error('最少需要上传1张图片')) 206 +required: true,
  207 +message: '请上传图片',
  208 +trigger: 'change',
  209 +validator: (rule, value, callback) => {
  210 +const hasSuccessImg = this.imagesList.some(item => item.status === 'success')
  211 +const imgCount = this.imagesList.filter(item => item.status === 'success').length
  212 +if (!hasSuccessImg || imgCount < 1) {
  213 +callback(new Error('最少需要上传1张图片'))
207 } else if (imgCount > 3) { 214 } else if (imgCount > 3) {
208 - callback(new Error('最多只能上传3张图片')) 215 +callback(new Error('最多只能上传3张图片'))
209 } else { 216 } else {
210 - callback() 217 +callback()
211 } 218 }
212 } 219 }
213 } 220 }
214 ], 221 ],
215 - content: [  
216 - { type: 'string', required: true, message: '请输入工单描述', trigger: ['change'] } 222 +content: [
  223 + { type: 'string', required: true, message: '请输入巡查描述', trigger: ['change'] }
217 ], 224 ],
218 - isWorkOrder: [ 225 +isWorkOrder: [
219 { type: 'string', required: true, message: '请选择是否转为工单', trigger: ['change'] } 226 { type: 'string', required: true, message: '请选择是否转为工单', trigger: ['change'] }
220 ], 227 ],
221 - pressingTypeName: [ 228 +pressingTypeName: [
  229 + {
  230 +required: true,
  231 +message: '请选择紧急程度',
  232 +trigger: 'change',
  233 +validator: (rule, value, callback) => {
  234 +if (this.inspectForm.isWorkOrder === '2' && !value) {
  235 +callback(new Error('请选择紧急程度'))
  236 + } else {
  237 +callback()
  238 + }
  239 + }
  240 + }
  241 + ],
  242 +busiLineCn: [
  243 + {
  244 +required: true,
  245 +message: '请选择业务线',
  246 +trigger: 'change',
  247 +validator: (rule, value, callback) => {
  248 +if (this.inspectForm.isWorkOrder === '2' && !value) {
  249 +callback(new Error('请选择业务线'))
  250 + } else {
  251 +callback()
  252 + }
  253 + }
  254 + }
  255 + ],
  256 +orderName: [
222 { 257 {
223 - required: true,  
224 - message: '请选择紧急程度',  
225 - trigger: 'change',  
226 - validator: (rule, value, callback) => {  
227 - if (this.inspectForm.isWorkOrder === '2' && !value) {  
228 - callback(new Error('请选择紧急程度')) 258 +required: true,
  259 +message: '请选择问题类型',
  260 +trigger: 'change',
  261 +validator: (rule, value, callback) => {
  262 +if (this.inspectForm.isWorkOrder === '2' && !value) {
  263 +callback(new Error('请选择问题类型'))
229 } else { 264 } else {
230 - callback() 265 +callback()
231 } 266 }
232 } 267 }
233 } 268 }
234 ], 269 ],
235 - // 关键修改:希望完成时间 移除必填校验,或改为非必填  
236 - expectedFinishDate: [ 270 +expectedFinishDate: [
237 { 271 {
238 - // 移除 required: true,改为非必填  
239 - message: '请选择有效的希望完成时间', // 仅当有值时,校验格式(可选)  
240 - trigger: 'change',  
241 - validator: (rule, value, callback) => {  
242 - // 只有当字段有值时,才校验时间格式是否有效  
243 - if (value && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(value)) {  
244 - callback(new Error('请选择有效的希望完成时间')) 272 +message: '请选择有效的希望完成时间',
  273 +trigger: 'change',
  274 +validator: (rule, value, callback) => {
  275 +if (value && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(value)) {
  276 +callback(new Error('请选择有效的希望完成时间'))
245 } else { 277 } else {
246 - callback() // 无值时直接通过校验 278 +callback()
247 } 279 }
248 } 280 }
249 } 281 }
@@ -251,185 +283,183 @@ export default { @@ -251,185 +283,183 @@ export default {
251 } 283 }
252 } 284 }
253 }, 285 },
254 - onLoad(option){  
255 - this.paramsOptins = option  
256 - this.getPressingTypeDict() 286 +onLoad(option) {
  287 +this.paramsOptins = option
  288 +// 初始化业务线选项
  289 +const userStore = useUserStore();
  290 +if (userStore.userInfo?.user?.busiLine) {
  291 +const rawBusiLines = userStore.userInfo.user.busiLine.split(',');
  292 +this.busiLineOptions = rawBusiLines.map(item => ({
  293 +name: getDictLabel('business_line', item.trim()) || item.trim()
  294 +}));
  295 +}
257 }, 296 },
258 - onReady() {  
259 - this.$refs.inspectFormRef.setRules(this.inspectFormRules) 297 +onReady() {
  298 +this.$refs.inspectFormRef.setRules(this.inspectFormRules)
260 }, 299 },
261 - methods: {  
262 - getPressingTypeDict() {  
263 - const dictList = uni.$dict.getDictSimpleList('workorder_pressing_type')  
264 - this.pressingTypeList = uni.$dict.transformLabelValueToNameValue(dictList)  
265 - console.log(this.pressingTypeList)  
266 - },  
267 -  
268 - handleWorkOrderChange() {  
269 - if (this.inspectForm.isWorkOrder === '1') {  
270 - this.inspectForm.pressingTypeName = ''  
271 - this.inspectForm.pressingType = ''  
272 - this.inspectForm.expectedFinishDate = '' // 清空非必填字段  
273 - this.$refs.inspectFormRef.validateField(['pressingTypeName', 'expectedFinishDate']) 300 +onShow() {
  301 +this.pressingTypeList = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('workorder_pressing_type'))
  302 + },
  303 +methods: {
  304 +handleWorkOrderChange() {
  305 +if (this.inspectForm.isWorkOrder === '1') {
  306 +this.inspectForm.pressingTypeName = ''
  307 +this.inspectForm.pressingType = ''
  308 +this.inspectForm.busiLineCn = ''
  309 +this.inspectForm.orderName = ''
  310 +this.inspectForm.orderCode = ''
  311 +this.$setupCtx.cascaderOptions.value = []
  312 +this.$setupCtx.cascaderValue.value = []
  313 +this.inspectForm.expectedFinishDate = ''
  314 +this.$refs.inspectFormRef.validateField(['pressingTypeName', 'busiLineCn', 'orderName', 'expectedFinishDate'])
  315 + } else {
  316 +// 修复点:直接读取data中的 busiLineOptions,不再访问setup
  317 +const busiOpts = this.busiLineOptions
  318 +if (busiOpts.length > 0) {
  319 +this.inspectForm.busiLineCn = busiOpts[0].name
  320 +this.handleBusiLineChange()
  321 + }
274 } 322 }
275 }, 323 },
276 -  
277 -  
278 - handleActionSheetOpen(type) {  
279 - uni.hideKeyboard()  
280 - console.log('showActionSheet')  
281 - if (type === 'pressingType') {  
282 - console.log('showActionSheet')  
283 - this.showActionSheet = true 324 +handleBusiLineChange() {
  325 +this.inspectForm.orderName = ''
  326 +this.inspectForm.orderCode = ''
  327 +this.$setupCtx.cascaderValue.value = []
  328 +this.$setupCtx.cascaderOptions.value = []
  329 +const busiLineEn = this.$setupCtx.getBusiLineEnByCn(this.inspectForm.busiLineCn)
  330 +this.$setupCtx.loadProblemCascader(busiLineEn)
  331 +this.$refs.inspectFormRef.validateField('busiLineCn')
  332 + },
  333 +handleActionSheetOpen(type) {
  334 +uni.hideKeyboard()
  335 +if (type === 'pressingType') {
  336 +this.showActionSheet = true
284 } 337 }
285 - uni.hideKeyboard()  
286 }, 338 },
287 -  
288 - handleActionSheetClose() {  
289 - this.showActionSheet = false 339 +handleActionSheetClose() {
  340 +this.showActionSheet = false
290 }, 341 },
291 -  
292 - handlePressingTypeSelect(e) {  
293 - console.log(e)  
294 - this.inspectForm.pressingTypeName = e.name  
295 - this.inspectForm.pressingType = e.value  
296 - this.showActionSheet = false  
297 - this.$refs.inspectFormRef.validateField('pressingTypeName') 342 +handlePressingTypeSelect(e) {
  343 +this.inspectForm.pressingTypeName = e.name
  344 +this.inspectForm.pressingType = e.value
  345 +this.showActionSheet = false
  346 +this.$refs.inspectFormRef.validateField('pressingTypeName')
298 }, 347 },
299 -  
300 - expectedFinishDateConfirm(e) {  
301 - this.inspectForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss')  
302 - this.show = false  
303 - this.$refs.inspectFormRef.validateField('expectedFinishDate') 348 +expectedFinishDateConfirm(e) {
  349 +this.inspectForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss')
  350 +this.show = false
  351 +this.$refs.inspectFormRef.validateField('expectedFinishDate')
304 }, 352 },
305 - // 新增:清除希望完成时间的方法  
306 - clearExpectedFinishDate() {  
307 - this.inspectForm.expectedFinishDate = ''  
308 - // 清除后触发一次字段校验,确保校验状态更新  
309 - this.$refs.inspectFormRef.validateField('expectedFinishDate') 353 +clearExpectedFinishDate() {
  354 +this.inspectForm.expectedFinishDate = ''
  355 +this.$refs.inspectFormRef.validateField('expectedFinishDate')
310 }, 356 },
311 -  
312 - deleteImg(event) {  
313 - this.imagesList.splice(event.index, 1)  
314 - this.$refs.inspectFormRef.validateField('images')  
315 - uni.showToast({ title: '图片删除成功', icon: 'success' }) 357 +deleteImg(event) {
  358 +this.imagesList.splice(event.index, 1)
  359 +this.$refs.inspectFormRef.validateField('images')
  360 +uni.showToast({ title: '图片删除成功', icon: 'success' })
316 }, 361 },
317 -  
318 - async uploadImgs(event) {  
319 - const fileList = Array.isArray(event.file) ? event.file : [event.file]  
320 - const targetImgList = this.imagesList  
321 -  
322 - const filePaths = fileList.map(item => item.url)  
323 - const tempItems = fileList.map(item => ({  
324 - ...item,  
325 - status: 'uploading',  
326 - message: '上传中' 362 +async uploadImgs(event) {
  363 +const fileList = Array.isArray(event.file) ? event.file : [event.file]
  364 +const targetImgList = this.imagesList
  365 +const filePaths = fileList.map(item => item.url)
  366 +const tempItems = fileList.map(item => ({
  367 +...item,
  368 +status: 'uploading',
  369 +message: '上传中'
327 })) 370 }))
328 - const startIndex = targetImgList.length  
329 - targetImgList.push(...tempItems)  
330 -  
331 - try {  
332 - const uploadResultUrls = await uploadImages({  
333 - filePaths: filePaths,  
334 - ignoreError: true 371 +const startIndex = targetImgList.length
  372 +targetImgList.push(...tempItems)
  373 +try {
  374 +const uploadResultUrls = await uploadImages({
  375 +filePaths: filePaths,
  376 +ignoreError: true
335 }) 377 })
336 -  
337 - uploadResultUrls.forEach((url, index) => {  
338 - if (targetImgList[startIndex + index]) {  
339 - targetImgList.splice(startIndex + index, 1, {  
340 - ...fileList[index],  
341 - status: 'success',  
342 - message: '',  
343 - url: url 378 +uploadResultUrls.forEach((url, index) => {
  379 +if (targetImgList[startIndex + index]) {
  380 +targetImgList.splice(startIndex + index, 1, {
  381 +...fileList[index],
  382 +status: 'success',
  383 +message: '',
  384 +url: url
344 }) 385 })
345 } 386 }
346 }) 387 })
347 -  
348 - if (uploadResultUrls.length < fileList.length) {  
349 - const failCount = fileList.length - uploadResultUrls.length  
350 - uni.showToast({ title: `成功上传${uploadResultUrls.length}张,失败${failCount}张`, icon: 'none' }) 388 +if (uploadResultUrls.length < fileList.length) {
  389 +const failCount = fileList.length - uploadResultUrls.length
  390 +uni.showToast({ title: `成功上传${uploadResultUrls.length}张,失败${failCount}张`, icon: 'none' })
351 } else { 391 } else {
352 - uni.showToast({ title: `成功上传${fileList.length}张图片`, icon: 'success' }) 392 +uni.showToast({ title: `成功上传${fileList.length}张图片`, icon: 'success' })
353 } 393 }
354 -  
355 - this.$refs.inspectFormRef.validateField('images') 394 +this.$refs.inspectFormRef.validateField('images')
356 } catch (err) { 395 } catch (err) {
357 - console.error('图片上传失败:', err)  
358 - for (let i = 0; i < fileList.length; i++) {  
359 - if (targetImgList[startIndex + i]) {  
360 - targetImgList.splice(startIndex + i, 1, {  
361 - ...fileList[i],  
362 - status: 'failed',  
363 - message: '上传失败' 396 +console.error('图片上传失败:', err)
  397 +for (let i = 0; i < fileList.length; i++) {
  398 +if (targetImgList[startIndex + i]) {
  399 +targetImgList.splice(startIndex + i, 1, {
  400 +...fileList[i],
  401 +status: 'failed',
  402 +message: '上传失败'
364 }) 403 })
365 } 404 }
366 } 405 }
367 - uni.showToast({ title: '图片上传失败,请重试', icon: 'none' })  
368 - this.$refs.inspectFormRef.validateField('images') 406 +uni.showToast({ title: '图片上传失败,请重试', icon: 'none' })
  407 +this.$refs.inspectFormRef.validateField('images')
369 } 408 }
370 }, 409 },
371 -  
372 - handleExceed() {  
373 - uni.showToast({ title: '最多只能上传3张图片', icon: 'none' }) 410 +handleExceed() {
  411 +uni.showToast({ title: '最多只能上传3张图片', icon: 'none' })
374 }, 412 },
375 -  
376 - getImgUrlList(imgList) {  
377 - return imgList.filter(item => item.status === 'success').map(item => item.url) 413 +getImgUrlList(imgList) {
  414 +return imgList.filter(item => item.status === 'success').map(item => item.url)
378 }, 415 },
379 -  
380 - async submitInspect() {  
381 - console.log('当前选择是否转为工单:', this.inspectForm.isWorkOrder)  
382 - try {  
383 - await this.$refs.inspectFormRef.validate()  
384 - console.log(this.imagesList)  
385 -  
386 - // 构造基础提交数据  
387 - const baseSubmitData = {  
388 - "batchNo": this.paramsOptins.batchNo,  
389 - "planNo": this.paramsOptins.planNo,  
390 - "imgHost": "1",  
391 - "imgList": this.getImgUrlList(this.imagesList),  
392 - "inspectionState": this.inspectForm.isWorkOrder,  
393 - "transState": this.inspectForm.isWorkOrder === '1' ? '1' : '2',  
394 - "transWorkNo": "default'",  
395 - "remark": this.inspectForm.content.trim() 416 +async submitInspect() {
  417 +console.log('当前选择是否转为工单:', this.inspectForm.isWorkOrder)
  418 +try {
  419 +await this.$refs.inspectFormRef.validate()
  420 +console.log(this.imagesList)
  421 +const baseSubmitData = {
  422 +"batchNo": this.paramsOptins.batchNo,
  423 +"planNo": this.paramsOptins.planNo,
  424 +"imgHost": "1",
  425 +"imgList": this.getImgUrlList(this.imagesList),
  426 +"inspectionState": this.inspectForm.isWorkOrder,
  427 +"transState": this.inspectForm.isWorkOrder === '1' ? '1' : '2',
  428 +"transWorkNo": "default'",
  429 +"remark": this.inspectForm.content.trim()
396 } 430 }
397 -  
398 - // 构造最终提交数据  
399 - let submitData = { ...baseSubmitData }  
400 - if (this.inspectForm.isWorkOrder === '2') {  
401 - // 紧急程度仍为必填,必传  
402 - submitData.pressingType = this.inspectForm.pressingType  
403 - // 关键修改:仅当希望完成时间有值时,才添加该参数  
404 - if (this.inspectForm.expectedFinishDate) {  
405 - submitData.expectedFinishDate = new Date(this.inspectForm.expectedFinishDate).getTime() 431 +let submitData = { ...baseSubmitData }
  432 +if (this.inspectForm.isWorkOrder === '2') {
  433 +submitData.pressingType = this.inspectForm.pressingType
  434 +const busiLineEn = this.$setupCtx.getBusiLineEnByCn(this.inspectForm.busiLineCn)
  435 +submitData.busiLine = busiLineEn
  436 +submitData.orderName = this.inspectForm.orderName
  437 +submitData.orderCode = this.inspectForm.orderCode
  438 +if (this.inspectForm.expectedFinishDate) {
  439 +submitData.expectedFinishDate = new Date(this.inspectForm.expectedFinishDate).getTime()
406 } 440 }
407 } 441 }
408 -  
409 - uni.showLoading({ title: '提交中...' })  
410 - await inspectionCreate(submitData)  
411 -  
412 - uni.hideLoading()  
413 - uni.showToast({  
414 - title: '提交成功',  
415 - icon: 'success',  
416 - duration: 1000 442 +uni.showLoading({ title: '提交中...' })
  443 +await inspectionCreate(submitData)
  444 +uni.hideLoading()
  445 +uni.showToast({
  446 +title: '提交成功',
  447 +icon: 'success',
  448 +duration: 1000
417 }) 449 })
418 -  
419 - setTimeout(() => {  
420 - uni.reLaunch({  
421 - url: '/pages-sub/daily/patrol-manage/index' 450 +setTimeout(() => {
  451 +uni.reLaunch({
  452 +url: '/pages-sub/daily/patrol-manage/index'
422 }) 453 })
423 }, 1000) 454 }, 1000)
424 -  
425 } catch (error) { 455 } catch (error) {
426 - uni.hideLoading()  
427 - if (!Array.isArray(error)) {  
428 - console.error('巡查表单提交失败:', error)  
429 - uni.showToast({  
430 - title: '提交失败,请重试',  
431 - icon: 'none',  
432 - duration: 2000 456 +uni.hideLoading()
  457 +if (!Array.isArray(error)) {
  458 +console.error('巡查表单提交失败:', error)
  459 +uni.showToast({
  460 +title: '提交失败,请重试',
  461 +icon: 'none',
  462 +duration: 2000
433 }) 463 })
434 } 464 }
435 } 465 }
@@ -437,20 +467,15 @@ export default { @@ -437,20 +467,15 @@ export default {
437 } 467 }
438 } 468 }
439 </script> 469 </script>
440 -  
441 <style lang="scss" scoped> 470 <style lang="scss" scoped>
442 .u-page { 471 .u-page {
443 - min-height: 100vh; 472 +min-height: 100vh;
444 } 473 }
445 -  
446 .inspect-form-content { 474 .inspect-form-content {
447 - background: #fff;  
448 - padding: 20rpx; 475 +background: #fff;
  476 +padding: 20rpx;
449 } 477 }
450 -  
451 .form-item { 478 .form-item {
452 - margin-bottom: 20rpx; 479 +margin-bottom: 20rpx;
453 } 480 }
454 -  
455 -  
456 -</style>  
457 \ No newline at end of file 481 \ No newline at end of file
  482 +</style>
pages-sub/problem/work-order-manage/add-order.vue
@@ -212,7 +212,7 @@ const loadProblemCascader = async (parentCode) =&gt; { @@ -212,7 +212,7 @@ const loadProblemCascader = async (parentCode) =&gt; {
212 // 表单Ref 212 // 表单Ref
213 const workOrderFormRef = ref(null) 213 const workOrderFormRef = ref(null)
214 const problemImgs = useUploadImgs({ 214 const problemImgs = useUploadImgs({
215 - maxCount: 3, 215 + maxCount: 9,
216 uploadText: '选择问题照片', 216 uploadText: '选择问题照片',
217 sizeType: ['compressed'], 217 sizeType: ['compressed'],
218 formRef: workOrderFormRef, 218 formRef: workOrderFormRef,
pages-sub/problem/work-order-manage/order-detail.vue
@@ -116,6 +116,10 @@ @@ -116,6 +116,10 @@
116 </template> 116 </template>
117 </up-cell> 117 </up-cell>
118 118
  119 +
  120 +
  121 + <up-cell title="重点巡检区域" :value="orderDetail.keyInspection==0?'否':'是'" align="middle"></up-cell>
  122 +
119 <!-- 希望完成时间 --> 123 <!-- 希望完成时间 -->
120 <up-cell 124 <up-cell
121 v-if="orderDetail.expectedFinishDate!==0" 125 v-if="orderDetail.expectedFinishDate!==0"
pages/index/index.vue
@@ -24,26 +24,22 @@ @@ -24,26 +24,22 @@
24 24
25 <!-- 任务完成情况卡片 --> 25 <!-- 任务完成情况卡片 -->
26 <view class="task-chart-card"> 26 <view class="task-chart-card">
27 - <view class="card-header">  
28 - <view class="unit-tip">数字指标</view>  
29 - <!-- ActionSheet 点击唤起 -->  
30 - <view class="sort-trigger" @click="openActionSheet">  
31 - {{ sortLabel }}  
32 - <up-icon name="arrow-down" size="14" style="margin-left:4rpx;"></up-icon>  
33 - </view>  
34 - <!-- ActionSheet组件 -->  
35 - <up-action-sheet  
36 - :show="showActionSheet"  
37 - :actions="actionSheetOptions"  
38 - @select="handleSheetSelect"  
39 - @close="showActionSheet = false"  
40 - />  
41 - <view class="date-picker-wrap">  
42 - <neo-datetime-pro v-model="dateRange" type="daterange" :clearable="false" placeholder="请选择日期范围"  
43 - @confirm="handleDateConfirm" :max-date="maxDate" @popup-show="handlePopupShow"  
44 - @popup-hide="handlePopupHide" />  
45 - </view>  
46 -</view> 27 + <view class="card-header">
  28 + <view class="unit-tip">数字指标</view>
  29 + <!-- ActionSheet 点击唤起 -->
  30 + <view class="sort-trigger" @click="openActionSheet">
  31 + {{ sortLabel }}
  32 + <up-icon name="arrow-down" size="14" style="margin-left:4rpx;"></up-icon>
  33 + </view>
  34 + <!-- ActionSheet组件 -->
  35 + <up-action-sheet :show="showActionSheet" :actions="actionSheetOptions" @select="handleSheetSelect"
  36 + @close="showActionSheet = false" />
  37 + <view class="date-picker-wrap">
  38 + <neo-datetime-pro v-model="dateRange" type="daterange" :clearable="false" placeholder="请选择日期范围"
  39 + @confirm="handleDateConfirm" :max-date="maxDate" @popup-show="handlePopupShow"
  40 + @popup-hide="handlePopupHide" />
  41 + </view>
  42 + </view>
47 43
48 44
49 <!-- 双折线K线图 --> 45 <!-- 双折线K线图 -->
@@ -52,23 +48,16 @@ @@ -52,23 +48,16 @@
52 :option="klineOption" :width="chartWidth" :height="chartHeight" /> 48 :option="klineOption" :width="chartWidth" :height="chartHeight" />
53 </view> --> 49 </view> -->
54 50
55 - <up-grid  
56 - :border="false"  
57 - col="4"  
58 - >  
59 - <up-grid-item  
60 - v-for="(baseListItem,baseListIndex) in baseList"  
61 - :key="baseListIndex"  
62 - >  
63 - <!-- <up-icon  
64 - :customStyle="{paddingTop:20+'rpx'}"  
65 - :name="baseListItem.name"  
66 - :size="22"  
67 - ></up-icon> -->  
68 - <text class="grid-text" style="font-size: 24px;">{{baseListItem.num}}</text>  
69 - <text class="grid-text">{{baseListItem.title}}</text> 51 + <up-grid :border="false" col="4">
  52 + <up-grid-item v-for="(baseListItem, baseListIndex) in baseList" :key="baseListIndex">
  53 + <view class="grid-item-icon-box">
  54 + <up-icon :name="baseListItem.name" size="32" color="#1677ff"></up-icon>
  55 + <text class="grid-num-icon">{{ baseListItem.num }}</text>
  56 + <text class="grid-label-icon">{{ baseListItem.title }}</text>
  57 + </view>
70 </up-grid-item> 58 </up-grid-item>
71 - </up-grid> 59 + </up-grid>
  60 +
72 </view> 61 </view>
73 </view> 62 </view>
74 63
@@ -98,13 +87,8 @@ @@ -98,13 +87,8 @@
98 </view> 87 </view>
99 88
100 89
101 - <scroll-view  
102 - class="task-list-container"  
103 - scroll-y  
104 - @scroll="onListScroll"  
105 - @scrolltoupper="handleTouchTop"  
106 - enhanced  
107 - > 90 + <scroll-view class="task-list-container" scroll-y @scroll="onListScroll" @scrolltoupper="handleTouchTop"
  91 + enhanced>
108 <view v-if="taskList.length === 0" class="empty-box"> 92 <view v-if="taskList.length === 0" class="empty-box">
109 <up-empty /> 93 <up-empty />
110 </view> 94 </view>
@@ -113,7 +97,7 @@ @@ -113,7 +97,7 @@
113 <view class="task-item__content"> 97 <view class="task-item__content">
114 <view class="name-fixed-wrap"> 98 <view class="name-fixed-wrap">
115 <view class="task-item__name "> 99 <view class="task-item__name ">
116 - 事项{{ index + 1 }} 100 + 事项{{ index + 1 }}
117 </view> 101 </view>
118 <view class="task-name-text"> 102 <view class="task-name-text">
119 {{ item.taskName || '无' }} 103 {{ item.taskName || '无' }}
@@ -121,7 +105,8 @@ @@ -121,7 +105,8 @@
121 </view> 105 </view>
122 106
123 <view v-if="currentTabValue === 0" class="task-item__footer" style="padding-left: 60px;"> 107 <view v-if="currentTabValue === 0" class="task-item__footer" style="padding-left: 60px;">
124 - <image class="loc-icon" src="https://img.jichengshanshui.com.cn:28207/appimg/定位icon@3x.png" mode="aspectFit"></image> 108 + <image class="loc-icon" src="https://img.jichengshanshui.com.cn:28207/appimg/定位icon@3x.png"
  109 + mode="aspectFit"></image>
125 <template v-if="isWalkTextMode"> 110 <template v-if="isWalkTextMode">
126 <text>距离当前{{ item.distance }}m ·约{{ item.estimatedWalkingMinutes }}分钟步行</text> 111 <text>距离当前{{ item.distance }}m ·约{{ item.estimatedWalkingMinutes }}分钟步行</text>
127 </template> 112 </template>
@@ -143,7 +128,7 @@ @@ -143,7 +128,7 @@
143 import { ref, computed, nextTick, watch } from 'vue'; 128 import { ref, computed, nextTick, watch } from 'vue';
144 import { onShow, onUnload } from '@dcloudio/uni-app'; 129 import { onShow, onUnload } from '@dcloudio/uni-app';
145 import { useUserStore } from '@/pinia/user'; 130 import { useUserStore } from '@/pinia/user';
146 -// import { timeFormat } from '@/uni_modules/uview-plus/libs/function/date'; 131 +import { timeFormat } from '@/uni_modules/uview-plus';
147 import uniCharts from '@/components/uni-charts/uni-charts.vue'; 132 import uniCharts from '@/components/uni-charts/uni-charts.vue';
148 import { getUnreadCount } from "@/api/user"; 133 import { getUnreadCount } from "@/api/user";
149 import { 134 import {
@@ -211,8 +196,8 @@ const currentTabValue = ref(0); @@ -211,8 +196,8 @@ const currentTabValue = ref(0);
211 const dateRange = ref<string[]>([]); 196 const dateRange = ref<string[]>([]);
212 const yesterday = new Date(); 197 const yesterday = new Date();
213 yesterday.setDate(yesterday.getDate() - 1); 198 yesterday.setDate(yesterday.getDate() - 1);
214 -const maxDate = ref(timeFormat(yesterday, 'YYYY-MM-DD'));  
215 -const minDate = '2026-01-06'; 199 +const maxDate = ref(timeFormat(yesterday, 'yyyy-mm-dd'));
  200 +// const minDate = '2026-01-06';
216 const isDatePickerOpen = ref(false); 201 const isDatePickerOpen = ref(false);
217 const handlePopupShow = () => { 202 const handlePopupShow = () => {
218 isDatePickerOpen.value = true; 203 isDatePickerOpen.value = true;
@@ -365,23 +350,12 @@ const fetchOrgIndicator = async (orgId: number, beginDate: string, endDate: stri @@ -365,23 +350,12 @@ const fetchOrgIndicator = async (orgId: number, beginDate: string, endDate: stri
365 }); 350 });
366 console.log('orgTaskIndicator 返回:', res); 351 console.log('orgTaskIndicator 返回:', res);
367 baseList.value = [ 352 baseList.value = [
368 - {  
369 - num: String(res.totalCnt ?? 0),  
370 - title: '应办任务数'  
371 - },  
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: '超时未完成'  
383 - } 353 + { num: String(res.totalCnt ?? 0), title: '应办任务数', name: 'file-text' },
  354 + { num: String(res.doneCnt ?? 0), title: '已办任务数', name: 'checkmark-circle' },
  355 + { num: String(res.pendingCnt ?? 0), title: '待办任务数', name: 'clock' },
  356 + { num: String(res.overdueCnt ?? 0), title: '超时未完成', name: 'close-circle' }
384 ]; 357 ];
  358 +
385 } catch (err) { 359 } catch (err) {
386 console.error('orgTaskIndicator 请求失败:', err); 360 console.error('orgTaskIndicator 请求失败:', err);
387 } 361 }
@@ -393,11 +367,13 @@ const initRecent7Days = () =&gt; { @@ -393,11 +367,13 @@ const initRecent7Days = () =&gt; {
393 const end = new Date(now); 367 const end = new Date(now);
394 end.setDate(end.getDate() - 1); 368 end.setDate(end.getDate() - 1);
395 const start = new Date(end); 369 const start = new Date(end);
  370 + console.log(start)
396 start.setDate(start.getDate() - 6); 371 start.setDate(start.getDate() - 6);
397 dateRange.value = [ 372 dateRange.value = [
398 - timeFormat(start, 'YYYY-MM-DD'),  
399 - timeFormat(end, 'YYYY-MM-DD') 373 + timeFormat(start, 'yyyy-mm-dd'),
  374 + timeFormat(end, 'yyyy-mm-dd')
400 ]; 375 ];
  376 + console.log(dateRange.value)
401 initChartData(dateRange.value[0], dateRange.value[1]); 377 initChartData(dateRange.value[0], dateRange.value[1]);
402 }; 378 };
403 379
@@ -640,17 +616,21 @@ $border-color: #e5e5e5; @@ -640,17 +616,21 @@ $border-color: #e5e5e5;
640 font-size: 16px; 616 font-size: 16px;
641 margin-bottom: 4px; 617 margin-bottom: 4px;
642 } 618 }
  619 +
643 .login-desc { 620 .login-desc {
644 font-size: 16px; 621 font-size: 16px;
645 } 622 }
  623 +
646 .msg-icon { 624 .msg-icon {
647 position: relative; 625 position: relative;
648 padding: 5px; 626 padding: 5px;
649 top: 10px; 627 top: 10px;
650 border-radius: 50%; 628 border-radius: 50%;
  629 +
651 &--hover { 630 &--hover {
652 background-color: rgba(255, 255, 255, 0.2); 631 background-color: rgba(255, 255, 255, 0.2);
653 } 632 }
  633 +
654 .msg-badge { 634 .msg-badge {
655 position: absolute; 635 position: absolute;
656 top: -2px; 636 top: -2px;
@@ -675,6 +655,7 @@ $border-color: #e5e5e5; @@ -675,6 +655,7 @@ $border-color: #e5e5e5;
675 opacity: 1; 655 opacity: 1;
676 overflow: hidden; 656 overflow: hidden;
677 } 657 }
  658 +
678 .chart-wrap[hidden] { 659 .chart-wrap[hidden] {
679 opacity: 0; 660 opacity: 0;
680 height: 0; 661 height: 0;
@@ -695,6 +676,7 @@ $border-color: #e5e5e5; @@ -695,6 +676,7 @@ $border-color: #e5e5e5;
695 margin: 0 $spacing-lg $spacing-md; 676 margin: 0 $spacing-lg $spacing-md;
696 padding: $spacing-md; 677 padding: $spacing-md;
697 flex-shrink: 0; 678 flex-shrink: 0;
  679 +
698 .card-header { 680 .card-header {
699 display: flex; 681 display: flex;
700 justify-content: space-between; 682 justify-content: space-between;
@@ -702,16 +684,19 @@ $border-color: #e5e5e5; @@ -702,16 +684,19 @@ $border-color: #e5e5e5;
702 font-size: 12px; 684 font-size: 12px;
703 color: $text-color-placeholder; 685 color: $text-color-placeholder;
704 margin-bottom: $spacing-md; 686 margin-bottom: $spacing-md;
  687 +
705 .date-picker-wrap { 688 .date-picker-wrap {
706 border-radius: 3px; 689 border-radius: 3px;
707 transition: background-color 0.2s; 690 transition: background-color 0.2s;
708 position: relative; 691 position: relative;
709 z-index: 999; 692 z-index: 999;
  693 +
710 &:active { 694 &:active {
711 background-color: $bg-color; 695 background-color: $bg-color;
712 } 696 }
713 } 697 }
714 } 698 }
  699 +
715 .chart-container { 700 .chart-container {
716 width: 100%; 701 width: 100%;
717 height: 150px; 702 height: 150px;
@@ -720,6 +705,7 @@ $border-color: #e5e5e5; @@ -720,6 +705,7 @@ $border-color: #e5e5e5;
720 justify-content: center; 705 justify-content: center;
721 } 706 }
722 } 707 }
  708 +
723 .con-wrap { 709 .con-wrap {
724 border-radius: 8px; 710 border-radius: 8px;
725 background-color: #fff; 711 background-color: #fff;
@@ -758,6 +744,7 @@ $border-color: #e5e5e5; @@ -758,6 +744,7 @@ $border-color: #e5e5e5;
758 transition: all 0.2s ease; 744 transition: all 0.2s ease;
759 font-weight: 500; 745 font-weight: 500;
760 } 746 }
  747 +
761 .tab-capsule.active { 748 .tab-capsule.active {
762 color: #d4af37; 749 color: #d4af37;
763 background: #fff8dc; 750 background: #fff8dc;
@@ -775,7 +762,7 @@ $border-color: #e5e5e5; @@ -775,7 +762,7 @@ $border-color: #e5e5e5;
775 } 762 }
776 763
777 .tip-top { 764 .tip-top {
778 - padding: 8px 0 0 70px; 765 + padding: 8px 0 0 70px;
779 flex-shrink: 0; 766 flex-shrink: 0;
780 } 767 }
781 768
@@ -789,6 +776,7 @@ $border-color: #e5e5e5; @@ -789,6 +776,7 @@ $border-color: #e5e5e5;
789 .empty-box { 776 .empty-box {
790 padding: 60rpx 0; 777 padding: 60rpx 0;
791 } 778 }
  779 +
792 .sort-trigger { 780 .sort-trigger {
793 display: flex; 781 display: flex;
794 align-items: center; 782 align-items: center;
@@ -801,36 +789,44 @@ $border-color: #e5e5e5; @@ -801,36 +789,44 @@ $border-color: #e5e5e5;
801 padding: $spacing-md; 789 padding: $spacing-md;
802 transition: background-color 0.2s; 790 transition: background-color 0.2s;
803 border-bottom: 1px solid $border-color; 791 border-bottom: 1px solid $border-color;
  792 +
804 &:last-child { 793 &:last-child {
805 border-bottom: none; 794 border-bottom: none;
806 } 795 }
  796 +
807 &--hover { 797 &--hover {
808 background-color: $bg-color; 798 background-color: $bg-color;
809 } 799 }
  800 +
810 &__content { 801 &__content {
811 width: 100%; 802 width: 100%;
812 } 803 }
  804 +
813 .name-fixed-wrap { 805 .name-fixed-wrap {
814 display: flex; 806 display: flex;
815 } 807 }
  808 +
816 .task-item__name { 809 .task-item__name {
817 width: 60px; 810 width: 60px;
818 } 811 }
819 - .task-name-text {  
820 - flex: 1;  
821 - font-size: 14px;  
822 - color: $text-color;  
823 - display: -webkit-box;  
824 - -webkit-line-clamp: 2;  
825 - -webkit-box-orient: vertical;  
826 - overflow: hidden;  
827 - text-overflow: ellipsis;  
828 -} 812 +
  813 + .task-name-text {
  814 + flex: 1;
  815 + font-size: 14px;
  816 + color: $text-color;
  817 + display: -webkit-box;
  818 + -webkit-line-clamp: 2;
  819 + -webkit-box-orient: vertical;
  820 + overflow: hidden;
  821 + text-overflow: ellipsis;
  822 + }
  823 +
829 &__name { 824 &__name {
830 font-size: 15px; 825 font-size: 15px;
831 color: $text-color; 826 color: $text-color;
832 display: inline-block; 827 display: inline-block;
833 } 828 }
  829 +
834 &__footer { 830 &__footer {
835 display: flex; 831 display: flex;
836 align-items: flex-start; 832 align-items: flex-start;
@@ -841,6 +837,7 @@ $border-color: #e5e5e5; @@ -841,6 +837,7 @@ $border-color: #e5e5e5;
841 align-items: center; 837 align-items: center;
842 } 838 }
843 } 839 }
  840 +
844 .loc-icon { 841 .loc-icon {
845 width: 20px; 842 width: 20px;
846 height: 20px; 843 height: 20px;
@@ -849,4 +846,23 @@ $border-color: #e5e5e5; @@ -849,4 +846,23 @@ $border-color: #e5e5e5;
849 .urgency-tag { 846 .urgency-tag {
850 color: #7D7D7D; 847 color: #7D7D7D;
851 } 848 }
  849 +
  850 +.grid-item-icon-box {
  851 + display:flex;
  852 + flex-direction:column;
  853 + align-items:center;
  854 + padding:16rpx;
  855 +}
  856 +.grid-num-icon {
  857 + font-size:40rpx;
  858 + font-weight:600;
  859 + color:#333;
  860 + margin-top:8rpx;
  861 +}
  862 +.grid-label-icon {
  863 + font-size:22rpx;
  864 + color:#666;
  865 + margin-top:4rpx;
  866 +}
  867 +
852 </style> 868 </style>
853 \ No newline at end of file 869 \ No newline at end of file
uni_modules/uview-plus/components/u-cascader/u-cascader.vue
1 <template> 1 <template>
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" 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"
14 @close="close" 12 @close="close"
15 - @closed="$emit('closed')"  
16 - :round="round"  
17 - :pageInline="pageInline"  
18 - :closeOnClickOverlay="closeOnClickOverlay"  
19 > 13 >
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> 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>
59 </view> 61 </view>
60 </view> 62 </view>
61 - </view> 63 + </template>
62 </view> 64 </view>
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>  
134 - </view>  
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> 65 </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> 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>
  70 + </view>
  71 + <view class="u-padding-20 up-flex-fill">
  72 + <up-button @click="handleConfirm" type="primary">{{ t("up.common.confirm") }}</up-button>
162 </view> 73 </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>  
174 </view> 74 </view>
175 - </u-popup> 75 + </up-popup>
176 </template> 76 </template>
177 77
178 <script> 78 <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';  
189 import { t } from '../../libs/i18n' 79 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 - * */  
244 export default { 80 export default {
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 - // 打开弹窗时,设置月份数据 81 + name: 'up-cascader',
  82 + props: {
298 show: { 83 show: {
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 84 + type: Boolean,
  85 + default: false
323 }, 86 },
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 '' 87 + data: {
  88 + type: Array,
  89 + default() {
  90 + return [];
365 } 91 }
366 }, 92 },
367 - currentMonths() {  
368 - if (this.monthSwitch && this.months.length) {  
369 - return [this.months[this.monthIndex]] 93 + modelValue: {
  94 + type: Array,
  95 + default() {
  96 + return [];
370 } 97 }
371 - return this.months  
372 }, 98 },
373 - switchPrevDisabled() {  
374 - return this.monthIndex <= 0 99 + valueKey: {
  100 + type: String,
  101 + default: 'value'
375 }, 102 },
376 - switchNextDisabled() {  
377 - return this.monthIndex >= this.months.length - 1 103 + labelKey: {
  104 + type: String,
  105 + default: 'label'
378 }, 106 },
379 - switchPrevYearDisabled() {  
380 - return this.monthIndex - 12 < 0 107 + childrenKey: {
  108 + type: String,
  109 + default: 'children'
381 }, 110 },
382 - switchNextYearDisabled() {  
383 - return this.monthIndex + 12 > this.months.length - 1 111 + maskCloseAble: {
  112 + type: Boolean,
  113 + default: true
384 }, 114 },
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 115 + zIndex: {
  116 + type: [String, Number],
  117 + default: 0
392 }, 118 },
393 - singleDateLabel() {  
394 - return this.selected[0] || '--' 119 + autoClose: {
  120 + type: Boolean,
  121 + default: false
395 }, 122 },
396 - rangeStartDateLabel() {  
397 - return this.selected[0] || '--' 123 + headerDirection: {
  124 + type: String,
  125 + default: 'row'
398 }, 126 },
399 - rangeEndDateLabel() {  
400 - if (this.selected.length >= 2) {  
401 - return this.selected[this.selected.length - 1]  
402 - }  
403 - return '--' 127 + optionsCols: {
  128 + type: [Number],
  129 + default: 2
404 }, 130 },
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 - } 131 + closeable: {
  132 + type: Boolean,
  133 + default: true
416 } 134 }
417 }, 135 },
418 - mounted() {  
419 - this.start = Date.now()  
420 - this.init() 136 + data() {
  137 + return {
  138 + levelList: [],
  139 + selectedValueIndexs: [],
  140 + tabsIndex: 0,
  141 + popupShow: false,
  142 + confirmValues: []
  143 + }
421 }, 144 },
422 - emits: ["confirm", "close", "closed"],  
423 - methods: {  
424 - addUnit,  
425 - padTime(num) {  
426 - return String(num).padStart(2, '0')  
427 - },  
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))  
440 - }  
441 - const hour = getNumber(0, 23)  
442 - const minute = getNumber(1, 59)  
443 - const second = getNumber(2, 59)  
444 - return [hour, minute, second]  
445 - },  
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)}`  
455 - },  
456 - initTimeValues() {  
457 - const value = this.getDefaultTimeValue()  
458 - this.singleTime = value  
459 - this.rangeStartTime = value  
460 - this.rangeEndTime = value  
461 - },  
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)  
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  
498 - },  
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  
506 - }  
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  
519 - }  
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 - }  
570 - }  
571 - }  
572 - },  
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 - },  
594 - close() {  
595 - this.$emit('close')  
596 - },  
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 145 + watch: {
  146 + data: {
  147 + handler() {
  148 + this.initLevelList();
  149 + this.setDefaultValue();
  150 + },
  151 + immediate: true
611 }, 152 },
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  
634 - }  
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 153 + show: {
  154 + handler(val) {
  155 + this.popupShow = val;
  156 + if (val) {
  157 + this.tabsIndex = 0;
  158 + this.initLevelList();
  159 + this.setDefaultValue();
694 } 160 }
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') 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 + }
707 } 188 }
708 } 189 }
709 - const index = this.months.findIndex(({ year, month }) => {  
710 - return `${year}-${padZero(month)}` === selected  
711 - })  
712 - return index === -1 ? 0 : index 190 + return tabsList;
713 }, 191 },
714 - prevMonth() {  
715 - if (!this.switchPrevDisabled) {  
716 - this.monthIndex -= 1  
717 - }  
718 - },  
719 - nextMonth() {  
720 - if (!this.switchNextDisabled) {  
721 - this.monthIndex += 1  
722 - } 192 + uZIndex() {
  193 + return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
723 }, 194 },
724 - prevYear() {  
725 - if (!this.switchPrevYearDisabled) {  
726 - this.monthIndex -= 12  
727 - } 195 + closeIconColor() {
  196 + return 'var(--up-main-color, #303133)';
728 }, 197 },
729 - nextYear() {  
730 - if (!this.switchNextYearDisabled) {  
731 - this.monthIndex += 12 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 = [];
732 } 237 }
733 }, 238 },
734 - jumpToToday() {  
735 - if (this.todayDisabled) {  
736 - return 239 + setDefaultValue() {
  240 + if (!this.data || this.data.length === 0) {
  241 + this.confirmValues = [];
  242 + return;
737 } 243 }
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) 244 + if (!this.modelValue || this.modelValue.length === 0) {
  245 + this.confirmValues = [];
  246 + return;
744 } 247 }
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) 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;
752 } 266 }
753 - return  
754 } 267 }
755 - this.scrollIntoDefaultMonth(targetMonth)  
756 - this.$nextTick(selectToday) 268 + this.confirmValues = this.getSelectedValues();
757 }, 269 },
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  
790 - } 270 +
  271 + 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');
791 }, 279 },
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 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]);
806 } else { 295 } else {
807 - break 296 + this.$set(this.levelList, levelIndex + 1, currentItem[this.childrenKey]);
  297 + }
  298 + this.tabsIndex = levelIndex + 1;
  299 + // 有子级,不自动确认
  300 + if (this.autoClose) return;
  301 + } else {
  302 + // 已选到最后一级,自动确认
  303 + if (this.autoClose) {
  304 + this.handleConfirm();
808 } 305 }
809 } 306 }
810 - this.monthIndex = monthIndex  
811 - this.scrollIntoViewScroll = `month-${monthIndex}`  
812 }, 307 },
813 - // 更新月份的top值  
814 - onUpdateMonthTop(topArr = []) {  
815 - if (this.monthSwitch) {  
816 - return 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();
817 } 314 }
818 - this.updateMonthTop(topArr)  
819 }, 315 },
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"); 316 + handleCancel() {
  317 + this.close();
  318 + },
  319 + handleConfirm() {
  320 + // 校验:必须选择到最后一级
  321 + if (!this.isCurrentLeafNode()) {
  322 + uni.showToast({ title: '请选择到最后一级', icon: 'none' });
  323 + return;
839 } 324 }
840 - this.scrollIntoDefaultMonth(selected) 325 + const values = this.getSelectedValues();
  326 + this.confirmValues = [...values];
  327 + // 向外输出 选中值数组
  328 + this.$emit('update:modelValue', values);
  329 + this.$emit('confirm', values);
  330 + this.close();
841 } 331 }
842 } 332 }
843 } 333 }
844 </script> 334 </script>
845 335
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; 336 +<style lang="scss">
  337 + .area-box {
  338 + width: 100%;
888 overflow: hidden; 339 overflow: hidden;
889 - }  
890 -  
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 - }  
899 -  
900 - &__time-picker__cancel {  
901 - font-size: 14px;  
902 - color: #909399;  
903 - } 340 + height: 800rpx;
904 341
905 - &__time-picker__title {  
906 - font-size: 15px;  
907 - color: #303133;  
908 - font-weight: 600;  
909 - } 342 + >view {
  343 + width: 150%;
  344 + transition: transform 0.3s ease-in-out 0s;
  345 + transform: translateX(0);
910 346
911 - &__time-picker__confirm {  
912 - font-size: 14px;  
913 - color: #3c9cff;  
914 - }  
915 -  
916 - &__time-picker__body {  
917 - width: 320px;  
918 - height: 200px;  
919 - } 347 + &.change {
  348 + transform: translateX(-33.3333333%);
  349 + }
  350 + }
920 351
921 - &__time-picker__item {  
922 - height: 40px;  
923 - line-height: 40px;  
924 - text-align: center;  
925 - font-size: 16px;  
926 - color: #303133; 352 + .area-item {
  353 + height: 800rpx;
  354 + }
927 } 355 }
928 356
929 - &__confirm {  
930 - padding: 7px 18px; 357 + .u-cascader-action {
  358 + border-top: 1px solid var(--up-border-color, #eee);
931 } 359 }
932 -}  
933 -</style> 360 +</style>
934 \ No newline at end of file 361 \ No newline at end of file