Просмотр исходного кода

feat(日程视图): 新增日、周、月视图组件及主页面

实现日程视图功能,包含日视图、周视图和月视图三种展示方式
日视图展示当天、明日及逾期待处理任务卡片
周视图按天分栏展示一周任务及状态标识
月视图使用日历形式展示当月任务及状态标识
weieryang 1 месяц назад
Родитель
Сommit
e10724ed4d

+ 265 - 0
apps/web-ele/src/views/schedule/view/components/day/index.vue

@@ -0,0 +1,265 @@
1
+<script setup lang="ts">
2
+// 模拟数据
3
+const emergencyTasks = [
4
+  {
5
+    id: 1,
6
+    title: '应急演练',
7
+    icon: 'warning',
8
+    color: '#FFB020',
9
+    deadline: '12-31 23:59',
10
+    days: 61,
11
+  },
12
+  {
13
+    id: 2,
14
+    title: '纳税人普查',
15
+    icon: 'document',
16
+    color: '#409EFF',
17
+    deadline: '12-31 23:59',
18
+    days: 61,
19
+  },
20
+];
21
+
22
+const todayTasks = [
23
+  {
24
+    id: 1,
25
+    title: '监控录像抽检',
26
+    icon: 'monitor',
27
+    color: '#F56C6C',
28
+    deadline: '12-07 23:59',
29
+  },
30
+  {
31
+    id: 2,
32
+    title: '爆缸试水1',
33
+    icon: 'water',
34
+    color: '#409EFF',
35
+    deadline: '12-14 23:00',
36
+  },
37
+  {
38
+    id: 3,
39
+    title: '应急消防用品月检',
40
+    icon: 'fire',
41
+    color: '#67C23A',
42
+    deadline: '12-31 13:59',
43
+  },
44
+  {
45
+    id: 4,
46
+    title: '核对销售(油品)调查1',
47
+    icon: 'check',
48
+    color: '#E6A23C',
49
+    deadline: '12-15 23:59',
50
+  },
51
+  {
52
+    id: 5,
53
+    title: '便利店盘点',
54
+    icon: 'shopping',
55
+    color: '#E6A23C',
56
+    deadline: '12-31 23:59',
57
+  },
58
+  {
59
+    id: 6,
60
+    title: '新员工三级教育',
61
+    icon: 'user',
62
+    color: '#409EFF',
63
+    deadline: '12-06 23:00',
64
+  },
65
+  {
66
+    id: 7,
67
+    title: '厨房宿舍月检',
68
+    icon: 'home',
69
+    color: '#909399',
70
+    deadline: '12-31 23:59',
71
+  },
72
+  {
73
+    id: 8,
74
+    title: '收油流程巡检',
75
+    icon: 'oil',
76
+    color: '#409EFF',
77
+    deadline: '12-07 23:59',
78
+  },
79
+  {
80
+    id: 9,
81
+    title: '第一次例会',
82
+    icon: 'meeting',
83
+    color: '#409EFF',
84
+    deadline: '12-15 23:00',
85
+  },
86
+  {
87
+    id: 10,
88
+    title: '建军计划',
89
+    icon: 'plan',
90
+    color: '#67C23A',
91
+    deadline: '12-03 23:00',
92
+  },
93
+  {
94
+    id: 11,
95
+    title: '员工培训',
96
+    icon: 'training',
97
+    color: '#409EFF',
98
+    deadline: '12-31 23:59',
99
+  },
100
+];
101
+
102
+const tomorrowTasks = [
103
+  {
104
+    id: 1,
105
+    title: '巡检日检',
106
+    icon: 'check',
107
+    color: '#67C23A',
108
+    deadline: '12-31 13:59',
109
+  },
110
+  {
111
+    id: 2,
112
+    title: '爆缸试水1',
113
+    icon: 'water',
114
+    color: '#409EFF',
115
+    deadline: '12-14 23:00',
116
+  },
117
+  {
118
+    id: 3,
119
+    title: '应急消防用品月检',
120
+    icon: 'fire',
121
+    color: '#67C23A',
122
+    deadline: '12-31 13:59',
123
+  },
124
+  {
125
+    id: 4,
126
+    title: '核对销售(油品)调查1',
127
+    icon: 'check',
128
+    color: '#E6A23C',
129
+    deadline: '12-15 23:59',
130
+  },
131
+  {
132
+    id: 5,
133
+    title: '便利店盘点',
134
+    icon: 'shopping',
135
+    color: '#E6A23C',
136
+    deadline: '12-31 23:59',
137
+  },
138
+  {
139
+    id: 6,
140
+    title: '新员工三级教育',
141
+    icon: 'user',
142
+    color: '#409EFF',
143
+    deadline: '12-06 23:00',
144
+  },
145
+  {
146
+    id: 7,
147
+    title: '巡检日检',
148
+    icon: 'check',
149
+    color: '#67C23A',
150
+    deadline: '12-31 13:59',
151
+  },
152
+];
153
+
154
+// 模拟图标映射
155
+const getIconEmoji = (icon: string) => {
156
+  const iconMap: Record<string, string> = {
157
+    warning: '⚠️',
158
+    document: '📄',
159
+    monitor: '📺',
160
+    water: '💧',
161
+    fire: '🔥',
162
+    check: '✅',
163
+    shopping: '🛒',
164
+    home: '🏠',
165
+    oil: '⛽',
166
+    meeting: '📋',
167
+    plan: '📝',
168
+    training: '🎓',
169
+    user: '👤',
170
+  };
171
+  return iconMap[icon] || '📌';
172
+};
173
+</script>
174
+
175
+<template>
176
+  <!-- 逾期待处理 -->
177
+  <div class="mb-6">
178
+    <h3 class="mb-3 text-lg font-semibold">逾期待处理</h3>
179
+    <div
180
+      class="grid grid-cols-4 gap-4 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
181
+    >
182
+      <div
183
+        v-for="task in emergencyTasks"
184
+        :key="task.id"
185
+        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
186
+      >
187
+        <div
188
+          class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"
189
+          :style="{ backgroundColor: `${task.color}20`, color: task.color }"
190
+        >
191
+          <span class="text-xl">{{ getIconEmoji(task.icon) }}</span>
192
+        </div>
193
+        <div class="min-w-0 flex-1">
194
+          <div
195
+            class="overflow-hidden text-ellipsis whitespace-nowrap font-medium"
196
+          >
197
+            {{ task.title }}
198
+          </div>
199
+          <div class="text-sm text-gray-500">{{ task.deadline }}</div>
200
+        </div>
201
+        <div class="ml-4 whitespace-nowrap font-medium text-red-500">
202
+          {{ task.days }}天
203
+        </div>
204
+      </div>
205
+    </div>
206
+  </div>
207
+
208
+  <!-- 当日任务 -->
209
+  <div class="mb-6">
210
+    <h3 class="mb-3 text-lg font-semibold">当日</h3>
211
+    <div
212
+      class="grid grid-cols-4 gap-4 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
213
+    >
214
+      <div
215
+        v-for="task in todayTasks"
216
+        :key="task.id"
217
+        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
218
+      >
219
+        <div
220
+          class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"
221
+          :style="{ backgroundColor: `${task.color}20`, color: task.color }"
222
+        >
223
+          <span class="text-xl">{{ getIconEmoji(task.icon) }}</span>
224
+        </div>
225
+        <div class="min-w-0 flex-1">
226
+          <div
227
+            class="overflow-hidden text-ellipsis whitespace-nowrap font-medium"
228
+          >
229
+            {{ task.title }}
230
+          </div>
231
+          <div class="text-sm text-gray-500">{{ task.deadline }}</div>
232
+        </div>
233
+      </div>
234
+    </div>
235
+  </div>
236
+
237
+  <!-- 明日任务 -->
238
+  <div>
239
+    <h3 class="mb-3 text-lg font-semibold">明日</h3>
240
+    <div
241
+      class="grid grid-cols-4 gap-4 sm:grid-cols-4 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
242
+    >
243
+      <div
244
+        v-for="task in tomorrowTasks"
245
+        :key="task.id"
246
+        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
247
+      >
248
+        <div
249
+          class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"
250
+          :style="{ backgroundColor: `${task.color}20`, color: task.color }"
251
+        >
252
+          <span class="text-xl">{{ getIconEmoji(task.icon) }}</span>
253
+        </div>
254
+        <div class="min-w-0 flex-1">
255
+          <div
256
+            class="overflow-hidden text-ellipsis whitespace-nowrap font-medium"
257
+          >
258
+            {{ task.title }}
259
+          </div>
260
+          <div class="text-sm text-gray-500">{{ task.deadline }}</div>
261
+        </div>
262
+      </div>
263
+    </div>
264
+  </div>
265
+</template>

