Sfoglia il codice sorgente

feat(任务详情): 新增任务详情页面及功能组件

添加任务详情页面路由配置
实现任务详情主页面布局及基本信息展示
新增检查项、查阅和评论功能组件
为日程视图添加任务点击跳转功能
weieryang 1 mese fa
parent
commit
eabc13f4dd

+ 11 - 0
apps/web-ele/src/router/routes/local.ts

@@ -80,6 +80,17 @@ const localRoutes: RouteRecordStringComponent[] = [
80 80
     name: 'KnowledgeDetail',
81 81
     path: '/knowledge/detail/:classId',
82 82
   },
83
+  {
84
+    component: '/schedule/detail/index',
85
+    meta: {
86
+      activePath: '/schedule/detail',
87
+      icon: 'carbon:data-base',
88
+      title: '任务详情',
89
+      hideInMenu: true,
90
+    },
91
+    name: 'ScheduleDetail',
92
+    path: '/schedule/detail/:taskId',
93
+  },
83 94
 ];
84 95
 
85 96
 /**

+ 305 - 0
apps/web-ele/src/views/schedule/detail/components/check.vue

@@ -0,0 +1,305 @@
1
+<script lang="ts" setup>
2
+import { ref } from 'vue';
3
+import { ElCard, ElTag, ElButton, ElImage, ElTabs, ElTabPane } from 'element-plus';
4
+
5
+// 检查项类型定义
6
+interface CheckItem {
7
+  id: number;
8
+  title: string;
9
+  description: string;
10
+  isRequired: boolean;
11
+  images: string[];
12
+  score: number;
13
+  totalScore: number;
14
+  items?: CheckItem[];
15
+}
16
+
17
+// 区域类型定义
18
+type AreaType = '前庭' | '后庭' | '便利店' | '卫生间' | '其他';
19
+
20
+// 当前选中的区域
21
+const activeArea = ref<AreaType>('前庭');
22
+
23
+// 区域数据
24
+const areaData = ref<Record<AreaType, CheckItem[]>>({
25
+  // 前庭数据
26
+  '前庭': [
27
+    {
28
+      id: 1,
29
+      title: '【停车】',
30
+      description: '车辆需停在指定区域,无阻碍、无违章',
31
+      isRequired: true,
32
+      images: ['/images/parking1.jpg', '/images/parking2.jpg'],
33
+      score: 0,
34
+      totalScore: 0,
35
+      items: [
36
+        {
37
+          id: 11,
38
+          title: '【停车】入口车辆需停在指定区域,无阻碍、无违章',
39
+          description: '必检(必填项)',
40
+          isRequired: true,
41
+          images: ['/images/parking-entrance1.jpg', '/images/parking-entrance2.jpg'],
42
+          score: 0,
43
+          totalScore: 0,
44
+        }
45
+      ]
46
+    },
47
+    {
48
+      id: 2,
49
+      title: '【卫生】',
50
+      description: '生活服务区1米内地面无杂物、垃圾、地面需清洗干净、无积水、无积油、无积灰',
51
+      isRequired: true,
52
+      images: ['/images/clean1.jpg', '/images/clean2.jpg', '/images/clean3.jpg', '/images/clean4.jpg', '/images/clean5.jpg', '/images/clean6.jpg'],
53
+      score: 0,
54
+      totalScore: 0,
55
+      items: [
56
+        {
57
+          id: 21,
58
+          title: '【卫生】生活服务区1米内地面无杂物、垃圾、地面需清洗干净、无积水、无积油、无积灰',
59
+          description: '设配编号:101',
60
+          isRequired: true,
61
+          images: ['/images/sanitary1.jpg', '/images/sanitary2.jpg', '/images/sanitary3.jpg', '/images/sanitary4.jpg', '/images/sanitary5.jpg', '/images/sanitary6.jpg'],
62
+          score: 0,
63
+          totalScore: 0,
64
+        }
65
+      ]
66
+    }
67
+  ],
68
+  // 后庭数据
69
+  '后庭': [
70
+    {
71
+      id: 3,
72
+      title: '【安全】',
73
+      description: '加油岛消防器材是否按规定摆放并检查合格',
74
+      isRequired: false,
75
+      images: [],
76
+      score: 0,
77
+      totalScore: 0,
78
+      items: [
79
+        {
80
+          id: 31,
81
+          title: '【安全】加油岛消防器材是否按规定摆放并检查合格',
82
+          description: '说明',
83
+          isRequired: false,
84
+          images: [],
85
+          score: 0,
86
+          totalScore: 0,
87
+        }
88
+      ]
89
+    },
90
+    {
91
+      id: 4,
92
+      title: '【设备】',
93
+      description: '员工是否可以操作平衡车:具备统一证照、统一着装、袖口、领口、工牌、袖标、袖口、反光条、驾驶证',
94
+      isRequired: true,
95
+      images: [],
96
+      score: 0,
97
+      totalScore: 0,
98
+      items: [
99
+        {
100
+          id: 41,
101
+          title: '【设备】员工是否可以操作平衡车:具备统一证照、统一着装、袖口、领口、工牌、袖标、袖口、反光条、驾驶证',
102
+          description: '说明',
103
+          isRequired: true,
104
+          images: [],
105
+          score: 0,
106
+          totalScore: 0,
107
+        }
108
+      ]
109
+    }
110
+  ],
111
+  // 便利店数据
112
+  '便利店': [
113
+    {
114
+      id: 5,
115
+      title: '【环境】',
116
+      description: '便利店卫生需整洁有序,无杂物、无灰尘',
117
+      isRequired: true,
118
+      images: ['/images/env1.jpg'],
119
+      score: 0,
120
+      totalScore: 0,
121
+      items: [
122
+        {
123
+          id: 51,
124
+          title: '【环境】便利店卫生需整洁有序,无杂物、无灰尘',
125
+          description: '说明',
126
+          isRequired: true,
127
+          images: ['/images/env-detail1.jpg'],
128
+          score: 0,
129
+          totalScore: 0,
130
+        }
131
+      ]
132
+    }
133
+  ],
134
+  // 卫生间数据
135
+  '卫生间': [
136
+    {
137
+      id: 7,
138
+      title: '【卫生】',
139
+      description: '卫生间地面需保持清洁,无积水、无异味',
140
+      isRequired: true,
141
+      images: ['/images/toilet1.jpg', '/images/toilet2.jpg'],
142
+      score: 0,
143
+      totalScore: 0,
144
+      items: [
145
+        {
146
+          id: 71,
147
+          title: '【卫生】卫生间地面需保持清洁,无积水、无异味',
148
+          description: '必检(必填项)',
149
+          isRequired: true,
150
+          images: ['/images/toilet-detail1.jpg', '/images/toilet-detail2.jpg'],
151
+          score: 0,
152
+          totalScore: 0,
153
+        }
154
+      ]
155
+    }
156
+  ],
157
+  // 其他数据
158
+  '其他': [
159
+    {
160
+      id: 6,
161
+      title: '【综合】',
162
+      description: '服务区内是否有禁止吸烟标识,员工是否佩戴工牌,着装是否符合要求,门口是否有禁止吸烟标识',
163
+      isRequired: true,
164
+      images: [],
165
+      score: 0,
166
+      totalScore: 0,
167
+      items: [
168
+        {
169
+          id: 61,
170
+          title: '【综合】服务区内是否有禁止吸烟标识,员工是否佩戴工牌,着装是否符合要求,门口是否有禁止吸烟标识',
171
+          description: '说明',
172
+          isRequired: true,
173
+          images: [],
174
+          score: 0,
175
+          totalScore: 0,
176
+        }
177
+      ]
178
+    }
179
+  ]
180
+});
181
+
182
+// 获取当前区域的数据
183
+const currentCheckList = ref<CheckItem[]>(areaData.value[activeArea.value]);
184
+
185
+// 切换区域时更新数据
186
+const handleAreaChange = (area: AreaType) => {
187
+  activeArea.value = area;
188
+  currentCheckList.value = areaData.value[area];
189
+};
190
+
191
+// 处理说明按钮点击
192
+const handleExplain = (item: CheckItem) => {
193
+  console.log('查看说明:', item);
194
+};
195
+</script>
196
+
197
+<template>
198
+  <div class="check-list-container">
199
+    <!-- <ElCard class="check-list-card"> -->
200
+      <!-- 顶部统计 -->
201
+      <div class="flex items-center justify-between mb-6">
202
+        <div class="flex items-center space-x-4">
203
+          <span class="text-sm text-gray-500">总分:</span>
204
+          <span class="text-lg font-bold text-gray-800">0</span>
205
+          <span class="text-sm text-gray-500">未达标项:</span>
206
+          <span class="text-lg font-bold text-gray-800">0</span>
207
+          <span class="text-sm text-gray-500">异常项:</span>
208
+          <span class="text-lg font-bold text-gray-800">0</span>
209
+        </div>
210
+        <ElButton type="primary" size="small">
211
+          上传
212
+        </ElButton>
213
+      </div>
214
+
215
+      <!-- 区域切换Tabs -->
216
+      <ElTabs v-model="activeArea" @tab-change="handleAreaChange" class="mb-6">
217
+        <ElTabPane label="前庭" name="前庭" />
218
+        <ElTabPane label="后庭" name="后庭" />
219
+        <ElTabPane label="便利店" name="便利店" />
220
+        <ElTabPane label="卫生间" name="卫生间" />
221
+        <ElTabPane label="其他" name="其他" />
222
+      </ElTabs>
223
+
224
+      <!-- 检查项列表 -->
225
+      <div class="check-items">
226
+        <div v-for="item in currentCheckList" :key="item.id" class="mb-6">
227
+          <!-- 子检查项 -->
228
+          <div v-if="item.items && item.items.length > 0" class="space-y-4">
229
+            <div v-for="(subItem, index) in item.items" :key="subItem.id" class="mb-4 p-4 border rounded-lg">
230
+              <div class="flex items-start mb-2">
231
+                <div class="text-sm font-medium mr-2">
232
+                  {{ index + 1 }}.
233
+                </div>
234
+                <ElTag :type="subItem.isRequired ? 'danger' : 'warning'" size="small" class="mr-2">
235
+                  {{ subItem.isRequired ? '必检' : '选检' }}
236
+                </ElTag>
237
+                <div class="flex-1">
238
+                  <div class="font-medium text-sm">{{ subItem.title }}</div>
239
+                  <div class="text-xs text-gray-500 mt-1">{{ subItem.description }}</div>
240
+                </div>
241
+              </div>
242
+
243
+              <!-- 图片展示 -->
244
+              <div v-if="subItem.images && subItem.images.length > 0" class="flex flex-wrap gap-2 mt-3">
245
+                <ElImage
246
+                  v-for="(img, index) in subItem.images"
247
+                  :key="index"
248
+                  :src="img"
249
+                  :preview-src-list="subItem.images"
250
+                  class="check-image"
251
+                  fit="cover"
252
+                />
253
+              </div>
254
+
255
+              <!-- 评分信息 -->
256
+              <!-- <div class="flex items-center mt-3 text-sm">
257
+                <div class="mr-4">
258
+                  <span class="text-gray-500">得分:</span>
259
+                  <span class="font-medium ml-1">{{ subItem.score }}</span>
260
+                </div>
261
+                <div>
262
+                  <span class="text-gray-500">总分:</span>
263
+                  <span class="font-medium ml-1">{{ subItem.totalScore }}</span>
264
+                </div>
265
+              </div> -->
266
+            </div>
267
+          </div>
268
+        </div>
269
+      </div>
270
+
271
+      <!-- 底部导航 -->
272
+      <!-- <div class="flex justify-center items-center mt-8 space-x-4">
273
+        <ElButton type="primary" size="small">上一个</ElButton>
274
+        <ElButton type="primary" size="small">下一个</ElButton>
275
+      </div> -->
276
+    <!-- </ElCard> -->
277
+  </div>
278
+</template>
279
+
280
+<style scoped lang="scss">
281
+.check-list-container {
282
+  width: 100%;
283
+}
284
+
285
+.check-list-card {
286
+  border-radius: 8px;
287
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
288
+}
289
+
290
+.check-items {
291
+  /* 移除滚动条限制,让内容根据数据高度自适应 */
292
+}
293
+
294
+.check-image {
295
+  width: 80px;
296
+  height: 60px;
297
+  border-radius: 4px;
298
+  cursor: pointer;
299
+}
300
+
301
+.check-image:hover {
302
+  opacity: 0.8;
303
+  transition: opacity 0.3s ease;
304
+}
305
+</style>

