Commit bc2154934a924f10abdab01f46174db881080bd9

Authored by 刘淇
1 parent 9441e28e

考勤记录

api/index/index.js
... ... @@ -63,7 +63,7 @@ export const getCurrentOrg = (params) => {
63 63 * @returns {Promise}
64 64 */
65 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 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 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 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 35 'day-item--other': day.otherMonth,
41 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 41 </view>
48 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 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 71 </view>
74 72 </view>
75   - </view>
76   -</view>
  73 + </view>
77 74 </template>
78   -
79 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 79 const today = new Date();
84 80 const todayTs = today.getTime();
85 81 // 当前显示月份
... ... @@ -87,65 +83,50 @@ const currentYear = ref(today.getFullYear());
87 83 const currentMonth = ref(today.getMonth() + 1);
88 84 const selectDate = ref('');
89 85 const dayRows = ref<any[]>([]);
90   -
91 86 // 是否允许点击下一月:判断当前展示月份是否小于本月
92 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 89 const nowMonthDate = new Date(today.getFullYear(), today.getMonth(), 1);
95 90 return showMonthDate < nowMonthDate;
96 91 });
97 92  
98   -// ========== 写死静态统计数据 ==========
  93 +// 统计数据
99 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 132 const renderMonth = () => {
... ... @@ -159,46 +140,49 @@ const renderMonth = () =&gt; {
159 140 const prevLastDay = new Date(year, month - 1, 0).getDate();
160 141 // 当月第一天星期 0周日~6周六
161 142 const firstWeek = firstDay.getDay();
162   -
163 143 const rows: any[] = [];
164 144 let row: any[] = [];
165 145 // 填充上月尾部
166   - for(let i = 0; i < firstWeek; i++){
  146 + for (let i = 0; i < firstWeek; i++) {
167 147 const dayNum = prevLastDay - firstWeek + i + 1;
168 148 row.push({
169 149 day: dayNum,
170 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 159 day: d,
180 160 otherMonth: false,
181 161 dateStr,
182 162 dot: false,
183 163 dotColor: ''
184 164 };
185   - if(mark){
  165 + if (mark?.dot) {
186 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 173 row.push(item);
190   - if(row.length ===7){
  174 + if (row.length === 7) {
191 175 rows.push(row);
192 176 row = [];
193 177 }
194 178 }
195 179 // 填充下月开头
196 180 let nextDay = 1;
197   - while(row.length <7){
  181 + while (row.length < 7) {
198 182 row.push({
199 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 187 nextDay++;
204 188 }
... ... @@ -208,180 +192,209 @@ const renderMonth = () =&gt; {
208 192  
209 193 // 上一月
210 194 const prevMonth = () => {
211   - if(currentMonth.value === 1){
  195 + if (currentMonth.value === 1) {
212 196 currentYear.value--;
213 197 currentMonth.value = 12;
214   - }else{
  198 + } else {
215 199 currentMonth.value--;
216 200 }
217   - renderMonth();
  201 + getMonthAttendance();
218 202 };
219 203 // 下一月
220 204 const nextMonth = () => {
221 205 let nextY = currentYear.value;
222 206 let nextM = currentMonth.value + 1;
223   - if(nextM > 12){
  207 + if (nextM > 12) {
224 208 nextY++;
225   - nextM =1;
  209 + nextM = 1;
226 210 }
227 211 currentYear.value = nextY;
228 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 220 const selectDay = new Date(day.dateStr);
237   - if(selectDay > today){
  221 + if (selectDay > today) {
238 222 currentAttendance.value = null;
  223 + selectDate.value = day.dateStr;
239 224 return;
240 225 }
241 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 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 254 </script>
263   -
264 255 <style scoped lang="scss">
265   -.attendance-page{
  256 +.attendance-page {
266 257 padding: 20rpx;
267 258 background-color: #f5f5f7;
268 259 min-height: 100vh;
269 260 }
270   -.stat-card{
  261 +
  262 +.stat-card {
271 263 display: flex;
272 264 background-color: #3c9cff;
273 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 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 400 </style>
... ...
pages-sub/daily/patrol-manage/add-patrol-record.vue
1 1 <template>
2 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 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 81 </template>
163   -
164 82 <script setup lang="ts">
165   -import { ref } from 'vue'
  83 +import { ref, getCurrentInstance } from 'vue'
166 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 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 173 <script lang="ts">
171 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 176 import { timeFormat } from '@/uni_modules/uview-plus'
174   -
  177 +import { useUserStore } from '@/pinia/user';
  178 +import { getDictLabel } from '@/common/utils/dict';
175 179 export default {
176   - data() {
177   - return {
178   - imagesList: [],
179   - radioList: [
  180 +data() {
  181 +return {
  182 +imagesList: [],
  183 +radioList: [
180 184 { label: '否', value: '1' },
181 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 214 } else if (imgCount > 3) {
208   - callback(new Error('最多只能上传3张图片'))
  215 +callback(new Error('最多只能上传3张图片'))
209 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 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 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 277 } else {
246   - callback() // 无值时直接通过校验
  278 +callback()
247 279 }
248 280 }
249 281 }
... ... @@ -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 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 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 454 }, 1000)
424   -
425 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 467 }
438 468 }
439 469 </script>
440   -
441 470 <style lang="scss" scoped>
442 471 .u-page {
443   - min-height: 100vh;
  472 +min-height: 100vh;
444 473 }
445   -
446 474 .inspect-form-content {
447   - background: #fff;
448   - padding: 20rpx;
  475 +background: #fff;
  476 +padding: 20rpx;
449 477 }
450   -
451 478 .form-item {
452   - margin-bottom: 20rpx;
  479 +margin-bottom: 20rpx;
453 480 }
454   -
455   -
456   -</style>
457 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 212 // 表单Ref
213 213 const workOrderFormRef = ref(null)
214 214 const problemImgs = useUploadImgs({
215   - maxCount: 3,
  215 + maxCount: 9,
216 216 uploadText: '选择问题照片',
217 217 sizeType: ['compressed'],
218 218 formRef: workOrderFormRef,
... ...
pages-sub/problem/work-order-manage/order-detail.vue
... ... @@ -116,6 +116,10 @@
116 116 </template>
117 117 </up-cell>
118 118  
  119 +
  120 +
  121 + <up-cell title="重点巡检区域" :value="orderDetail.keyInspection==0?'否':'是'" align="middle"></up-cell>
  122 +
119 123 <!-- 希望完成时间 -->
120 124 <up-cell
121 125 v-if="orderDetail.expectedFinishDate!==0"
... ...
pages/index/index.vue
... ... @@ -24,26 +24,22 @@
24 24  
25 25 <!-- 任务完成情况卡片 -->
26 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 45 <!-- 双折线K线图 -->
... ... @@ -52,23 +48,16 @@
52 48 :option="klineOption" :width="chartWidth" :height="chartHeight" />
53 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 58 </up-grid-item>
71   - </up-grid>
  59 + </up-grid>
  60 +
72 61 </view>
73 62 </view>
74 63  
... ... @@ -98,13 +87,8 @@
98 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 92 <view v-if="taskList.length === 0" class="empty-box">
109 93 <up-empty />
110 94 </view>
... ... @@ -113,7 +97,7 @@
113 97 <view class="task-item__content">
114 98 <view class="name-fixed-wrap">
115 99 <view class="task-item__name ">
116   - 事项{{ index + 1 }}
  100 + 事项{{ index + 1 }}
117 101 </view>
118 102 <view class="task-name-text">
119 103 {{ item.taskName || '无' }}
... ... @@ -121,7 +105,8 @@
121 105 </view>
122 106  
123 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 110 <template v-if="isWalkTextMode">
126 111 <text>距离当前{{ item.distance }}m ·约{{ item.estimatedWalkingMinutes }}分钟步行</text>
127 112 </template>
... ... @@ -143,7 +128,7 @@
143 128 import { ref, computed, nextTick, watch } from 'vue';
144 129 import { onShow, onUnload } from '@dcloudio/uni-app';
145 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 132 import uniCharts from '@/components/uni-charts/uni-charts.vue';
148 133 import { getUnreadCount } from "@/api/user";
149 134 import {
... ... @@ -211,8 +196,8 @@ const currentTabValue = ref(0);
211 196 const dateRange = ref<string[]>([]);
212 197 const yesterday = new Date();
213 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 201 const isDatePickerOpen = ref(false);
217 202 const handlePopupShow = () => {
218 203 isDatePickerOpen.value = true;
... ... @@ -365,23 +350,12 @@ const fetchOrgIndicator = async (orgId: number, beginDate: string, endDate: stri
365 350 });
366 351 console.log('orgTaskIndicator 返回:', res);
367 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 359 } catch (err) {
386 360 console.error('orgTaskIndicator 请求失败:', err);
387 361 }
... ... @@ -393,11 +367,13 @@ const initRecent7Days = () =&gt; {
393 367 const end = new Date(now);
394 368 end.setDate(end.getDate() - 1);
395 369 const start = new Date(end);
  370 + console.log(start)
396 371 start.setDate(start.getDate() - 6);
397 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 377 initChartData(dateRange.value[0], dateRange.value[1]);
402 378 };
403 379  
... ... @@ -640,17 +616,21 @@ $border-color: #e5e5e5;
640 616 font-size: 16px;
641 617 margin-bottom: 4px;
642 618 }
  619 +
643 620 .login-desc {
644 621 font-size: 16px;
645 622 }
  623 +
646 624 .msg-icon {
647 625 position: relative;
648 626 padding: 5px;
649 627 top: 10px;
650 628 border-radius: 50%;
  629 +
651 630 &--hover {
652 631 background-color: rgba(255, 255, 255, 0.2);
653 632 }
  633 +
654 634 .msg-badge {
655 635 position: absolute;
656 636 top: -2px;
... ... @@ -675,6 +655,7 @@ $border-color: #e5e5e5;
675 655 opacity: 1;
676 656 overflow: hidden;
677 657 }
  658 +
678 659 .chart-wrap[hidden] {
679 660 opacity: 0;
680 661 height: 0;
... ... @@ -695,6 +676,7 @@ $border-color: #e5e5e5;
695 676 margin: 0 $spacing-lg $spacing-md;
696 677 padding: $spacing-md;
697 678 flex-shrink: 0;
  679 +
698 680 .card-header {
699 681 display: flex;
700 682 justify-content: space-between;
... ... @@ -702,16 +684,19 @@ $border-color: #e5e5e5;
702 684 font-size: 12px;
703 685 color: $text-color-placeholder;
704 686 margin-bottom: $spacing-md;
  687 +
705 688 .date-picker-wrap {
706 689 border-radius: 3px;
707 690 transition: background-color 0.2s;
708 691 position: relative;
709 692 z-index: 999;
  693 +
710 694 &:active {
711 695 background-color: $bg-color;
712 696 }
713 697 }
714 698 }
  699 +
715 700 .chart-container {
716 701 width: 100%;
717 702 height: 150px;
... ... @@ -720,6 +705,7 @@ $border-color: #e5e5e5;
720 705 justify-content: center;
721 706 }
722 707 }
  708 +
723 709 .con-wrap {
724 710 border-radius: 8px;
725 711 background-color: #fff;
... ... @@ -758,6 +744,7 @@ $border-color: #e5e5e5;
758 744 transition: all 0.2s ease;
759 745 font-weight: 500;
760 746 }
  747 +
761 748 .tab-capsule.active {
762 749 color: #d4af37;
763 750 background: #fff8dc;
... ... @@ -775,7 +762,7 @@ $border-color: #e5e5e5;
775 762 }
776 763  
777 764 .tip-top {
778   - padding: 8px 0 0 70px;
  765 + padding: 8px 0 0 70px;
779 766 flex-shrink: 0;
780 767 }
781 768  
... ... @@ -789,6 +776,7 @@ $border-color: #e5e5e5;
789 776 .empty-box {
790 777 padding: 60rpx 0;
791 778 }
  779 +
792 780 .sort-trigger {
793 781 display: flex;
794 782 align-items: center;
... ... @@ -801,36 +789,44 @@ $border-color: #e5e5e5;
801 789 padding: $spacing-md;
802 790 transition: background-color 0.2s;
803 791 border-bottom: 1px solid $border-color;
  792 +
804 793 &:last-child {
805 794 border-bottom: none;
806 795 }
  796 +
807 797 &--hover {
808 798 background-color: $bg-color;
809 799 }
  800 +
810 801 &__content {
811 802 width: 100%;
812 803 }
  804 +
813 805 .name-fixed-wrap {
814 806 display: flex;
815 807 }
  808 +
816 809 .task-item__name {
817 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 824 &__name {
830 825 font-size: 15px;
831 826 color: $text-color;
832 827 display: inline-block;
833 828 }
  829 +
834 830 &__footer {
835 831 display: flex;
836 832 align-items: flex-start;
... ... @@ -841,6 +837,7 @@ $border-color: #e5e5e5;
841 837 align-items: center;
842 838 }
843 839 }
  840 +
844 841 .loc-icon {
845 842 width: 20px;
846 843 height: 20px;
... ... @@ -849,4 +846,23 @@ $border-color: #e5e5e5;
849 846 .urgency-tag {
850 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 868 </style>
853 869 \ No newline at end of file
... ...
uni_modules/uview-plus/components/u-cascader/u-cascader.vue
1 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 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 61 </view>
60 62 </view>
61   - </view>
  63 + </template>
62 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 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 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 74 </view>
175   - </u-popup>
  75 + </up-popup>
176 76 </template>
177 77  
178 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 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 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 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 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 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 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 361 \ No newline at end of file
... ...