+ 431 - 0
apps/web-ele/src/views/schedule/view/components/month/index.vue

@@ -0,0 +1,431 @@
1
+<script setup>
2
+import { ref, computed } from 'vue';
3
+import { ElCalendar } from 'element-plus';
4
+
5
+// Props from parent component
6
+const props = defineProps({
7
+  month: {
8
+    type: Number,
9
+    required: true,
10
+    default: ''
11
+  },
12
+});
13
+
14
+// 模拟任务数据
15
+const tasks = ref([
16
+  {
17
+    id: 1,
18
+    name: '未来路摄像头抓图AI检查',
19
+    date: '2025-12-15',
20
+    status: 'processable',
21
+    department: '管理部',
22
+  },
23
+  {
24
+    id: 2,
25
+    name: '龙飞街摄像头抓图AI检查',
26
+    date: '2025-12-15',
27
+    status: 'processable',
28
+    department: '管理部',
29
+  },
30
+  {
31
+    id: 3,
32
+    name: '中州大道监控设备维护',
33
+    date: '2025-12-15',
34
+    status: 'not-started',
35
+    department: '技术部',
36
+  },
37
+  {
38
+    id: 4,
39
+    name: '农业路交通流量分析',
40
+    date: '2025-12-15',
41
+    status: 'cancelled',
42
+    department: '数据分析部',
43
+  },
44
+  {
45
+    id: 5,
46
+    name: '未来路摄像头抓图AI检查',
47
+    date: '2025-12-16',
48
+    status: 'processable',
49
+    department: '管理部',
50
+  },
51
+  {
52
+    id: 6,
53
+    name: '龙飞街摄像头抓图AI检查',
54
+    date: '2025-12-16',
55
+    status: 'processable',
56
+    department: '管理部',
57
+  },
58
+  {
59
+    id: 7,
60
+    name: '文化路监控系统升级',
61
+    date: '2025-12-16',
62
+    status: 'completed',
63
+    department: '技术部',
64
+  },
65
+  {
66
+    id: 8,
67
+    name: '纬五路设备巡检',
68
+    date: '2025-12-16',
69
+    status: 'overdue',
70
+    department: '运维部',
71
+  },
72
+  {
73
+    id: 9,
74
+    name: '未来路摄像头抓图AI检查',
75
+    date: '2025-12-17',
76
+    status: 'processable',
77
+    department: '管理部',
78
+  },
79
+  {
80
+    id: 10,
81
+    name: '龙飞街摄像头抓图AI检查',
82
+    date: '2025-12-17',
83
+    status: 'processable',
84
+    department: '管理部',
85
+  },
86
+  {
87
+    id: 11,
88
+    name: '经三路摄像头调试',
89
+    date: '2025-12-17',
90
+    status: 'not-started',
91
+    department: '技术部',
92
+  },
93
+  {
94
+    id: 12,
95
+    name: '东风路设备更新计划',
96
+    date: '2025-12-17',
97
+    status: 'cancelled',
98
+    department: '采购部',
99
+  },
100
+  {
101
+    id: 13,
102
+    name: '未来路摄像头抓图AI检查',
103
+    date: '2025-12-18',
104
+    status: 'processable',
105
+    department: '管理部',
106
+  },
107
+  {
108
+    id: 14,
109
+    name: '龙飞街摄像头抓图AI检查',
110
+    date: '2025-12-18',
111
+    status: 'processable',
112
+    department: '管理部',
113
+  },
114
+  {
115
+    id: 15,
116
+    name: '花园路监控中心改造',
117
+    date: '2025-12-18',
118
+    status: 'completed',
119
+    department: '技术部',
120
+  },
121
+  {
122
+    id: 16,
123
+    name: '桐柏路设备维护',
124
+    date: '2025-12-18',
125
+    status: 'overdue',
126
+    department: '运维部',
127
+  },
128
+  {
129
+    id: 17,
130
+    name: '未来路摄像头抓图AI检查',
131
+    date: '2025-12-19',
132
+    status: 'processable',
133
+    department: '管理部',
134
+  },
135
+  {
136
+    id: 18,
137
+    name: '龙飞街摄像头抓图AI检查',
138
+    date: '2025-12-19',
139
+    status: 'processable',
140
+    department: '管理部',
141
+  },
142
+  {
143
+    id: 19,
144
+    name: '大学路监控设备安装',
145
+    date: '2025-12-19',
146
+    status: 'not-started',
147
+    department: '技术部',
148
+  },
149
+  {
150
+    id: 20,
151
+    name: '汝河路系统测试',
152
+    date: '2025-12-19',
153
+    status: 'cancelled',
154
+    department: '质量部',
155
+  },
156
+  {
157
+    id: 21,
158
+    name: '未来路摄像头抓图AI检查',
159
+    date: '2025-12-20',
160
+    status: 'processable',
161
+    department: '管理部',
162
+  },
163
+  {
164
+    id: 22,
165
+    name: '龙飞街摄像头抓图AI检查',
166
+    date: '2025-12-20',
167
+    status: 'processable',
168
+    department: '管理部',
169
+  },
170
+  {
171
+    id: 23,
172
+    name: '航海路监控数据分析',
173
+    date: '2025-12-20',
174
+    status: 'completed',
175
+    department: '数据分析部',
176
+  },
177
+  {
178
+    id: 24,
179
+    name: '长江路设备巡检',
180
+    date: '2025-12-20',
181
+    status: 'overdue',
182
+    department: '运维部',
183
+  },
184
+  {
185
+    id: 25,
186
+    name: '未来路摄像头抓图AI检查',
187
+    date: '2025-12-21',
188
+    status: 'processable',
189
+    department: '管理部',
190
+  },
191
+  {
192
+    id: 26,
193
+    name: '龙飞街摄像头抓图AI检查',
194
+    date: '2025-12-21',
195
+    status: 'processable',
196
+    department: '管理部',
197
+  },
198
+  {
199
+    id: 27,
200
+    name: '嵩山路监控系统优化',
201
+    date: '2025-12-21',
202
+    status: 'not-started',
203
+    department: '技术部',
204
+  },
205
+  {
206
+    id: 28,
207
+    name: '紫荆山路设备调试',
208
+    date: '2025-12-21',
209
+    status: 'completed',
210
+    department: '技术部',
211
+  },
212
+  {
213
+    id: 29,
214
+    name: '商城路设备维护计划',
215
+    date: '2025-12-21',
216
+    status: 'overdue',
217
+    department: '运维部',
218
+  },
219
+  {
220
+    id: 30,
221
+    name: '金水路交通管理培训',
222
+    date: '2025-12-21',
223
+    status: 'cancelled',
224
+    department: '人力资源部',
225
+  },
226
+]);
227
+
228
+// Get task status class based on task properties
229
+const getTaskStatusClass = (task) => {
230
+  switch (task.status) {
231
+    case 'cancelled':
232
+    case 'not-started': {
233
+      return 'not-started';
234
+    }
235
+    case 'completed': {
236
+      return 'completed';
237
+    }
238
+    case 'overdue': {
239
+      return 'overdue';
240
+    }
241
+    case 'processable': {
242
+      return 'processable';
243
+    }
244
+    default: {
245
+      return 'not-started';
246
+    }
247
+  }
248
+};
249
+
250
+// Get tasks for a specific date
251
+const getTasksForDate = (date) => {
252
+  const dateStr = date.toISOString().split('T')[0];
253
+  return tasks.value.filter(task => {
254
+    if (typeof task.date === 'string') {
255
+      return task.date === dateStr;
256
+    }
257
+    return false;
258
+  });
259
+};
260
+
261
+// Set initial calendar value based on props
262
+const initialCalendarValue = computed(() => {
263
+  if (props.month) {
264
+    // If month prop is provided, use it to set the calendar month
265
+    return new Date(`2025-${props.month.toString().padStart(2, '0')}-01`);
266
+  }
267
+  // Default to December 2025 where there are tasks
268
+  return new Date('2025-12-01');
269
+});
270
+</script>
271
+
272
+<template>
273
+  <div class="month-schedule">
274
+    <div class="status-legend">
275
+      <div class="legend-item">
276
+        <span class="status-indicator not-started"></span>
277
+        <span>灰色任务名:任务未开始/任务取消</span>
278
+      </div>
279
+      <div class="legend-item">
280
+        <span class="status-indicator processable"></span>
281
+        <span>黑色任务名:任务可处理</span>
282
+      </div>
283
+      <div class="legend-item">
284
+        <span class="status-indicator overdue"></span>
285
+        <span>红色任务名:任务过期未完成</span>
286
+      </div>
287
+      <div class="legend-item">
288
+        <span class="status-indicator completed"></span>
289
+        <span>黑色任务名:任务已完成</span>
290
+      </div>
291
+    </div>
292
+    
293
+    <ElCalendar :value="initialCalendarValue">
294
+      <template #header="{ date }">
295
+        <span></span>
296
+      </template>
297
+      <template #date-cell="{ data }">
298
+        <div class="calendar-cell">
299
+          <div class="tasks-container">
300
+            <div 
301
+              v-for="task in getTasksForDate(data.date)" 
302
+              :key="task.id" 
303
+              class="task-item"
304
+              :class="getTaskStatusClass(task)"
305
+            >
306
+              <div class="task-name">{{ task.name }}</div>
307
+            </div>
308
+          </div>
309
+        </div>
310
+      </template>
311
+    </ElCalendar>
312
+  </div>
313
+</template>
314
+
315
+<style scoped>
316
+.month-schedule {
317
+  width: 100%;
318
+  overflow-x: auto;
319
+}
320
+
321
+.status-legend {
322
+  display: flex;
323
+  gap: 20px;
324
+  margin-bottom: 10px;
325
+  padding: 10px;
326
+  background-color: #f5f5f5;
327
+  border-radius: 4px;
328
+}
329
+
330
+.legend-item {
331
+  display: flex;
332
+  align-items: center;
333
+  gap: 5px;
334
+  font-size: 12px;
335
+}
336
+
337
+.status-indicator {
338
+  width: 12px;
339
+  height: 12px;
340
+  border-radius: 50%;
341
+}
342
+
343
+.status-indicator.not-started {
344
+  background-color: #ccc;
345
+}
346
+
347
+.status-indicator.processable {
348
+  background-color: #000;
349
+}
350
+
351
+.status-indicator.overdue {
352
+  background-color: #ff0000;
353
+}
354
+
355
+.status-indicator.completed {
356
+  background-color: #000;
357
+  position: relative;
358
+}
359
+
360
+.status-indicator.completed::after {
361
+  content: '';
362
+  position: absolute;
363
+  top: 50%;
364
+  left: -2px;
365
+  right: -2px;
366
+  height: 1px;
367
+  background-color: #fff;
368
+  transform: rotate(45deg);
369
+}
370
+
371
+/* .calendar-cell {
372
+  height: 150px;
373
+  padding: 4px;
374
+  display: flex;
375
+  flex-direction: column;
376
+} */
377
+
378
+::v-deep .el-calendar-table__row .current {
379
+  height: 150px;
380
+}
381
+
382
+::v-deep .el-calendar-table__row .el-calendar-day {
383
+  height: 150px;
384
+}
385
+
386
+.date-text {
387
+  font-weight: bold;
388
+  margin-bottom: 4px;
389
+}
390
+
391
+.tasks-container {
392
+  flex: 1;
393
+  overflow-y: auto;
394
+}
395
+
396
+.task-item {
397
+  margin-bottom: 4px;
398
+  padding: 2px;
399
+  transition: all 0.2s;
400
+  cursor: pointer;
401
+}
402
+
403
+.task-item.not-started {
404
+  cursor: default;
405
+}
406
+
407
+.task-name {
408
+  font-size: 11px;
409
+  line-height: 1.3;
410
+  text-align: center;
411
+}
412
+
413
+/* Task status styles */
414
+.task-item.not-started {
415
+  color: #999;
416
+  pointer-events: none;
417
+}
418
+
419
+.task-item.processable {
420
+  color: #000;
421
+}
422
+
423
+.task-item.overdue {
424
+  color: #ff0000;
425
+}
426
+
427
+.task-item.completed {
428
+  color: #000;
429
+  text-decoration: line-through;
430
+}
431
+</style>