+ 539 - 0
apps/web-ele/src/views/schedule/detail/components/comment.vue

@@ -0,0 +1,539 @@
1
+<script lang="ts" setup>
2
+import { ref, computed } from 'vue';
3
+import { ElRate, ElInput, ElSelect, ElOption, ElButton, ElIcon, ElUpload, ElMessage, ElImageViewer } from 'element-plus';
4
+import { Camera, Plus, Message } from '@element-plus/icons-vue';
5
+
6
+// 评分等级映射
7
+const ratingLevelMap = {
8
+  1: '差',
9
+  2: '较差',
10
+  3: '一般',
11
+  4: '良好',
12
+  5: '优秀',
13
+};
14
+
15
+// 模拟评论数据
16
+const comments = ref([
17
+  {
18
+    id: 1,
19
+    username: '王凯',
20
+    department: '当值经理',
21
+    avatar: '',
22
+    rating: 5,
23
+    content: '问题及时处理,处理结果第一时间反馈,下一步需注重现场卫生',
24
+    createdAt: '2025-11-07 19:42:05',
25
+    images: ['https://picsum.photos/200/150?random=1'],
26
+    showReply: false,
27
+    replyContent: '',
28
+    replies: [
29
+      {
30
+        username: '系统管理员',
31
+        department: '管理部',
32
+        content: '已收到您的反馈,我们会加强现场卫生管理,感谢您的建议!',
33
+        createdAt: '2025-11-07 20:00:00',
34
+      },
35
+    ],
36
+  },
37
+  {
38
+    id: 2,
39
+    username: '张小明',
40
+    department: '管理部',
41
+    avatar: '',
42
+    rating: 4,
43
+    content: '整体不错,细节可以再优化一下',
44
+    createdAt: '2025-11-06 16:30:00',
45
+    images: [],
46
+    showReply: false,
47
+    replyContent: '',
48
+    replies: [],
49
+  },
50
+]);
51
+
52
+// 评分
53
+const rating = ref(5);
54
+// 评价内容
55
+const commentContent = ref('');
56
+// 抄送人
57
+const ccPerson = ref('');
58
+// 评论图片
59
+const commentImages = ref<any[]>([]);
60
+
61
+// 图片预览相关
62
+const previewVisible = ref(false);
63
+const previewImages = ref<string[]>([]);
64
+
65
+// 处理评论图片上传
66
+const handleCommentImageUpload = (options: any) => {
67
+  // 模拟上传成功处理
68
+  const fileData = {
69
+    uid: options.file.uid,
70
+    name: options.file.name,
71
+    url: URL.createObjectURL(options.file),
72
+    status: 'success',
73
+    raw: options.file,
74
+  };
75
+  // 直接通过v-model:file-list绑定,无需手动添加
76
+};
77
+
78
+// 处理评论图片移除
79
+const handleCommentImageRemove = (file: any, fileList: any[]) => {
80
+  commentImages.value = fileList;
81
+};
82
+
83
+// 预览评论图片
84
+const handleCommentImagePreview = (file: any) => {
85
+  console.log('预览图片', file);
86
+  // TODO: 实现图片预览功能
87
+};
88
+
89
+// 评论提交
90
+const handleCommentSubmit = () => {
91
+  if (!commentContent.value.trim() && commentImages.value.length === 0) {
92
+    ElMessage.warning('请输入评论内容或上传图片');
93
+    return;
94
+  }
95
+  console.log('提交评论', { rating: rating.value, content: commentContent.value, ccPerson: ccPerson.value, images: commentImages.value });
96
+};
97
+
98
+// 切换回复输入框
99
+const toggleReply = (comment: any) => {
100
+  comment.showReply = !comment.showReply;
101
+};
102
+
103
+// 取消回复
104
+const cancelReply = (comment: any) => {
105
+  comment.showReply = false;
106
+  comment.replyContent = '';
107
+};
108
+
109
+// 提交回复
110
+const submitReply = (comment: any) => {
111
+  if (!comment.replyContent.trim()) {
112
+    ElMessage.warning('请输入回复内容');
113
+    return;
114
+  }
115
+  
116
+  // 模拟提交回复
117
+  const newReply = {
118
+    username: '当前用户',
119
+    content: comment.replyContent,
120
+    createdAt: new Date().toLocaleString(),
121
+  };
122
+  
123
+  // 添加到回复列表
124
+  if (!comment.replies) {
125
+    comment.replies = [];
126
+  }
127
+  comment.replies.push(newReply);
128
+  
129
+  // 清空回复内容并隐藏回复框
130
+  comment.replyContent = '';
131
+  comment.showReply = false;
132
+  
133
+  ElMessage.success('回复提交成功');
134
+};
135
+
136
+// 图片预览
137
+const previewImage = (imageUrl: string) => {
138
+  previewImages.value = [imageUrl];
139
+  previewVisible.value = true;
140
+};
141
+
142
+// 关闭图片预览
143
+const closePreview = () => {
144
+  previewVisible.value = false;
145
+};
146
+
147
+// 获取评分等级
148
+const getRatingLevel = (rating: number) => {
149
+  return ratingLevelMap[rating as keyof typeof ratingLevelMap] || '';
150
+};
151
+</script>
152
+
153
+<template>
154
+  <div class="comment-container">
155
+    <!-- 评价表单 -->
156
+    <div class="comment-form">
157
+      <div class="form-header">
158
+        <div class="rating-section">
159
+          <span class="rating-text">总体评价:</span>
160
+          <ElRate v-model="rating" class="rating-stars" />
161
+        </div>
162
+      </div>
163
+      <div class="form-content">
164
+        <div class="input-section">
165
+          <!-- 使用ElUpload组件实现多图片上传 -->
166
+          <div class="upload-section">
167
+            <ElUpload
168
+              v-model:file-list="commentImages"
169
+              action="#"
170
+              accept=".jpg,.jpeg,.png,.gif"
171
+              list-type="picture-card"
172
+              show-file-list
173
+              :auto-upload="true"
174
+              :drag="false"
175
+              :limit="10"
176
+              :multiple="true"
177
+              :http-request="handleCommentImageUpload"
178
+              :on-remove="handleCommentImageRemove"
179
+              :on-preview="handleCommentImagePreview"
180
+              class="mb-2"
181
+            >
182
+              <div class="flex h-full w-full items-center justify-center">
183
+                <ElIcon class="el-upload__icon">
184
+                  <Plus size="18" />
185
+                </ElIcon>
186
+              </div>
187
+            </ElUpload>
188
+          </div>
189
+          <ElInput
190
+            v-model="commentContent"
191
+            type="textarea"
192
+            :rows="2"
193
+            placeholder="请对执行任务的情况进行点评,您的点评会帮助执行人更好的完善工作哦~"
194
+            class="comment-input"
195
+          />
196
+          <div class="form-footer">
197
+            <div class="footer-left">
198
+              <ElSelect v-model="ccPerson" placeholder="请选择抄送人" class="cc-select">
199
+                <ElOption label="测试抄送人" value="test" />
200
+              </ElSelect>
201
+              <ElButton type="primary" size="small" class="comment-btn" @click="handleCommentSubmit">
202
+                评论
203
+              </ElButton>
204
+            </div>
205
+          </div>
206
+        </div>
207
+      </div>
208
+    </div>
209
+
210
+    <!-- 评论列表 -->
211
+    <div class="comment-list">
212
+      <div v-for="comment in comments" :key="comment.id" class="comment-item">
213
+        <div class="comment-header">
214
+          <div class="username-section">
215
+            <span class="username">{{ comment.username }}</span>
216
+            <span class="department">({{ comment.department }})</span>
217
+            <div class="rating-section">
218
+              <ElRate v-model="comment.rating" disabled class="rating-stars" />
219
+              <span class="rating-level">{{ getRatingLevel(comment.rating) }}</span>
220
+            </div>
221
+          </div>
222
+        </div>
223
+        <div class="comment-content">
224
+          {{ comment.content }}
225
+        </div>
226
+        <!-- 评论图片 -->
227
+        <div v-if="comment.images && comment.images.length > 0" class="comment-images">
228
+          <img
229
+            v-for="(image, index) in comment.images"
230
+            :key="index"
231
+            :src="image"
232
+            alt="评论图片"
233
+            class="comment-image"
234
+            @click="previewImage(image)"
235
+          />
236
+        </div>
237
+        <!-- 评论时间 -->
238
+        <div class="comment-time">{{ comment.createdAt }}</div>
239
+        <!-- 评论操作 -->
240
+        <div class="comment-footer">
241
+          <div class="comment-actions">
242
+            <ElIcon class="reply-icon">
243
+              <Message class="h-4 w-4" />
244
+            </ElIcon>
245
+            <span class="reply-btn" @click="toggleReply(comment)">回复</span>
246
+          </div>
247
+        </div>
248
+        
249
+        <!-- 回复输入框 -->
250
+        <div v-if="comment.showReply" class="reply-form">
251
+          <ElInput
252
+            v-model="comment.replyContent"
253
+            type="textarea"
254
+            :rows="2"
255
+            placeholder="请输入回复内容"
256
+            class="reply-input"
257
+          />
258
+          <div class="reply-footer">
259
+            <ElButton type="default" size="small" @click="cancelReply(comment)">取消</ElButton>
260
+            <ElButton type="primary" size="small" @click="submitReply(comment)">提交</ElButton>
261
+          </div>
262
+        </div>
263
+        
264
+        <!-- 回复信息 -->
265
+        <div v-if="comment.replies && comment.replies.length > 0" class="reply-list">
266
+          <div v-for="(reply, index) in comment.replies" :key="index" class="reply-item">
267
+            <div class="reply-header">
268
+              <span class="reply-username">{{ reply.username }}</span>
269
+              <span v-if="reply.department" class="reply-department">({{ reply.department }})</span>
270
+            </div>
271
+            <div class="reply-content">{{ reply.content }}</div>
272
+            <div class="reply-time">{{ reply.createdAt }}</div>
273
+          </div>
274
+        </div>
275
+      </div>
276
+    </div>
277
+  </div>
278
+  
279
+  <!-- 图片预览组件 -->
280
+  <ElImageViewer
281
+    v-if="previewVisible"
282
+    :url-list="previewImages"
283
+    @close="closePreview"
284
+  />
285
+</template>
286
+
287
+<style scoped lang="scss">
288
+.comment-container {
289
+  width: 100%;
290
+  font-size: 14px;
291
+
292
+  .comment-form {
293
+    margin-bottom: 24px;
294
+    padding: 16px 0;
295
+    
296
+    .form-header {
297
+      margin-bottom: 12px;
298
+    }
299
+
300
+    .rating-section {
301
+      display: flex;
302
+      align-items: center;
303
+      
304
+      .rating-text {
305
+        color: var(--text-color-secondary);
306
+        font-size: 14px;
307
+        margin-right: 8px;
308
+      }
309
+      
310
+      .rating-stars {
311
+        margin-right: 0;
312
+      }
313
+    }
314
+
315
+    .form-content {
316
+      .input-section {
317
+        width: 100%;
318
+        
319
+        .upload-section {
320
+          margin-bottom: 12px;
321
+          
322
+          :deep(.el-upload--picture-card) {
323
+            width: 60px;
324
+            height: 60px;
325
+          }
326
+          
327
+          :deep(.el-upload-list--picture-card .el-upload-list__item) {
328
+            width: 60px;
329
+            height: 60px;
330
+          }
331
+        }
332
+        
333
+        .comment-input {
334
+          margin-bottom: 12px;
335
+          
336
+          :deep(.el-textarea__inner) {
337
+            border-radius: 4px;
338
+            resize: none;
339
+          }
340
+        }
341
+        
342
+        .form-footer {
343
+          .footer-left {
344
+            display: flex;
345
+            align-items: center;
346
+            
347
+            .cc-select {
348
+              width: 200px;
349
+              margin-right: 12px;
350
+            }
351
+            
352
+            .comment-btn {
353
+              padding: 4px 12px;
354
+            }
355
+          }
356
+        }
357
+      }
358
+    }
359
+  }
360
+
361
+  .comment-list {
362
+    .comment-item {
363
+      margin-bottom: 20px;
364
+      padding: 0;
365
+      background-color: transparent;
366
+      border: none;
367
+      font-size: 14px;
368
+
369
+      .comment-header {
370
+        margin-bottom: 8px;
371
+        
372
+        .username-section {
373
+          display: flex;
374
+          align-items: center;
375
+          
376
+          .username {
377
+            font-weight: 500;
378
+            color: var(--text-color-primary);
379
+            margin-right: 8px;
380
+          }
381
+          
382
+          .department {
383
+            color: var(--text-color-secondary);
384
+            font-size: 13px;
385
+            margin-right: 16px;
386
+          }
387
+          
388
+          .rating-section {
389
+            display: flex;
390
+            align-items: center;
391
+            
392
+            .rating-stars {
393
+              margin-right: 8px;
394
+            }
395
+            
396
+            .rating-level {
397
+              color: #f7ba2a;
398
+              font-size: 13px;
399
+            }
400
+          }
401
+        }
402
+      }
403
+
404
+      .comment-content {
405
+        color: var(--text-color-primary);
406
+        line-height: 1.5;
407
+        margin-bottom: 12px;
408
+        padding-right: 20px;
409
+      }
410
+
411
+      .comment-images {
412
+        display: flex;
413
+        gap: 12px;
414
+        margin-bottom: 12px;
415
+        
416
+        .comment-image {
417
+          width: 60px;
418
+          height: 60px;
419
+          border-radius: 4px;
420
+          object-fit: cover;
421
+          cursor: pointer;
422
+          transition: transform 0.2s;
423
+          
424
+          &:hover {
425
+            transform: scale(1.05);
426
+          }
427
+        }
428
+      }
429
+      
430
+      /* 评论时间样式 */
431
+      .comment-time {
432
+        font-size: 12px;
433
+        color: #999999;
434
+        margin-top: 8px;
435
+      }
436
+      
437
+      /* 评论操作样式 */
438
+      .comment-footer {
439
+        display: flex;
440
+        justify-content: flex-start;
441
+        align-items: center;
442
+        margin-top: 4px;
443
+        
444
+        .comment-actions {
445
+          display: flex;
446
+          align-items: center;
447
+          
448
+          .reply-icon {
449
+            margin-right: 4px;
450
+            color: #409eff;
451
+            font-size: 12px;
452
+          }
453
+          
454
+          .reply-btn {
455
+            color: #409eff;
456
+            font-size: 12px;
457
+            cursor: pointer;
458
+            
459
+            &:hover {
460
+              text-decoration: underline;
461
+            }
462
+          }
463
+        }
464
+      }
465
+      
466
+      /* 回复表单样式 */
467
+      .reply-form {
468
+        margin-top: 12px;
469
+        padding: 12px;
470
+        background-color: var(--bg-color-light);
471
+        border-radius: 4px;
472
+        border: 1px solid var(--border-color);
473
+        
474
+        .reply-input {
475
+          margin-bottom: 12px;
476
+          
477
+          :deep(.el-textarea__inner) {
478
+            border-radius: 4px;
479
+            resize: none;
480
+          }
481
+        }
482
+        
483
+        .reply-footer {
484
+          display: flex;
485
+          justify-content: flex-end;
486
+          gap: 8px;
487
+          
488
+          :deep(.el-button) {
489
+            padding: 4px 12px;
490
+          }
491
+        }
492
+      }
493
+      
494
+      /* 回复列表样式 */
495
+      .reply-list {
496
+        margin-top: 12px;
497
+        margin-left: 40px;
498
+        
499
+        .reply-item {
500
+          margin-bottom: 12px;
501
+          padding: 8px 12px;
502
+          background-color: var(--bg-color-light);
503
+          border-radius: 4px;
504
+          border-left: 3px solid #409eff;
505
+          
506
+          .reply-header {
507
+            margin-bottom: 4px;
508
+            
509
+            .reply-username {
510
+              font-weight: 500;
511
+              color: var(--text-color-primary);
512
+              font-size: 13px;
513
+              margin-right: 4px;
514
+            }
515
+            
516
+            .reply-department {
517
+              color: var(--text-color-secondary);
518
+              font-size: 13px;
519
+            }
520
+          }
521
+          
522
+          .reply-content {
523
+            color: var(--text-color-primary);
524
+            font-size: 13px;
525
+            line-height: 1.4;
526
+            margin-bottom: 4px;
527
+          }
528
+          
529
+          .reply-time {
530
+            font-size: 12px;
531
+            color: #999999;
532
+            text-align: left;
533
+          }
534
+        }
535
+      }
536
+    }
537
+  }
538
+}
539
+</style>

+ 104 - 0
apps/web-ele/src/views/schedule/detail/components/consult.vue

@@ -0,0 +1,104 @@
1
+<script lang="ts" setup>
2
+import { ref } from 'vue';
3
+import { ElTabs, ElTabPane } from 'element-plus';
4
+
5
+// 当前选中的tab
6
+const activeTab = ref('read');
7
+
8
+// 查阅统计数据
9
+const consultStats = ref({
10
+  readCount: 2,
11
+  unreadCount: 1,
12
+  readers: ['东冬', '世鹏'],
13
+  unreaders: ['张三']
14
+});
15
+
16
+// 处理tab切换
17
+const handleTabChange = (tab: string) => {
18
+  activeTab.value = tab;
19
+};
20
+
21
+// 获取名字的最后两个字
22
+const getLastTwoChars = (name: string) => {
23
+  return name.slice(-2);
24
+};
25
+</script>
26
+
27
+<template>
28
+  <div class="consult-container">
29
+    <!-- Tabs切换 -->
30
+    <ElTabs v-model="activeTab" @tab-change="handleTabChange" class="mb-4">
31
+      <ElTabPane 
32
+        :label="`${consultStats.readCount}人已阅`" 
33
+        name="read"
34
+      />
35
+      <ElTabPane 
36
+        :label="`${consultStats.unreadCount}人未阅`" 
37
+        name="unread"
38
+      />
39
+    </ElTabs>
40
+    
41
+    <!-- 人员列表 -->
42
+    <div v-if="activeTab === 'read'" class="readers-list">
43
+      <div 
44
+        v-for="(reader, index) in consultStats.readers" 
45
+        :key="index"
46
+        class="reader-item mr-2 mb-2"
47
+      >
48
+        {{ getLastTwoChars(reader) }}
49
+      </div>
50
+    </div>
51
+    
52
+    <div v-else class="readers-list">
53
+      <div 
54
+        v-for="(unreader, index) in consultStats.unreaders" 
55
+        :key="index"
56
+        class="reader-item mr-2 mb-2 unread"
57
+      >
58
+        {{ getLastTwoChars(unreader) }}
59
+      </div>
60
+    </div>
61
+  </div>
62
+</template>
63
+
64
+<style scoped lang="scss">
65
+.consult-container {
66
+  width: 100%;
67
+  padding: 16px 0;
68
+  font-size: 14px;
69
+  
70
+  .readers-list {
71
+    display: flex;
72
+    flex-wrap: wrap;
73
+  }
74
+  
75
+  .reader-item {
76
+    display: flex;
77
+    align-items: center;
78
+    justify-content: center;
79
+    width: 32px;
80
+    height: 32px;
81
+    border-radius: 50%;
82
+    background-color: #409eff;
83
+    color: white;
84
+    font-size: 12px;
85
+    font-weight: 500;
86
+    cursor: pointer;
87
+    transition: all 0.3s ease;
88
+    
89
+    &:hover {
90
+      background-color: #66b1ff;
91
+      transform: scale(1.1);
92
+    }
93
+    
94
+    &.unread {
95
+      background-color: #dcdfe6;
96
+      color: #606266;
97
+      
98
+      &:hover {
99
+        background-color: #e4e7ed;
100
+      }
101
+    }
102
+  }
103
+}
104
+</style>