+ 483 - 0
apps/web-ele/src/views/schedule/view/components/week/index.vue

@@ -0,0 +1,483 @@
1
+<script setup>
2
+import { computed, ref } from 'vue';
3
+
4
+// Props from parent component
5
+const props = defineProps({
6
+  year: {
7
+    type: Number,
8
+    required: true,
9
+  },
10
+  weekNumber: {
11
+    type: Number,
12
+    required: true,
13
+    alias: 'week-number',
14
+  },
15
+});
16
+
17
+// 模拟任务数据
18
+const tasks = ref([
19
+  {
20
+    id: 1,
21
+    name: '未来路摄像头抓图AI检查',
22
+    date: '2025-12-15',
23
+    status: 'processable',
24
+    department: '管理部',
25
+  },
26
+  {
27
+    id: 2,
28
+    name: '龙飞街摄像头抓图AI检查',
29
+    date: '2025-12-15',
30
+    status: 'processable',
31
+    department: '管理部',
32
+  },
33
+  {
34
+    id: 3,
35
+    name: '中州大道监控设备维护',
36
+    date: '2025-12-15',
37
+    status: 'not-started',
38
+    department: '技术部',
39
+  },
40
+  {
41
+    id: 4,
42
+    name: '农业路交通流量分析',
43
+    date: '2025-12-15',
44
+    status: 'cancelled',
45
+    department: '数据分析部',
46
+  },
47
+  {
48
+    id: 5,
49
+    name: '未来路摄像头抓图AI检查',
50
+    date: '2025-12-16',
51
+    status: 'processable',
52
+    department: '管理部',
53
+  },
54
+  {
55
+    id: 6,
56
+    name: '龙飞街摄像头抓图AI检查',
57
+    date: '2025-12-16',
58
+    status: 'processable',
59
+    department: '管理部',
60
+  },
61
+  {
62
+    id: 7,
63
+    name: '文化路监控系统升级',
64
+    date: '2025-12-16',
65
+    status: 'completed',
66
+    department: '技术部',
67
+  },
68
+  {
69
+    id: 8,
70
+    name: '纬五路设备巡检',
71
+    date: '2025-12-16',
72
+    status: 'overdue',
73
+    department: '运维部',
74
+  },
75
+  {
76
+    id: 9,
77
+    name: '未来路摄像头抓图AI检查',
78
+    date: '2025-12-17',
79
+    status: 'processable',
80
+    department: '管理部',
81
+  },
82
+  {
83
+    id: 10,
84
+    name: '龙飞街摄像头抓图AI检查',
85
+    date: '2025-12-17',
86
+    status: 'processable',
87
+    department: '管理部',
88
+  },
89
+  {
90
+    id: 11,
91
+    name: '经三路摄像头调试',
92
+    date: '2025-12-17',
93
+    status: 'not-started',
94
+    department: '技术部',
95
+  },
96
+  {
97
+    id: 12,
98
+    name: '东风路设备更新计划',
99
+    date: '2025-12-17',
100
+    status: 'cancelled',
101
+    department: '采购部',
102
+  },
103
+  {
104
+    id: 13,
105
+    name: '未来路摄像头抓图AI检查',
106
+    date: '2025-12-18',
107
+    status: 'processable',
108
+    department: '管理部',
109
+  },
110
+  {
111
+    id: 14,
112
+    name: '龙飞街摄像头抓图AI检查',
113
+    date: '2025-12-18',
114
+    status: 'processable',
115
+    department: '管理部',
116
+  },
117
+  {
118
+    id: 15,
119
+    name: '花园路监控中心改造',
120
+    date: '2025-12-18',
121
+    status: 'completed',
122
+    department: '技术部',
123
+  },
124
+  {
125
+    id: 16,
126
+    name: '桐柏路设备维护',
127
+    date: '2025-12-18',
128
+    status: 'overdue',
129
+    department: '运维部',
130
+  },
131
+  {
132
+    id: 17,
133
+    name: '未来路摄像头抓图AI检查',
134
+    date: '2025-12-19',
135
+    status: 'processable',
136
+    department: '管理部',
137
+  },
138
+  {
139
+    id: 18,
140
+    name: '龙飞街摄像头抓图AI检查',
141
+    date: '2025-12-19',
142
+    status: 'processable',
143
+    department: '管理部',
144
+  },
145
+  {
146
+    id: 19,
147
+    name: '大学路监控设备安装',
148
+    date: '2025-12-19',
149
+    status: 'not-started',
150
+    department: '技术部',
151
+  },
152
+  {
153
+    id: 20,
154
+    name: '汝河路系统测试',
155
+    date: '2025-12-19',
156
+    status: 'cancelled',
157
+    department: '质量部',
158
+  },
159
+  {
160
+    id: 21,
161
+    name: '未来路摄像头抓图AI检查',
162
+    date: '2025-12-20',
163
+    status: 'processable',
164
+    department: '管理部',
165
+  },
166
+  {
167
+    id: 22,
168
+    name: '龙飞街摄像头抓图AI检查',
169
+    date: '2025-12-20',
170
+    status: 'processable',
171
+    department: '管理部',
172
+  },
173
+  {
174
+    id: 23,
175
+    name: '航海路监控数据分析',
176
+    date: '2025-12-20',
177
+    status: 'completed',
178
+    department: '数据分析部',
179
+  },
180
+  {
181
+    id: 24,
182
+    name: '长江路设备巡检',
183
+    date: '2025-12-20',
184
+    status: 'overdue',
185
+    department: '运维部',
186
+  },
187
+  {
188
+    id: 25,
189
+    name: '未来路摄像头抓图AI检查',
190
+    date: '2025-12-21',
191
+    status: 'processable',
192
+    department: '管理部',
193
+  },
194
+  {
195
+    id: 26,
196
+    name: '龙飞街摄像头抓图AI检查',
197
+    date: '2025-12-21',
198
+    status: 'processable',
199
+    department: '管理部',
200
+  },
201
+  {
202
+    id: 27,
203
+    name: '嵩山路监控系统优化',
204
+    date: '2025-12-21',
205
+    status: 'not-started',
206
+    department: '技术部',
207
+  },
208
+  {
209
+    id: 28,
210
+    name: '紫荆山路设备调试',
211
+    date: '2025-12-21',
212
+    status: 'completed',
213
+    department: '技术部',
214
+  },
215
+  {
216
+    id: 29,
217
+    name: '商城路设备维护计划',
218
+    date: '2025-12-21',
219
+    status: 'overdue',
220
+    department: '运维部',
221
+  },
222
+  {
223
+    id: 30,
224
+    name: '金水路交通管理培训',
225
+    date: '2025-12-21',
226
+    status: 'cancelled',
227
+    department: '人力资源部',
228
+  },
229
+]);
230
+
231
+// Calculate the 7 days of the specified week
232
+const weekDays = computed(() => {
233
+  const days = [];
234
+  const firstDayOfWeek = new Date(props.year, 0, 1);
235
+  const diff = firstDayOfWeek.getDay() === 0 ? 6 : firstDayOfWeek.getDay() - 1;
236
+
237
+  firstDayOfWeek.setDate(
238
+    firstDayOfWeek.getDate() - diff + (props.weekNumber - 1) * 7,
239
+  );
240
+
241
+  for (let i = 0; i < 7; i++) {
242
+    const currentDate = new Date(firstDayOfWeek);
243
+    currentDate.setDate(currentDate.getDate() + i);
244
+
245
+    const dateStr = currentDate.getDate().toString();
246
+    const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
247
+    const weekday = weekdays[currentDate.getDay()];
248
+
249
+    days.push({
250
+      date: dateStr,
251
+      weekday,
252
+      fullDate: currentDate.toISOString().split('T')[0],
253
+    });
254
+  }
255
+
256
+  return days;
257
+});
258
+
259
+// Get tasks for a specific day
260
+const getTasksForDay = (date) => {
261
+  return tasks.value.filter((task) => {
262
+    if (typeof task.date === 'string') {
263
+      const taskDate = new Date(task.date);
264
+      const dayDate = new Date(props.year, 0, 1);
265
+      const diff = dayDate.getDay() === 0 ? 6 : dayDate.getDay() - 1;
266
+      dayDate.setDate(
267
+        dayDate.getDate() -
268
+          diff +
269
+          (props.weekNumber - 1) * 7 +
270
+          Number.parseInt(date) -
271
+          1,
272
+      );
273
+
274
+      return taskDate.toDateString() === dayDate.toDateString();
275
+    }
276
+    return false;
277
+  });
278
+};
279
+
280
+// Get task status class based on task properties
281
+const getTaskStatusClass = (task) => {
282
+  switch (task.status) {
283
+    case 'cancelled':
284
+    case 'not-started': {
285
+      return 'not-started';
286
+    }
287
+    case 'completed': {
288
+      return 'completed';
289
+    }
290
+    case 'overdue': {
291
+      return 'overdue';
292
+    }
293
+    case 'processable': {
294
+      return 'processable';
295
+    }
296
+    default: {
297
+      return 'not-started';
298
+    }
299
+  }
300
+};
301
+</script>
302
+<template>
303
+<div class="week-schedule">
304
+    <div class="status-legend">
305
+      <div class="legend-item">
306
+        <span class="status-indicator not-started"></span>
307
+        <span>灰色任务名:任务未开始/任务取消</span>
308
+      </div>
309
+      <div class="legend-item">
310
+        <span class="status-indicator processable"></span>
311
+        <span>黑色任务名:任务可处理</span>
312
+      </div>
313
+      <div class="legend-item">
314
+        <span class="status-indicator overdue"></span>
315
+        <span>红色任务名:任务过期未完成</span>
316
+      </div>
317
+      <div class="legend-item">
318
+        <span class="status-indicator completed"></span>
319
+        <span>黑色任务名:任务已完成</span>
320
+      </div>
321
+    </div>
322
+    
323
+    <div class="week-container">
324
+      <div 
325
+        v-for="day in weekDays" 
326
+        :key="day.date" 
327
+        class="day-column"
328
+      >
329
+        <div class="day-header">
330
+          <div class="date-weekday">{{ day.date }} {{ day.weekday }}</div>
331
+        </div>
332
+        <div class="tasks-container">
333
+          <div 
334
+            v-for="task in tasks" 
335
+            :key="task.id" 
336
+            class="task-item"
337
+            :class="getTaskStatusClass(task)"
338
+          >
339
+            <div class="task-name">{{ task.name }}</div>
340
+          </div>
341
+        </div>
342
+      </div>
343
+    </div>
344
+  </div>
345
+</template>
346
+
347
+<style scoped>
348
+.week-schedule {
349
+  width: 100%;
350
+  overflow-x: auto;
351
+}
352
+
353
+.status-legend {
354
+  display: flex;
355
+  gap: 20px;
356
+  margin-bottom: 10px;
357
+  padding: 10px;
358
+  background-color: #f5f5f5;
359
+  border-radius: 4px;
360
+}
361
+
362
+.legend-item {
363
+  display: flex;
364
+  align-items: center;
365
+  gap: 5px;
366
+  font-size: 12px;
367
+}
368
+
369
+.status-indicator {
370
+  width: 12px;
371
+  height: 12px;
372
+  border-radius: 50%;
373
+}
374
+
375
+.status-indicator.not-started {
376
+  background-color: #ccc;
377
+}
378
+
379
+.status-indicator.processable {
380
+  background-color: #000;
381
+}
382
+
383
+.status-indicator.overdue {
384
+  background-color: #ff0000;
385
+}
386
+
387
+.status-indicator.completed {
388
+  background-color: #000;
389
+  position: relative;
390
+}
391
+
392
+.status-indicator.completed::after {
393
+  content: '';
394
+  position: absolute;
395
+  top: 50%;
396
+  left: -2px;
397
+  right: -2px;
398
+  height: 1px;
399
+  background-color: #fff;
400
+  transform: rotate(45deg);
401
+}
402
+
403
+.week-container {
404
+  display: flex;
405
+  border: 1px solid #e0e0e0;
406
+  border-radius: 4px;
407
+  overflow: hidden;
408
+}
409
+
410
+.day-column {
411
+  flex: 1;
412
+  border-right: 1px solid #e0e0e0;
413
+  min-width: 120px;
414
+}
415
+
416
+.day-column:last-child {
417
+  border-right: none;
418
+}
419
+
420
+.day-header {
421
+  background-color: #fafafa;
422
+  padding: 8px;
423
+  text-align: center;
424
+  border-bottom: 1px solid #e0e0e0;
425
+  font-weight: bold;
426
+}
427
+
428
+.date-weekday {
429
+  font-size: 14px;
430
+  font-weight: bold;
431
+  text-align: center;
432
+}
433
+
434
+.tasks-container {
435
+  padding: 8px;
436
+  min-height: 300px;
437
+}
438
+
439
+.task-item {
440
+  margin-bottom: 6px;
441
+  padding: 4px;
442
+  transition: all 0.2s;
443
+}
444
+
445
+.task-item:hover {
446
+  cursor: pointer;
447
+}
448
+
449
+.task-item.not-started:hover {
450
+  cursor: default;
451
+}
452
+
453
+.task-name {
454
+  font-size: 12px;
455
+  line-height: 1.4;
456
+  text-align: center;
457
+}
458
+
459
+.task-assignee {
460
+  font-size: 11px;
461
+  color: #888;
462
+  line-height: 1.3;
463
+}
464
+
465
+/* Task status styles */
466
+.task-item.not-started {
467
+  color: #999;
468
+  pointer-events: none;
469
+}
470
+
471
+.task-item.processable {
472
+  color: #000;
473
+}
474
+
475
+.task-item.overdue {
476
+  color: #ff0000;
477
+}
478
+
479
+.task-item.completed {
480
+  color: #000;
481
+  text-decoration: line-through;
482
+}
483
+</style>

+ 124 - 0
apps/web-ele/src/views/schedule/view/index.vue

@@ -0,0 +1,124 @@
1
+<script lang="ts" setup>
2
+import { computed, ref, watch } from 'vue';
3
+
4
+import { Page } from '@vben/common-ui';
5
+
6
+import dayjs from 'dayjs';
7
+import { ElRadioButton, ElRadioGroup } from 'element-plus';
8
+
9
+import Day from './components/day/index.vue';
10
+import Month from './components/month/index.vue';
11
+import Week from './components/week/index.vue';
12
+
13
+// 根据timeType计算默认日期
14
+const getDefaultDate = (type: string) => {
15
+  switch (type) {
16
+    case 'month': {
17
+      return dayjs().format('YYYY-MM');
18
+    }
19
+    case 'week': {
20
+      return dayjs().format('YYYY-MM-DD');
21
+    }
22
+    default: {
23
+      return dayjs().format('YYYY-MM-DD');
24
+    }
25
+  }
26
+};
27
+
28
+const searchParams = ref({
29
+  timeType: 'day',
30
+  station: '',
31
+  date: getDefaultDate('day'),
32
+});
33
+
34
+// 监听timeType变化,更新默认日期
35
+watch(
36
+  () => searchParams.value.timeType,
37
+  (newType) => {
38
+    searchParams.value.date = getDefaultDate(newType);
39
+  },
40
+);
41
+
42
+// 计算年份和周数
43
+const year = computed(() => {
44
+  return dayjs(searchParams.value.date).year();
45
+});
46
+
47
+const weekNumber = computed(() => {
48
+  return dayjs(searchParams.value.date).week();
49
+});
50
+</script>
51
+
52
+<template>
53
+  <Page title="日程视图">
54
+    <template #description>
55
+      <!--筛选区域-->
56
+      <div class="flex items-center justify-between space-x-4">
57
+        <div class="flex items-center space-x-4">
58
+          <ElSelect
59
+            v-model="searchParams.station"
60
+            placeholder="请选择油站"
61
+            class="w-48 min-w-48 max-w-48"
62
+            style="width: 12rem"
63
+          >
64
+            <ElOption label="第一站-龙-龙血站" value="1" />
65
+          </ElSelect>
66
+
67
+          <ElRadioGroup v-model="searchParams.timeType">
68
+            <ElRadioButton label="day">日</ElRadioButton>
69
+            <ElRadioButton label="week">周</ElRadioButton>
70
+            <ElRadioButton label="month">月</ElRadioButton>
71
+          </ElRadioGroup>
72
+          <ElDatePicker
73
+            v-model="searchParams.date"
74
+            :type="
75
+              searchParams.timeType === 'month'
76
+                ? 'month'
77
+                : searchParams.timeType === 'week'
78
+                  ? 'week'
79
+                  : 'date'
80
+            "
81
+            :placeholder="
82
+              searchParams.timeType === 'month'
83
+                ? '选择月份'
84
+                : searchParams.timeType === 'week'
85
+                  ? '选择周'
86
+                  : '选择日期'
87
+            "
88
+            :format="
89
+              searchParams.timeType === 'month'
90
+                ? 'YYYY-MM'
91
+                : searchParams.timeType === 'week'
92
+                  ? 'YYYY [年] ww [周]'
93
+                  : 'YYYY-MM-DD'
94
+            "
95
+            :value-format="
96
+              searchParams.timeType === 'month'
97
+                ? 'YYYY-MM'
98
+                : searchParams.timeType === 'week'
99
+                  ? 'YYYY-MM-DD'
100
+                  : 'YYYY-MM-DD'
101
+            "
102
+            class="w-48 min-w-48 max-w-48"
103
+            style="width: 12rem"
104
+          />
105
+        </div>
106
+
107
+        <div class="flex items-center space-x-4">
108
+          <ElButton type="primary"> 油站新增任务 </ElButton>
109
+          <ElButton type="primary"> 访客新增任务 </ElButton>
110
+          <ElButton type="primary"> 任务转接 </ElButton>
111
+        </div>
112
+      </div>
113
+    </template>
114
+    <ElCard class="mb-5 w-full">
115
+      <Day v-if="searchParams.timeType === 'day'" />
116
+      <Week
117
+        v-if="searchParams.timeType === 'week'"
118
+        :year="year"
119
+        :week-number="weekNumber"
120
+      />
121
+      <Month v-if="searchParams.timeType === 'month'" :year="year" />
122
+    </ElCard>
123
+  </Page>
124
+</template>