+ 201 - 0
apps/web-ele/src/views/schedule/detail/index.vue

@@ -0,0 +1,201 @@
1
+<script lang="ts" setup>
2
+import { ref } from 'vue';
3
+
4
+import { Page } from '@vben/common-ui';
5
+
6
+import {
7
+  ElButton,
8
+  ElDescriptions,
9
+  ElDescriptionsItem,
10
+  ElTag,
11
+} from 'element-plus';
12
+
13
+import CheckComponent from './components/check.vue';
14
+// 引入检查项组件、评论组件和查阅组件
15
+import CommentComponent from './components/comment.vue';
16
+import ConsultComponent from './components/consult.vue';
17
+
18
+// 模拟任务数据
19
+const taskData = ref({
20
+  name: '未来路摄像头抓图AI检查',
21
+  status: '待处理',
22
+  priority: '必做',
23
+  description: '摄像头自动抓图进行AI检查',
24
+  standardGuide: '',
25
+  executor: '钉钉技术支持',
26
+  planDate: '2025-12-09',
27
+  taskTime: '12-09 00:00 ~ 12-09 23:00',
28
+  taskType: '运营',
29
+  taskOwner: '',
30
+  frequency: '每天一次',
31
+  creator: '系统',
32
+});
33
+
34
+// 状态标签配置
35
+const statusConfig: any = {
36
+  待处理: { type: 'info', text: '待处理' },
37
+  处理中: { type: 'warning', text: '处理中' },
38
+  已完成: { type: 'success', text: '已完成' },
39
+  已关闭: { type: 'danger', text: '已关闭' },
40
+};
41
+
42
+// 优先级标签配置
43
+const priorityConfig: any = {
44
+  必做: { type: 'danger', text: '必做' },
45
+  选做: { type: 'warning', text: '选做' },
46
+  常规: { type: 'info', text: '常规' },
47
+};
48
+
49
+// 关闭任务
50
+const handleCloseTask = () => {
51
+  console.log('关闭任务');
52
+};
53
+
54
+// 转交任务
55
+const handleTransferTask = () => {
56
+  console.log('转交任务');
57
+};
58
+</script>
59
+
60
+<template>
61
+  <Page title="">
62
+    <template #description>
63
+      <!-- 任务头部信息 -->
64
+      <div class="flex items-center justify-between">
65
+        <div class="flex items-center">
66
+          <div
67
+            class="task-icon mr-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-500"
68
+          >
69
+            <img src="/icon/4.png" alt="任务图标" class="h-6 w-6" />
70
+          </div>
71
+          <div>
72
+            <h2 class="mb-1 text-xl font-bold">{{ taskData.name }}</h2>
73
+            <div class="flex items-center space-x-2">
74
+              <ElTag :type="priorityConfig[taskData.priority]?.type || 'info'">
75
+                {{
76
+                  priorityConfig[taskData.priority]?.text || taskData.priority
77
+                }}
78
+              </ElTag>
79
+              <ElTag :type="statusConfig[taskData.status]?.type || 'info'">
80
+                {{ statusConfig[taskData.status]?.text || taskData.status }}
81
+              </ElTag>
82
+            </div>
83
+          </div>
84
+        </div>
85
+        <div class="flex items-center space-x-4">
86
+          <ElButton type="primary" @click="handleTransferTask">
87
+            转交任务
88
+          </ElButton>
89
+          <ElButton type="primary" @click="handleCloseTask">
90
+            关闭任务
91
+          </ElButton>
92
+        </div>
93
+      </div>
94
+    </template>
95
+    <ElCard>
96
+      <template #header>
97
+        <div class="flex items-center justify-between">
98
+          <span class="text-lg font-bold text-gray-800">任务信息</span>
99
+        </div>
100
+      </template>
101
+      <ElDescriptions class="task-info" :column="4">
102
+        <ElDescriptionsItem label="任务描述:" :span="4">
103
+          {{ taskData.description }}
104
+        </ElDescriptionsItem>
105
+        <ElDescriptionsItem label="标准指引:" :span="4">
106
+          {{ taskData.standardGuide || '-' }}
107
+        </ElDescriptionsItem>
108
+        <ElDescriptionsItem label="执行人:">
109
+          {{ taskData.executor }}
110
+        </ElDescriptionsItem>
111
+        <ElDescriptionsItem label="计划时间:">
112
+          {{ taskData.planDate }}
113
+        </ElDescriptionsItem>
114
+        <ElDescriptionsItem label="任务时间:">
115
+          {{ taskData.taskTime }}
116
+        </ElDescriptionsItem>
117
+        <ElDescriptionsItem label="任务类型:">
118
+          {{ taskData.taskType }}
119
+        </ElDescriptionsItem>
120
+        <ElDescriptionsItem label="任务负责人:">
121
+          {{ taskData.creator || '-' }}
122
+        </ElDescriptionsItem>
123
+        <ElDescriptionsItem label="任务频率:">
124
+          {{ taskData.frequency }}
125
+        </ElDescriptionsItem>
126
+        <ElDescriptionsItem label="创建:">
127
+          {{ taskData.creator }}
128
+        </ElDescriptionsItem>
129
+        <!-- <ElDescriptionsItem :span="2" class="text-right">
130
+          <template #default>
131
+            <ElButton type="text" class="text-gray-500">收起 ↑</ElButton>
132
+          </template>
133
+        </ElDescriptionsItem> -->
134
+      </ElDescriptions>
135
+    </ElCard>
136
+
137
+    <!-- 检查项组件 -->
138
+    <ElCard class="mt-4">
139
+      <template #header>
140
+        <div class="flex items-center justify-between">
141
+          <span class="text-lg font-bold text-gray-800">检查项</span>
142
+        </div>
143
+      </template>
144
+      <CheckComponent />
145
+    </ElCard>
146
+
147
+    <!-- 查阅组件 -->
148
+    <ElCard class="mt-4">
149
+      <template #header>
150
+        <div class="flex items-center justify-between">
151
+          <span class="text-lg font-bold text-gray-800">查阅</span>
152
+        </div>
153
+      </template>
154
+      <ConsultComponent />
155
+    </ElCard>
156
+
157
+    <!-- 评论组件 -->
158
+    <ElCard class="mt-4">
159
+      <template #header>
160
+        <div class="flex items-center justify-between">
161
+          <span class="text-lg font-bold text-gray-800">评论</span>
162
+        </div>
163
+      </template>
164
+      <CommentComponent />
165
+    </ElCard>
166
+  </Page>
167
+</template>
168
+
169
+<style scoped lang="scss">
170
+.task-detail-container {
171
+  width: 100%;
172
+  height: 100%;
173
+}
174
+
175
+.task-header {
176
+  display: flex;
177
+  align-items: center;
178
+  justify-content: space-between;
179
+}
180
+
181
+.task-icon {
182
+  img {
183
+    filter: invert(1);
184
+  }
185
+}
186
+
187
+.task-info {
188
+  :deep(.el-descriptions__label) {
189
+    font-weight: 500;
190
+    color: var(--text-color-secondary);
191
+  }
192
+
193
+  :deep(.el-descriptions__content) {
194
+    color: var(--text-color-primary);
195
+  }
196
+
197
+  // :deep(.el-descriptions-item__content) {
198
+  //   padding: 8px 0;
199
+  // }
200
+}
201
+</style>

+ 15 - 3
apps/web-ele/src/views/schedule/view/components/day/index.vue

@@ -1,4 +1,13 @@
1 1
 <script setup lang="ts">
2
+import { useRouter } from 'vue-router';
3
+
4
+const router = useRouter();
5
+
6
+// 跳转至任务详情页
7
+const handleTaskClick = (taskId: number) => {
8
+  router.push(`/schedule/detail/${taskId}`);
9
+};
10
+
2 11
 // 模拟数据
3 12
 const emergencyTasks = [
4 13
   {
@@ -182,7 +191,8 @@ const getIconEmoji = (icon: string) => {
182 191
       <div
183 192
         v-for="task in emergencyTasks"
184 193
         :key="task.id"
185
-        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
194
+        class="hover:border-primary flex cursor-pointer items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md"
195
+        @click="handleTaskClick(task.id)"
186 196
       >
187 197
         <div
188 198
           class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"
@@ -214,7 +224,8 @@ const getIconEmoji = (icon: string) => {
214 224
       <div
215 225
         v-for="task in todayTasks"
216 226
         :key="task.id"
217
-        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
227
+        class="hover:border-primary flex cursor-pointer items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md"
228
+        @click="handleTaskClick(task.id)"
218 229
       >
219 230
         <div
220 231
           class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"
@@ -243,7 +254,8 @@ const getIconEmoji = (icon: string) => {
243 254
       <div
244 255
         v-for="task in tomorrowTasks"
245 256
         :key="task.id"
246
-        class="flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
257
+        class="hover:border-primary flex cursor-pointer items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md"
258
+        @click="handleTaskClick(task.id)"
247 259
       >
248 260
         <div
249 261
           class="mr-4 flex h-10 w-10 items-center justify-center rounded-full"