浏览代码

first commit

wangxx 5 月之前
父节点
当前提交
b6b64ffaf9
共有 100 个文件被更改,包括 23946 次插入0 次删除
  1. 468 0
      src/views/attendance/attendanceRecord/index.vue
  2. 278 0
      src/views/attendance/checkRecord/index.vue
  3. 603 0
      src/views/attendance/postRecord/index.vue
  4. 299 0
      src/views/attendance/record/index.vue
  5. 334 0
      src/views/check/checkCorrection/components/Deatils.vue
  6. 137 0
      src/views/check/checkCorrection/index.vue
  7. 234 0
      src/views/check/checkRecord/components/Details.vue
  8. 390 0
      src/views/check/checkRecord/index.vue
  9. 522 0
      src/views/check/checkTask/index copy.vue
  10. 579 0
      src/views/check/checkTask/index.vue
  11. 116 0
      src/views/dataBigScreen/abilityPortrait/components/dataDisplay.vue
  12. 951 0
      src/views/dataBigScreen/abilityPortrait/index.vue
  13. 80 0
      src/views/dataBigScreen/attendance/AttendanceStatus.vue
  14. 90 0
      src/views/dataBigScreen/attendance/AttendanceStatusTime.vue
  15. 65 0
      src/views/dataBigScreen/attendance/AttendanceTrend.vue
  16. 78 0
      src/views/dataBigScreen/attendance/ContinuousAttendance.vue
  17. 68 0
      src/views/dataBigScreen/attendance/DepartmentAttendance.vue
  18. 50 0
      src/views/dataBigScreen/attendance/DepartmentDetails.vue
  19. 61 0
      src/views/dataBigScreen/attendance/LeaveType.vue
  20. 112 0
      src/views/dataBigScreen/attendance/NumberOfAbsences.vue
  21. 56 0
      src/views/dataBigScreen/attendance/OvertimeHours.vue
  22. 123 0
      src/views/dataBigScreen/attendance/TotalContainer.vue
  23. 118 0
      src/views/dataBigScreen/attendance/WorkingHoursAndSeizureVolume.vue
  24. 103 0
      src/views/dataBigScreen/attendance/index.vue
  25. 102 0
      src/views/dataBigScreen/dashboard-work/AbnormalSituationRanking.vue
  26. 114 0
      src/views/dataBigScreen/dashboard-work/AbnormalTeam.vue
  27. 130 0
      src/views/dataBigScreen/dashboard-work/DateTotal.vue
  28. 81 0
      src/views/dataBigScreen/dashboard-work/DeliberateConcealment.vue
  29. 78 0
      src/views/dataBigScreen/dashboard-work/DistributionOfSeizureResults.vue
  30. 88 0
      src/views/dataBigScreen/dashboard-work/InspectionStaff.vue
  31. 85 0
      src/views/dataBigScreen/dashboard-work/LineChart.vue
  32. 248 0
      src/views/dataBigScreen/dashboard-work/SamplingPartySituation.vue
  33. 170 0
      src/views/dataBigScreen/dashboard-work/SpotCheckContent.vue
  34. 242 0
      src/views/dataBigScreen/dashboard-work/WorkQuality.vue
  35. 81 0
      src/views/dataBigScreen/dashboard-work/index.vue
  36. 185 0
      src/views/dataBigScreen/dashboard/ChannelDistribution.vue
  37. 288 0
      src/views/dataBigScreen/dashboard/ClassificationOfSeizedQuantity.vue
  38. 156 0
      src/views/dataBigScreen/dashboard/DeliberateConcealment.vue
  39. 95 0
      src/views/dataBigScreen/dashboard/LineChart.vue
  40. 196 0
      src/views/dataBigScreen/dashboard/MajorContrabandCases.vue
  41. 86 0
      src/views/dataBigScreen/dashboard/PieChart.vue
  42. 152 0
      src/views/dataBigScreen/dashboard/PieRadiusChart.vue
  43. 227 0
      src/views/dataBigScreen/dashboard/SeizedRanking.vue
  44. 91 0
      src/views/dataBigScreen/dashboard/TopTitle.vue
  45. 97 0
      src/views/dataBigScreen/dashboard/components/DashboardContainer.vue
  46. 70 0
      src/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue
  47. 73 0
      src/views/dataBigScreen/dashboard/components/ScrollSeamless.vue
  48. 86 0
      src/views/dataBigScreen/dashboard/index.vue
  49. 149 0
      src/views/dataBigScreen/examQuestionStatistics/components/dataDisplay.vue
  50. 857 0
      src/views/dataBigScreen/examQuestionStatistics/index.vue
  51. 82 0
      src/views/error/401.vue
  52. 227 0
      src/views/error/404.vue
  53. 1131 0
      src/views/index.vue
  54. 584 0
      src/views/item/items/index.vue
  55. 430 0
      src/views/item/record/index.vue
  56. 229 0
      src/views/login.vue
  57. 132 0
      src/views/monitor/cache/index.vue
  58. 246 0
      src/views/monitor/cache/list.vue
  59. 13 0
      src/views/monitor/druid/index.vue
  60. 502 0
      src/views/monitor/job/index.vue
  61. 283 0
      src/views/monitor/job/log.vue
  62. 233 0
      src/views/monitor/logininfor/index.vue
  63. 109 0
      src/views/monitor/online/index.vue
  64. 310 0
      src/views/monitor/operlog/index.vue
  65. 187 0
      src/views/monitor/server/index.vue
  66. 14 0
      src/views/redirect/index.vue
  67. 220 0
      src/views/register.vue
  68. 272 0
      src/views/system/category/index.vue
  69. 276 0
      src/views/system/checkCategory/index.vue
  70. 316 0
      src/views/system/config/index.vue
  71. 314 0
      src/views/system/dept/index.vue
  72. 362 0
      src/views/system/dict/data.vue
  73. 323 0
      src/views/system/dict/index.vue
  74. 230 0
      src/views/system/generator/index.vue
  75. 338 0
      src/views/system/item/index.vue
  76. 452 0
      src/views/system/menu/index.vue
  77. 292 0
      src/views/system/notice/index.vue
  78. 280 0
      src/views/system/point/index.vue
  79. 366 0
      src/views/system/position/index.vue
  80. 285 0
      src/views/system/post/index.vue
  81. 367 0
      src/views/system/project/index.vue
  82. 179 0
      src/views/system/role/authUser.vue
  83. 584 0
      src/views/system/role/index.vue
  84. 144 0
      src/views/system/role/selectUser.vue
  85. 276 0
      src/views/system/sql/index.vue
  86. 123 0
      src/views/system/user/authRole.vue
  87. 252 0
      src/views/system/user/components/UserInfoEdit.vue
  88. 487 0
      src/views/system/user/index.vue
  89. 94 0
      src/views/system/user/profile/index.vue
  90. 59 0
      src/views/system/user/profile/resetPwd.vue
  91. 180 0
      src/views/system/user/profile/userAvatar.vue
  92. 67 0
      src/views/system/user/profile/userInfo.vue
  93. 71 0
      src/views/tool/build/CodeTypeDialog.vue
  94. 68 0
      src/views/tool/build/DraggableItem.vue
  95. 115 0
      src/views/tool/build/IconsDialog.vue
  96. 906 0
      src/views/tool/build/RightPanel.vue
  97. 93 0
      src/views/tool/build/TreeNodeDialog.vue
  98. 653 0
      src/views/tool/build/index.vue
  99. 48 0
      src/views/tool/gen/basicInfoForm.vue
  100. 0 0
      src/views/tool/gen/createTable.vue

+ 468 - 0
src/views/attendance/attendanceRecord/index.vue

@@ -0,0 +1,468 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="用户姓名" prop="userName">
5
+        <el-input
6
+          v-model="queryParams.userName"
7
+          placeholder="请输入用户姓名"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="班组名称" prop="teamName">
13
+        <el-input
14
+          v-model="queryParams.teamName"
15
+          placeholder="请输入班组名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item label="科室名称" prop="departmentName">
21
+        <el-input
22
+          v-model="queryParams.departmentName"
23
+          placeholder="请输入科室名称"
24
+          clearable
25
+          @keyup.enter="handleQuery"
26
+        />
27
+      </el-form-item>
28
+      <el-form-item label="考勤日期" prop="attendanceDate">
29
+        <el-date-picker clearable
30
+          v-model="queryParams.attendanceDateRange"
31
+          type="daterange"
32
+          value-format="YYYY-MM-DD"
33
+          start-placeholder="开始日期"
34
+          end-placeholder="结束日期">
35
+        </el-date-picker>
36
+      </el-form-item>
37
+      <el-form-item>
38
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
39
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
40
+      </el-form-item>
41
+    </el-form>
42
+
43
+    <el-row :gutter="10" class="mb8">
44
+      <el-col :span="1.5">
45
+        <el-button
46
+          type="primary"
47
+          plain
48
+          icon="Plus"
49
+          @click="handleAdd"
50
+          v-hasPermi="['attendance:attendanceRecord:add']"
51
+        >新增</el-button>
52
+      </el-col>
53
+      <!-- <el-col :span="1.5">
54
+        <el-button
55
+          type="success"
56
+          plain
57
+          icon="Edit"
58
+          :disabled="single"
59
+          @click="handleUpdate"
60
+          v-hasPermi="['attendance:attendanceRecord:edit']"
61
+        >修改</el-button>
62
+      </el-col> -->
63
+      <el-col :span="1.5">
64
+        <el-button
65
+          type="danger"
66
+          plain
67
+          icon="Delete"
68
+          :disabled="multiple"
69
+          @click="handleDelete"
70
+          v-hasPermi="['attendance:attendanceRecord:remove']"
71
+        >删除</el-button>
72
+      </el-col>
73
+      <el-col :span="1.5">
74
+        <el-button
75
+          type="warning"
76
+          plain
77
+          icon="Download"
78
+          @click="handleExport"
79
+          v-hasPermi="['attendance:attendanceRecord:export']"
80
+        >导出</el-button>
81
+      </el-col>
82
+      <el-col :span="1.5">
83
+        <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['attendance:attendanceRecord:import']">导入</el-button>
84
+      </el-col>
85
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
86
+    </el-row>
87
+
88
+    <el-table v-loading="loading" :data="attendanceRecordList" @selection-change="handleSelectionChange">
89
+      <el-table-column type="selection" width="55" align="center" />
90
+      <el-table-column label="用户名称" align="center" prop="userName" />
91
+      <el-table-column label="科室名称" align="center" prop="departmentName" />
92
+      <el-table-column label="班组名称" align="center" prop="teamName" />
93
+      <el-table-column label="考勤日期" align="center" prop="attendanceDate" width="180" />
94
+      <el-table-column label="签到时间" align="center" prop="checkInTime" width="180" />
95
+      <el-table-column label="签退时间" align="center" prop="checkOutTime" width="180" />
96
+      <el-table-column label="工作时长(分钟)" align="center" prop="workDuration" />
97
+      <el-table-column label="备注" align="center" prop="remark" />
98
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
99
+        <template #default="scope">
100
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['attendance:attendanceRecord:edit']">修改</el-button>
101
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['attendance:attendanceRecord:remove']">删除</el-button>
102
+        </template>
103
+      </el-table-column>
104
+    </el-table>
105
+    
106
+    <pagination
107
+      v-show="total>0"
108
+      :total="total"
109
+      v-model:page="queryParams.pageNum"
110
+      v-model:limit="queryParams.pageSize"
111
+      @pagination="getList"
112
+    />
113
+
114
+    <!-- 添加或修改考勤记录对话框 -->
115
+    <el-dialog :title="title" v-model="open" width="400px" append-to-body>
116
+      <el-form ref="attendanceRecordRef" :model="form" :rules="rules" label-width="80px">
117
+        <el-form-item label="用户" prop="userId">
118
+          <template v-if="disabledUserSelect">
119
+            <el-input v-model="form.userName" :disabled="disabledUserSelect" />
120
+          </template>
121
+          <template v-else>
122
+            <UserSelect ref="userSelect" v-model="form.userId" :disabled="disabledUserSelect"  width="288px" />
123
+          </template>
124
+        </el-form-item>
125
+        <el-form-item label="签到时间" prop="checkInTime">
126
+          <el-date-picker clearable
127
+            style="width:288px"
128
+            v-model="form.checkInTime"
129
+            type="datetime"
130
+            format="YYYY-MM-DD HH:mm:ss"
131
+            value-format="YYYY-MM-DD HH:mm:ss"
132
+            placeholder="请选择签到时间">
133
+          </el-date-picker>
134
+        </el-form-item>
135
+        <el-form-item label="签退时间" prop="checkOutTime">
136
+          <el-date-picker clearable
137
+            style="width:288px"
138
+            v-model="form.checkOutTime"
139
+            type="datetime"
140
+            format="YYYY-MM-DD HH:mm:ss"
141
+            value-format="YYYY-MM-DD HH:mm:ss"
142
+            placeholder="请选择签退时间">
143
+          </el-date-picker>
144
+        </el-form-item>
145
+        <el-form-item label="备注" prop="remark">
146
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
147
+        </el-form-item>
148
+      </el-form>
149
+      <template #footer>
150
+        <div class="dialog-footer">
151
+          <el-button type="primary" @click="submitForm">确 定</el-button>
152
+          <el-button @click="cancel">取 消</el-button>
153
+        </div>
154
+      </template>
155
+    </el-dialog>
156
+
157
+    <!-- 考勤记录导入对话框 -->
158
+    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
159
+      <el-upload ref="uploadRef" 
160
+      :limit="1" accept=".xlsx, .xls" 
161
+      :headers="upload.headers" 
162
+      :action="upload.url + '?updateSupport=' + upload.updateSupport" 
163
+      :disabled="upload.isUploading" 
164
+      :on-progress="handleFileUploadProgress" 
165
+      :on-success="handleFileSuccess" 
166
+      :auto-upload="false" drag>
167
+        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
168
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
169
+        <template #tip>
170
+          <div class="el-upload__tip text-center">
171
+            <!-- <div class="el-upload__tip">
172
+              <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
173
+            </div> -->
174
+            <span>仅允许导入xls、xlsx格式文件。</span>
175
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
176
+          </div>
177
+        </template>
178
+      </el-upload>
179
+      <template #footer>
180
+        <div class="dialog-footer">
181
+          <el-button type="primary" @click="submitFileForm">确 定</el-button>
182
+          <el-button @click="upload.open = false">取 消</el-button>
183
+        </div>
184
+      </template>
185
+    </el-dialog>
186
+  </div>
187
+</template>
188
+
189
+<script setup name="AttendanceRecord">
190
+import { listAttendanceRecord, getAttendanceRecord, delAttendanceRecord, addAttendanceRecord, updateAttendanceRecord } from "@/api/attendance/attendanceRecord"
191
+import UserSelect from "@/components/UserSelect"
192
+
193
+const { proxy } = getCurrentInstance()
194
+
195
+const attendanceRecordList = ref([])
196
+const open = ref(false)
197
+const loading = ref(true)
198
+const showSearch = ref(true)
199
+const ids = ref([])
200
+const single = ref(true)
201
+const multiple = ref(true)
202
+const total = ref(0)
203
+const title = ref("")
204
+// 控制 UserSelect 组件是否可编辑
205
+const disabledUserSelect = ref(false)
206
+
207
+import { getToken } from "@/utils/auth"
208
+
209
+const data = reactive({
210
+  form: {},
211
+  queryParams: {
212
+    pageNum: 1,
213
+    pageSize: 10,
214
+    tenantId: null,
215
+    revision: null,
216
+    userId: null,
217
+    checkInTime: null,
218
+    checkOutTime: null,
219
+    workDuration: null,
220
+    status: null,
221
+    teamCode: null,
222
+    teamName: null,
223
+    departmentCode: null,
224
+    departmentName: null,
225
+    stationCode: null,
226
+    stationName: null,
227
+    userName: null,
228
+    remark: null,
229
+    attendanceDate: null,
230
+    overDuration: null,
231
+    statusDesc: null,
232
+    attendanceDateRange: [],
233
+    attendanceDateStart: null,
234
+    attendanceDateEnd: null,
235
+  },
236
+  rules: {
237
+    userId: [
238
+      { required: !disabledUserSelect, message: "用户不能为空", trigger: "blur" }
239
+    ],
240
+    checkInTime: [
241
+      { required: true, message: "签到时间不能为空", trigger: "blur" }
242
+    ],
243
+    checkOutTime: [
244
+      { required: true, message: "签退时间不能为空", trigger: "blur" }
245
+    ],
246
+    workDuration: [
247
+      { required: true, message: "工作时长(分钟)不能为空", trigger: "blur" }
248
+    ],
249
+    status: [
250
+      { required: true, message: "考勤状态不能为空", trigger: "change" }
251
+    ],
252
+    teamCode: [
253
+      { required: true, message: "班组编码不能为空", trigger: "blur" }
254
+    ],
255
+    teamName: [
256
+      { required: true, message: "班组名称不能为空", trigger: "blur" }
257
+    ],
258
+    departmentCode: [
259
+      { required: true, message: "科室编码不能为空", trigger: "blur" }
260
+    ],
261
+    departmentName: [
262
+      { required: true, message: "科室名称不能为空", trigger: "blur" }
263
+    ],
264
+    stationCode: [
265
+      { required: true, message: "机构站编码不能为空", trigger: "blur" }
266
+    ],
267
+    stationName: [
268
+      { required: true, message: "机构站名称不能为空", trigger: "blur" }
269
+    ],
270
+    userName: [
271
+      { required: true, message: "用户名称不能为空", trigger: "blur" }
272
+    ],
273
+    attendanceDate: [
274
+      { required: true, message: "考勤日期不能为空", trigger: "blur" }
275
+    ],
276
+    overDuration: [
277
+      { required: true, message: "加班时长(分钟)不能为空", trigger: "blur" }
278
+    ],
279
+    statusDesc: [
280
+      { required: true, message: "考勤状态名称不能为空", trigger: "blur" }
281
+    ]
282
+  }
283
+})
284
+
285
+const upload = reactive({
286
+  // 是否显示弹出层
287
+  open: false,
288
+  // 弹出层标题
289
+  title: "",
290
+  // 是否禁用上传
291
+  isUploading: false,
292
+  // 是否更新已经存在的用户数据
293
+  updateSupport: 0,
294
+  // 设置上传的请求头部
295
+  headers: { Authorization: "Bearer " + getToken() },
296
+  // 上传的地址
297
+  url: import.meta.env.VITE_APP_BASE_API + "/attendance/attendanceRecord/importData"
298
+})
299
+
300
+const { queryParams, form, rules } = toRefs(data)
301
+
302
+/** 查询考勤记录列表 */
303
+function getList() {
304
+  loading.value = true
305
+  const searchValue = queryParams.value
306
+  queryParams.value.attendanceDateStart = searchValue.attendanceDateRange[0];
307
+  queryParams.value.attendanceDateEnd = searchValue.attendanceDateRange[1];
308
+  console.log('attendanceDateRange', queryParams.value)
309
+  listAttendanceRecord(queryParams.value).then(response => {
310
+    attendanceRecordList.value = response.rows
311
+    total.value = response.total
312
+    loading.value = false
313
+  })
314
+}
315
+
316
+// 取消按钮
317
+function cancel() {
318
+  open.value = false
319
+  reset()
320
+}
321
+
322
+// 表单重置
323
+function reset() {
324
+  form.value = {
325
+    tenantId: null,
326
+    revision: null,
327
+    createBy: null,
328
+    createTime: null,
329
+    updateBy: null,
330
+    updateTime: null,
331
+    id: null,
332
+    userId: null,
333
+    checkInTime: null,
334
+    checkOutTime: null,
335
+    workDuration: null,
336
+    status: null,
337
+    teamCode: null,
338
+    teamName: null,
339
+    departmentCode: null,
340
+    departmentName: null,
341
+    stationCode: null,
342
+    stationName: null,
343
+    userName: null,
344
+    remark: null,
345
+    attendanceDate: null,
346
+    overDuration: null,
347
+    statusDesc: null,
348
+    attendanceDateRange: [],
349
+    attendanceDateStart: null,
350
+    attendanceDateEnd: null,
351
+  }
352
+  proxy.resetForm("attendanceRecordRef")
353
+}
354
+
355
+/** 搜索按钮操作 */
356
+function handleQuery() {
357
+  queryParams.value.pageNum = 1
358
+  getList()
359
+}
360
+
361
+/** 重置按钮操作 */
362
+function resetQuery() {
363
+  queryParams.value.attendanceDateRange = [] 
364
+  proxy.resetForm("queryRef")
365
+  handleQuery()
366
+}
367
+
368
+// 多选框选中数据
369
+function handleSelectionChange(selection) {
370
+  ids.value = selection.map(item => item.id)
371
+  single.value = selection.length != 1
372
+  multiple.value = !selection.length
373
+}
374
+
375
+/** 新增按钮操作 */
376
+function handleAdd() {
377
+  reset()
378
+  open.value = true
379
+  title.value = "添加考勤记录"
380
+  // 新增时允许编辑
381
+  disabledUserSelect.value = false
382
+}
383
+
384
+/** 修改按钮操作 */
385
+function handleUpdate(row) {
386
+  reset()
387
+  const _id = row.id || ids.value
388
+  getAttendanceRecord(_id).then(response => {
389
+    form.value = response.data
390
+    open.value = true
391
+    title.value = "修改考勤记录"
392
+    // 修改时禁止编辑
393
+    disabledUserSelect.value = true
394
+  })
395
+}
396
+
397
+/** 提交按钮 */
398
+function submitForm() {
399
+  proxy.$refs["attendanceRecordRef"].validate(valid => {
400
+    if (valid) {
401
+      if (form.value.id != null) {
402
+        updateAttendanceRecord(form.value).then(response => {
403
+          proxy.$modal.msgSuccess("修改成功")
404
+          open.value = false
405
+          getList()
406
+        })
407
+      } else {
408
+        addAttendanceRecord(form.value).then(response => {
409
+          proxy.$modal.msgSuccess("新增成功")
410
+          open.value = false
411
+          getList()
412
+        })
413
+      }
414
+    }
415
+  })
416
+}
417
+
418
+/** 删除按钮操作 */
419
+function handleDelete(row) {
420
+  const _ids = row.id || ids.value
421
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
422
+    return delAttendanceRecord(_ids)
423
+  }).then(() => {
424
+    getList()
425
+    proxy.$modal.msgSuccess("删除成功")
426
+  }).catch(() => {})
427
+}
428
+
429
+/** 导出按钮操作 */
430
+function handleExport() {
431
+  proxy.download('attendance/attendanceRecord/export', {
432
+    ...queryParams.value
433
+  }, `attendanceRecord_${new Date().getTime()}.xlsx`)
434
+}
435
+
436
+getList()
437
+
438
+/** 导入按钮操作 */
439
+function handleImport() {
440
+  upload.title = "考勤记录导入"
441
+  upload.open = true
442
+}
443
+
444
+/** 下载模板操作 */
445
+function importTemplate() {
446
+  proxy.download("attendance/attendanceRecord/importTemplate", {
447
+  }, `attendance_template_${new Date().getTime()}.xlsx`)
448
+}
449
+
450
+/**文件上传中处理 */
451
+const handleFileUploadProgress = (event, file, fileList) => {
452
+  upload.isUploading = true
453
+}
454
+
455
+/** 文件上传成功处理 */
456
+const handleFileSuccess = (response, file, fileList) => {
457
+  upload.open = false
458
+  upload.isUploading = false
459
+  proxy.$refs["uploadRef"].handleRemove(file)
460
+  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
461
+  getList()
462
+}
463
+
464
+/** 提交上传文件 */
465
+function submitFileForm() {
466
+  proxy.$refs["uploadRef"].submit()
467
+}
468
+</script>

+ 278 - 0
src/views/attendance/checkRecord/index.vue

@@ -0,0 +1,278 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
4
+      <el-form-item label="打卡人姓名" prop="userName">
5
+        <el-input
6
+          v-model="queryParams.userName"
7
+          placeholder="请输入打卡人姓名"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="打卡日期">
13
+        <el-date-picker
14
+          clearable
15
+          v-model="dateRange"
16
+          value-format="YYYY-MM-DD"
17
+          type="daterange"
18
+          range-separator="-"
19
+          start-placeholder="开始日期"
20
+          end-placeholder="结束日期">
21
+        </el-date-picker>
22
+      </el-form-item>
23
+      <el-form-item label="打卡类型" prop="checkInType">
24
+        <el-select
25
+               v-model="queryParams.checkInType"
26
+               placeholder="打卡类型"
27
+               clearable
28
+               style="width: 240px"
29
+            >
30
+               <el-option
31
+                  v-for="dict in attendance_check_in_type"
32
+                  :key="dict.value"
33
+                  :label="dict.label"
34
+                  :value="dict.value"
35
+               />
36
+            </el-select>
37
+      </el-form-item>
38
+      <el-form-item>
39
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
40
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
41
+      </el-form-item>
42
+    </el-form>
43
+
44
+    <el-row :gutter="10" class="mb8">
45
+      <el-col :span="1.5">
46
+        <el-button
47
+          type="warning"
48
+          plain
49
+          icon="Download"
50
+          @click="handleExport"
51
+          v-hasPermi="['attendance:checkRecord:export']"
52
+        >导出</el-button>
53
+      </el-col>
54
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
55
+    </el-row>
56
+
57
+    <el-table v-loading="loading" :data="checkRecordList" @selection-change="handleSelectionChange">
58
+      <el-table-column label="打卡人姓名" align="center" prop="userName" />
59
+      <el-table-column label="打卡类型" align="center" prop="checkInTypeDesc" />
60
+      <el-table-column label="打卡时间" align="center" prop="checkInTime" />
61
+      <el-table-column label="备注" align="center" prop="remark" />
62
+    </el-table>
63
+    
64
+    <pagination
65
+      v-show="total>0"
66
+      :total="total"
67
+      v-model:page="queryParams.pageNum"
68
+      v-model:limit="queryParams.pageSize"
69
+      @pagination="getList"
70
+    />
71
+
72
+    <!-- 添加或修改打卡记录对话框 -->
73
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
74
+      <el-form ref="checkRecordRef" :model="form" :rules="rules" label-width="80px">
75
+        <el-form-item label="租户号" prop="tenantId">
76
+          <el-input v-model="form.tenantId" placeholder="请输入租户号" />
77
+        </el-form-item>
78
+        <el-form-item label="乐观锁" prop="revision">
79
+          <el-input v-model="form.revision" placeholder="请输入乐观锁" />
80
+        </el-form-item>
81
+        <el-form-item label="打卡人ID" prop="userId">
82
+          <el-input v-model="form.userId" placeholder="请输入打卡人ID" />
83
+        </el-form-item>
84
+        <el-form-item label="打卡时间" prop="checkInTime">
85
+          <el-date-picker clearable
86
+            v-model="form.checkInTime"
87
+            type="date"
88
+            value-format="YYYY-MM-DD"
89
+            placeholder="请选择打卡时间">
90
+          </el-date-picker>
91
+        </el-form-item>
92
+        <el-form-item label="打卡人名称" prop="userName">
93
+          <el-input v-model="form.userName" placeholder="请输入打卡人名称" />
94
+        </el-form-item>
95
+        <el-form-item label="备注" prop="remark">
96
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
97
+        </el-form-item>
98
+        <el-form-item label="打卡类型名称" prop="checkInTypeDesc">
99
+          <el-input v-model="form.checkInTypeDesc" placeholder="请输入打卡类型名称" />
100
+        </el-form-item>
101
+      </el-form>
102
+      <template #footer>
103
+        <div class="dialog-footer">
104
+          <el-button type="primary" @click="submitForm">确 定</el-button>
105
+          <el-button @click="cancel">取 消</el-button>
106
+        </div>
107
+      </template>
108
+    </el-dialog>
109
+  </div>
110
+</template>
111
+
112
+<script setup name="CheckRecord">
113
+import { listCheckRecord, getCheckRecord, delCheckRecord, addCheckRecord, updateCheckRecord } from "@/api/attendance/checkRecord"
114
+
115
+const { proxy } = getCurrentInstance()
116
+
117
+const checkRecordList = ref([])
118
+const open = ref(false)
119
+const loading = ref(true)
120
+const showSearch = ref(true)
121
+const ids = ref([])
122
+const single = ref(true)
123
+const multiple = ref(true)
124
+const total = ref(0)
125
+const title = ref("")
126
+const dateRange = ref([])
127
+const { attendance_check_in_type } = proxy.useDict("attendance_check_in_type")
128
+
129
+const data = reactive({
130
+  form: {},
131
+  queryParams: {
132
+    pageNum: 1,
133
+    pageSize: 10,
134
+    tenantId: null,
135
+    revision: null,
136
+    userId: null,
137
+    checkInType: null,
138
+    checkInTime: null,
139
+    userName: null,
140
+    remark: null,
141
+    checkInTypeDesc: null
142
+  },
143
+  rules: {
144
+    userId: [
145
+      { required: true, message: "打卡人ID不能为空", trigger: "blur" }
146
+    ],
147
+    checkInType: [
148
+      { required: true, message: "打卡类型不能为空", trigger: "change" }
149
+    ],
150
+    checkInTime: [
151
+      { required: true, message: "打卡时间不能为空", trigger: "blur" }
152
+    ],
153
+    userName: [
154
+      { required: true, message: "打卡人名称不能为空", trigger: "blur" }
155
+    ],
156
+    checkInTypeDesc: [
157
+      { required: true, message: "打卡类型名称不能为空", trigger: "blur" }
158
+    ]
159
+  }
160
+})
161
+
162
+const { queryParams, form, rules } = toRefs(data)
163
+
164
+/** 查询打卡记录列表 */
165
+function getList() {
166
+  loading.value = true
167
+  listCheckRecord(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
168
+    checkRecordList.value = response.rows
169
+    total.value = response.total
170
+    loading.value = false
171
+  })
172
+}
173
+
174
+// 取消按钮
175
+function cancel() {
176
+  open.value = false
177
+  reset()
178
+}
179
+
180
+// 表单重置
181
+function reset() {
182
+  form.value = {
183
+    tenantId: null,
184
+    revision: null,
185
+    createBy: null,
186
+    createTime: null,
187
+    updateBy: null,
188
+    updateTime: null,
189
+    id: null,
190
+    userId: null,
191
+    checkInType: null,
192
+    checkInTime: null,
193
+    userName: null,
194
+    remark: null,
195
+    checkInTypeDesc: null
196
+  }
197
+  proxy.resetForm("checkRecordRef")
198
+}
199
+
200
+/** 搜索按钮操作 */
201
+function handleQuery() {
202
+  queryParams.value.pageNum = 1
203
+  getList()
204
+}
205
+
206
+/** 重置按钮操作 */
207
+function resetQuery() {
208
+  dateRange.value = []
209
+  proxy.resetForm("queryRef")
210
+  handleQuery()
211
+}
212
+
213
+// 多选框选中数据
214
+function handleSelectionChange(selection) {
215
+  ids.value = selection.map(item => item.id)
216
+  single.value = selection.length != 1
217
+  multiple.value = !selection.length
218
+}
219
+
220
+/** 新增按钮操作 */
221
+function handleAdd() {
222
+  reset()
223
+  open.value = true
224
+  title.value = "添加打卡记录"
225
+}
226
+
227
+/** 修改按钮操作 */
228
+function handleUpdate(row) {
229
+  reset()
230
+  const _id = row.id || ids.value
231
+  getCheckRecord(_id).then(response => {
232
+    form.value = response.data
233
+    open.value = true
234
+    title.value = "修改打卡记录"
235
+  })
236
+}
237
+
238
+/** 提交按钮 */
239
+function submitForm() {
240
+  proxy.$refs["checkRecordRef"].validate(valid => {
241
+    if (valid) {
242
+      if (form.value.id != null) {
243
+        updateCheckRecord(form.value).then(response => {
244
+          proxy.$modal.msgSuccess("修改成功")
245
+          open.value = false
246
+          getList()
247
+        })
248
+      } else {
249
+        addCheckRecord(form.value).then(response => {
250
+          proxy.$modal.msgSuccess("新增成功")
251
+          open.value = false
252
+          getList()
253
+        })
254
+      }
255
+    }
256
+  })
257
+}
258
+
259
+/** 删除按钮操作 */
260
+function handleDelete(row) {
261
+  const _ids = row.id || ids.value
262
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
263
+    return delCheckRecord(_ids)
264
+  }).then(() => {
265
+    getList()
266
+    proxy.$modal.msgSuccess("删除成功")
267
+  }).catch(() => {})
268
+}
269
+
270
+/** 导出按钮操作 */
271
+function handleExport() {
272
+  proxy.download('attendance/checkRecord/export', {
273
+    ...queryParams.value
274
+  }, `checkRecord_${new Date().getTime()}.xlsx`)
275
+}
276
+
277
+getList()
278
+</script>

+ 603 - 0
src/views/attendance/postRecord/index.vue

@@ -0,0 +1,603 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+      <el-form-item label="用户名称" prop="userName">
5
+        <el-input
6
+          v-model="queryParams.userName"
7
+          placeholder="请输入用户名称"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="班组名称" prop="attendanceTeamName">
13
+        <el-input
14
+          v-model="queryParams.attendanceTeamName"
15
+          placeholder="请输入班组名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item label="科室名称" prop="attendanceDepartmentName">
21
+        <el-input
22
+          v-model="queryParams.attendanceDepartmentName"
23
+          placeholder="请输入科室名称"
24
+          clearable
25
+          @keyup.enter="handleQuery"
26
+        />
27
+      </el-form-item>
28
+      <el-form-item label="考勤日期" prop="attendanceDate">
29
+        <el-date-picker
30
+          clearable
31
+          v-model="dateRange"
32
+          value-format="YYYY-MM-DD"
33
+          type="daterange"
34
+          range-separator="-"
35
+          start-placeholder="开始日期"
36
+          end-placeholder="结束日期">
37
+        </el-date-picker>
38
+      </el-form-item>
39
+      <el-form-item label="航站楼名称" prop="terminlName">
40
+        <el-input
41
+          v-model="queryParams.terminlName"
42
+          placeholder="请输入航站楼名称"
43
+          clearable
44
+          @keyup.enter="handleQuery"
45
+        />
46
+      </el-form-item>
47
+      <el-form-item label="区域名称" prop="regionalName">
48
+        <el-input
49
+          v-model="queryParams.regionalName"
50
+          placeholder="请输入区域名称"
51
+          clearable
52
+          @keyup.enter="handleQuery"
53
+        />
54
+      </el-form-item>
55
+      <el-form-item label="通道名称" prop="channelName">
56
+        <el-input
57
+          v-model="queryParams.channelName"
58
+          placeholder="请输入通道名称"
59
+          clearable
60
+          @keyup.enter="handleQuery"
61
+        />
62
+      </el-form-item>
63
+      <el-form-item label="岗位名称" prop="positionName">
64
+        <el-input
65
+          v-model="queryParams.positionName"
66
+          placeholder="请输入岗位名称"
67
+          clearable
68
+          @keyup.enter="handleQuery"
69
+        />
70
+      </el-form-item>
71
+      <el-form-item>
72
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
73
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
74
+      </el-form-item>
75
+    </el-form>
76
+
77
+    <el-row :gutter="10" class="mb8">
78
+      <el-col :span="1.5">
79
+        <el-button
80
+          type="primary"
81
+          plain
82
+          icon="Plus"
83
+          @click="handleAdd"
84
+          v-hasPermi="['attendance:postRecord:add']"
85
+        >新增</el-button>
86
+      </el-col>
87
+      <el-col :span="1.5">
88
+        <el-button
89
+          type="danger"
90
+          plain
91
+          icon="Delete"
92
+          :disabled="multiple"
93
+          @click="handleDelete"
94
+          v-hasPermi="['attendance:postRecord:remove']"
95
+        >删除</el-button>
96
+      </el-col>
97
+      <el-col :span="1.5">
98
+        <el-button
99
+          type="warning"
100
+          plain
101
+          icon="Download"
102
+          @click="handleExport"
103
+          v-hasPermi="['attendance:postRecord:export']"
104
+        >导出</el-button>
105
+      </el-col>
106
+      <el-col :span="1.5">
107
+        <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['attendance:postRecord:import']">导入</el-button>
108
+      </el-col>
109
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
110
+    </el-row>
111
+
112
+    <el-table v-loading="loading" :data="postRecordList" @selection-change="handleSelectionChange">
113
+      <el-table-column type="selection" width="55" align="center" />
114
+      <el-table-column label="用户姓名" align="center" prop="userName" width="80" />
115
+      <el-table-column label="班组名称" align="center" prop="attendanceTeamName" width="80" />
116
+      <el-table-column label="科室名称" align="center" prop="attendanceDepartmentName" width="80" />
117
+      <el-table-column label="考勤日期" align="center" prop="attendanceDate" width="100" />
118
+      <el-table-column label="上岗时间" align="center" prop="checkInTime" width="155" />
119
+      <el-table-column label="离岗时间" align="center" prop="checkOutTime" width="155" />
120
+      <el-table-column label="工作时长(分钟)" align="center" prop="workDuration" width="115" />
121
+      <el-table-column label="航站楼名称" align="center" prop="terminlName" width="110" />
122
+      <el-table-column label="区域名称" align="center" prop="regionalName" width="110" />
123
+      <el-table-column label="通道名称" align="center" prop="channelName" width="110" />
124
+      <el-table-column label="岗位名称" align="center" prop="positionName" width="110" />
125
+      <el-table-column label="备注" align="center" prop="remark" />
126
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
127
+        <template #default="scope">
128
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['attendance:postRecord:edit']">修改</el-button>
129
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['attendance:postRecord:remove']">删除</el-button>
130
+        </template>
131
+      </el-table-column>
132
+    </el-table>
133
+    
134
+    <pagination
135
+      v-show="total>0"
136
+      :total="total"
137
+      v-model:page="queryParams.pageNum"
138
+      v-model:limit="queryParams.pageSize"
139
+      @pagination="getList"
140
+    />
141
+
142
+    <!-- 添加或修改上岗记录对话框 -->
143
+    <el-dialog :title="title" v-model="open" width="400px" append-to-body>
144
+      <el-form ref="postRecordRef" :model="form" :rules="rules" label-width="80px">
145
+        <el-form-item label="用户" prop="userId">
146
+          <template v-if="disabledUserSelect">
147
+            <el-input v-model="form.userName" :disabled="disabledUserSelect" />
148
+          </template>
149
+          <template v-else>
150
+            <UserSelect ref="userSelect" v-model="form.userId" :disabled="disabledUserSelect" width="288px" />
151
+          </template>
152
+        </el-form-item>
153
+        <el-form-item label="上岗时间" prop="checkInTime">
154
+          <el-date-picker clearable
155
+            style="width:288px"
156
+            v-model="form.checkInTime"
157
+            type="datetime"
158
+            format="YYYY-MM-DD HH:mm:ss"
159
+            value-format="YYYY-MM-DD HH:mm:ss"
160
+            placeholder="请选择上岗时间">
161
+          </el-date-picker>
162
+        </el-form-item>
163
+        <el-form-item label="离岗时间" prop="checkOutTime">
164
+          <el-date-picker clearable
165
+            style="width:288px"
166
+            v-model="form.checkOutTime"
167
+            type="datetime"
168
+            format="YYYY-MM-DD HH:mm:ss"
169
+            value-format="YYYY-MM-DD HH:mm:ss"
170
+            placeholder="请选择离岗时间">
171
+          </el-date-picker>
172
+        </el-form-item>
173
+        <el-form-item label="班组" prop="attendanceTeamId" clearable>
174
+          <el-select v-model="form.attendanceTeamId" filterable placeholder="请选择班组" style="width: 288px">
175
+            <el-option
176
+              v-for="item in teamOptions"
177
+              :key="item.value"
178
+              :label="item.label"
179
+              :value="item.value"
180
+            />
181
+          </el-select>
182
+        </el-form-item>
183
+        <el-form-item label="通道" prop="terminlCode" clearable>
184
+          <el-cascader
185
+            v-model="form.positionValue"
186
+            :options="positonOptions"
187
+            @change="handlePositionChange"
188
+            placeholder="请选择航站楼、区域、通道"
189
+            style="width: 288px"
190
+          />
191
+        </el-form-item>
192
+        <el-form-item label="岗位" prop="positionCode">
193
+          <el-select v-model="form.positionCode" placeholder="请选择岗位" clearable style="width: 288px">
194
+            <el-option
195
+              v-for="dict in item_check_method"
196
+              :key="dict.value"
197
+              :label="dict.label"
198
+              :value="dict.value"
199
+            />
200
+          </el-select>
201
+        </el-form-item>
202
+        <el-form-item label="备注" prop="remark">
203
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
204
+        </el-form-item>
205
+      </el-form>
206
+      <template #footer>
207
+        <div class="dialog-footer">
208
+          <el-button type="primary" @click="submitForm">确 定</el-button>
209
+          <el-button @click="cancel">取 消</el-button>
210
+        </div>
211
+      </template>
212
+    </el-dialog>
213
+
214
+    
215
+    <!-- 上岗记录导入对话框 -->
216
+    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
217
+      <el-upload ref="uploadRef" 
218
+      :limit="1" accept=".xlsx, .xls" 
219
+      :headers="upload.headers" 
220
+      :action="upload.url + '?updateSupport=' + upload.updateSupport" 
221
+      :disabled="upload.isUploading" 
222
+      :on-progress="handleFileUploadProgress" 
223
+      :on-success="handleFileSuccess" 
224
+      :auto-upload="false" drag>
225
+        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
226
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
227
+        <template #tip>
228
+          <div class="el-upload__tip text-center">
229
+            <!-- <div class="el-upload__tip">
230
+              <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
231
+            </div> -->
232
+            <span>仅允许导入xls、xlsx格式文件。</span>
233
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
234
+          </div>
235
+        </template>
236
+      </el-upload>
237
+      <template #footer>
238
+        <div class="dialog-footer">
239
+          <el-button type="primary" @click="submitFileForm">确 定</el-button>
240
+          <el-button @click="upload.open = false">取 消</el-button>
241
+        </div>
242
+      </template>
243
+    </el-dialog>
244
+  </div>
245
+</template>
246
+
247
+<script setup name="PostRecord">
248
+import { listPostRecord, getPostRecord, delPostRecord, addPostRecord, updatePostRecord, positionList, teamList } from "@/api/attendance/postRecord"
249
+import UserSelect from "@/components/UserSelect"
250
+
251
+const { proxy } = getCurrentInstance()
252
+
253
+const { item_check_method } = proxy.useDict('item_check_method')
254
+
255
+const postRecordList = ref([])
256
+const open = ref(false)
257
+const loading = ref(true)
258
+const showSearch = ref(true)
259
+const ids = ref([])
260
+const single = ref(true)
261
+const multiple = ref(true)
262
+const total = ref(0)
263
+const title = ref("")
264
+const dateRange = ref([])
265
+// 控制 UserSelect 组件是否可编辑
266
+const disabledUserSelect = ref(false)
267
+const positonOptions = ref([])
268
+
269
+const teamOptions = ref([])
270
+
271
+import { getToken } from "@/utils/auth"
272
+
273
+const data = reactive({
274
+  form: {},
275
+  queryParams: {
276
+    pageNum: 1,
277
+    pageSize: 10,
278
+    tenantId: null,
279
+    revision: null,
280
+    userId: null,
281
+    channelCode: null,
282
+    shiftCode: null,
283
+    checkInTime: null,
284
+    checkOutTime: null,
285
+    workDuration: null,
286
+    status: null,
287
+    attendanceDate: null,
288
+    attendanceTeamId: null,
289
+    overDuration: null,
290
+    attendanceTeamName: null,
291
+    attendanceDepartmentId: null,
292
+    attendanceDepartmentName: null,
293
+    attendanceStationId: null,
294
+    attendanceStationName: null,
295
+    channelName: null,
296
+    regionalCode: null,
297
+    regionalName: null,
298
+    terminlCode: null,
299
+    terminlName: null,
300
+    userName: null,
301
+    shiftName: null,
302
+    remark: null,
303
+    statusDesc: null
304
+  },
305
+  rules: {
306
+    userId: [
307
+      { required: !disabledUserSelect, message: "用户ID不能为空", trigger: "blur" }
308
+    ],
309
+    channelCode: [
310
+      { required: true, message: "通道编码不能为空", trigger: "blur" }
311
+    ],
312
+    positionCode: [
313
+      { required: true, message: "岗位编码不能为空", trigger: "blur" }
314
+    ],
315
+    checkInTime: [
316
+      { required: true, message: "上岗时间不能为空", trigger: "blur" }
317
+    ],
318
+    checkOutTime: [
319
+      { required: true, message: "离岗时间不能为空", trigger: "blur" }
320
+    ],
321
+    workDuration: [
322
+      { required: true, message: "工作时长(分钟)不能为空", trigger: "blur" }
323
+    ],
324
+    status: [
325
+      { required: true, message: "考勤状态不能为空", trigger: "change" }
326
+    ],
327
+    attendanceDate: [
328
+      { required: true, message: "考勤日期不能为空", trigger: "blur" }
329
+    ],
330
+    overDuration: [
331
+      { required: true, message: "加班时长(分钟)不能为空", trigger: "blur" }
332
+    ],
333
+    attendanceTeamName: [
334
+      { required: true, message: "考勤班组名称不能为空", trigger: "blur" }
335
+    ],
336
+    attendanceDepartmentId: [
337
+      { required: true, message: "考勤科室ID不能为空", trigger: "blur" }
338
+    ],
339
+    attendanceDepartmentName: [
340
+      { required: true, message: "考勤科室名称不能为空", trigger: "blur" }
341
+    ],
342
+    attendanceStationId: [
343
+      { required: true, message: "考勤机构站ID不能为空", trigger: "blur" }
344
+    ],
345
+    attendanceStationName: [
346
+      { required: true, message: "考勤机构站名称不能为空", trigger: "blur" }
347
+    ],
348
+    channelName: [
349
+      { required: true, message: "通道名称不能为空", trigger: "blur" }
350
+    ],
351
+    regionalCode: [
352
+      { required: true, message: "区域编码不能为空", trigger: "blur" }
353
+    ],
354
+    regionalName: [
355
+      { required: true, message: "区域名称不能为空", trigger: "blur" }
356
+    ],
357
+    terminlCode: [
358
+      { required: true, message: "航站楼编码不能为空", trigger: "blur" }
359
+    ],
360
+    terminlName: [
361
+      { required: true, message: "航站楼名称不能为空", trigger: "blur" }
362
+    ],
363
+    userName: [
364
+      { required: true, message: "用户名称不能为空", trigger: "blur" }
365
+    ],
366
+    shiftName: [
367
+      { required: true, message: "班次名称不能为空", trigger: "blur" }
368
+    ],
369
+    statusDesc: [
370
+      { required: true, message: "考勤状态名称不能为空", trigger: "blur" }
371
+    ]
372
+  }
373
+})
374
+
375
+const upload = reactive({
376
+  // 是否显示弹出层
377
+  open: false,
378
+  // 弹出层标题
379
+  title: "",
380
+  // 是否禁用上传
381
+  isUploading: false,
382
+  // 是否更新已经存在的用户数据
383
+  updateSupport: 0,
384
+  // 设置上传的请求头部
385
+  headers: { Authorization: "Bearer " + getToken() },
386
+  // 上传的地址
387
+  url: import.meta.env.VITE_APP_BASE_API + "/attendance/postRecord/importData"
388
+})
389
+
390
+const { queryParams, form, rules } = toRefs(data)
391
+
392
+/** 查询上岗记录列表 */
393
+function getList() {
394
+  loading.value = true
395
+  listPostRecord(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
396
+    postRecordList.value = response.rows
397
+    total.value = response.total
398
+    loading.value = false
399
+  })
400
+}
401
+
402
+// 获取级联数据
403
+const fetchPositionData = async () => {
404
+  try {
405
+    const response = await positionList({})
406
+    positonOptions.value = response.data
407
+  } catch (error) {
408
+    console.error('获取通道级联数据失败:', error)
409
+  }
410
+}
411
+const fetchTeamData = async () => {
412
+  try {
413
+    const response = await teamList({})
414
+    const d = response.data
415
+    for (const key in d) {
416
+      if (d.hasOwnProperty(key)) {
417
+        const option = { "value": Number(key), "label": d[key] }
418
+        teamOptions.value.push(option)
419
+      }
420
+    }
421
+  } catch (error) {
422
+    console.error('获取班组数据失败:', error)
423
+  }
424
+}
425
+
426
+// 级联选择变化时的处理函数
427
+const handlePositionChange = (value) => {
428
+  if (value.length === 3) {
429
+    form.value.terminlCode = value[0]
430
+    form.value.regionalCode = value[1]
431
+    form.value.channelCode = value[2]
432
+  }
433
+}
434
+
435
+// 取消按钮
436
+function cancel() {
437
+  open.value = false
438
+  reset()
439
+}
440
+
441
+// 表单重置
442
+function reset() {
443
+  form.value = {
444
+    tenantId: null,
445
+    revision: null,
446
+    createBy: null,
447
+    createTime: null,
448
+    updateBy: null,
449
+    updateTime: null,
450
+    userId: null,
451
+    channelCode: null,
452
+    shiftCode: null,
453
+    checkInTime: null,
454
+    checkOutTime: null,
455
+    workDuration: null,
456
+    status: null,
457
+    attendanceDate: null,
458
+    attendanceTeamId: null,
459
+    overDuration: null,
460
+    id: null,
461
+    attendanceTeamName: null,
462
+    attendanceDepartmentId: null,
463
+    attendanceDepartmentName: null,
464
+    attendanceStationId: null,
465
+    attendanceStationName: null,
466
+    channelName: null,
467
+    regionalCode: null,
468
+    regionalName: null,
469
+    terminlCode: null,
470
+    terminlName: null,
471
+    userName: null,
472
+    shiftName: null,
473
+    remark: null,
474
+    statusDesc: null
475
+  }
476
+  proxy.resetForm("postRecordRef")
477
+}
478
+
479
+/** 搜索按钮操作 */
480
+function handleQuery() {
481
+  queryParams.value.pageNum = 1
482
+  getList()
483
+}
484
+
485
+/** 重置按钮操作 */
486
+function resetQuery() {
487
+  dateRange.value = []
488
+  proxy.resetForm("queryRef")
489
+  handleQuery()
490
+}
491
+
492
+// 多选框选中数据
493
+function handleSelectionChange(selection) {
494
+  ids.value = selection.map(item => item.id)
495
+  single.value = selection.length != 1
496
+  multiple.value = !selection.length
497
+}
498
+
499
+/** 新增按钮操作 */
500
+function handleAdd() {
501
+  reset()
502
+  open.value = true
503
+  title.value = "添加上岗记录"
504
+  // 新增时允许编辑
505
+  disabledUserSelect.value = false
506
+}
507
+
508
+/** 修改按钮操作 */
509
+function handleUpdate(row) {
510
+  reset()
511
+  const _id = row.id || ids.value
512
+  getPostRecord(_id).then(response => {
513
+    form.value = response.data
514
+    // 根据实际数据结构设置默认值
515
+    form.value.positionValue = [
516
+      form.value.terminlCode,
517
+      form.value.regionalCode,
518
+      form.value.channelCode
519
+    ]
520
+    open.value = true
521
+    title.value = "修改上岗记录"
522
+    // 修改时禁止编辑
523
+    disabledUserSelect.value = true
524
+  })
525
+}
526
+
527
+/** 提交按钮 */
528
+function submitForm() {
529
+  proxy.$refs["postRecordRef"].validate(valid => {
530
+    if (valid) {
531
+      if (form.value.id != null) {
532
+        updatePostRecord(form.value).then(response => {
533
+          proxy.$modal.msgSuccess("修改成功")
534
+          open.value = false
535
+          getList()
536
+        })
537
+      } else {
538
+        addPostRecord(form.value).then(response => {
539
+          proxy.$modal.msgSuccess("新增成功")
540
+          open.value = false
541
+          getList()
542
+        })
543
+      }
544
+    }
545
+  })
546
+}
547
+
548
+/** 删除按钮操作 */
549
+function handleDelete(row) {
550
+  const _ids = row.id || ids.value
551
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
552
+    return delPostRecord(_ids)
553
+  }).then(() => {
554
+    getList()
555
+    proxy.$modal.msgSuccess("删除成功")
556
+  }).catch(() => {})
557
+}
558
+
559
+/** 导出按钮操作 */
560
+function handleExport() {
561
+  proxy.download('attendance/postRecord/export', {
562
+    ...queryParams.value
563
+  }, `postRecord_${new Date().getTime()}.xlsx`)
564
+}
565
+
566
+onMounted(() => {
567
+  fetchPositionData()
568
+  fetchTeamData()
569
+  getList()
570
+})
571
+
572
+
573
+/** 导入按钮操作 */
574
+function handleImport() {
575
+  upload.title = "上岗记录导入"
576
+  upload.open = true
577
+}
578
+
579
+/** 下载模板操作 */
580
+function importTemplate() {
581
+  proxy.download("attendance/postRecord/importTemplate", {
582
+  }, `record_template_${new Date().getTime()}.xlsx`)
583
+}
584
+
585
+/**文件上传中处理 */
586
+const handleFileUploadProgress = (event, file, fileList) => {
587
+  upload.isUploading = true
588
+}
589
+
590
+/** 文件上传成功处理 */
591
+const handleFileSuccess = (response, file, fileList) => {
592
+  upload.open = false
593
+  upload.isUploading = false
594
+  proxy.$refs["uploadRef"].handleRemove(file)
595
+  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
596
+  getList()
597
+}
598
+
599
+/** 提交上传文件 */
600
+function submitFileForm() {
601
+  proxy.$refs["uploadRef"].submit()
602
+}
603
+</script>

+ 299 - 0
src/views/attendance/record/index.vue

@@ -0,0 +1,299 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+
5
+      <el-form-item label="用户姓名" prop="userName">
6
+        <el-input v-model="queryParams.userName" placeholder="请输入用户姓名" clearable @keyup.enter="handleQuery" />
7
+      </el-form-item>
8
+      <el-form-item label="班组名称" prop="attendanceTeamName">
9
+        <el-input v-model="queryParams.attendanceTeamName" placeholder="请输入班组名称" clearable @keyup.enter="handleQuery" />
10
+      </el-form-item>
11
+     
12
+      
13
+      <el-form-item label="工作区域" prop="terminlName">
14
+        <el-input v-model="queryParams.terminlName" placeholder="请输入工作区域" clearable @keyup.enter="handleQuery" />
15
+      </el-form-item>
16
+      <el-form-item label="维护时间" prop="attendanceDateRange">
17
+        <el-date-picker clearable v-model="queryParams.attendanceDateRange" type="daterange" range-separator="至"
18
+          start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" placeholder="请选择维护时间区间">
19
+        </el-date-picker>
20
+      </el-form-item>
21
+
22
+
23
+      <el-form-item>
24
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
25
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
26
+      </el-form-item>
27
+    </el-form>
28
+
29
+    <el-row :gutter="10" class="mb8">
30
+      <!-- <el-col :span="1.5">
31
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
32
+          v-hasPermi="['attendance:record:add']">新增</el-button>
33
+      </el-col>
34
+      <el-col :span="1.5">
35
+        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
36
+          v-hasPermi="['attendance:record:edit']">修改</el-button>
37
+      </el-col> -->
38
+      <el-col :span="1.5">
39
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
40
+          v-hasPermi="['attendance:record:remove']">删除</el-button>
41
+      </el-col>
42
+      <el-col :span="1.5">
43
+        <el-button type="warning" plain icon="Download" @click="handleExport"
44
+          v-hasPermi="['attendance:record:export']">导出</el-button>
45
+      </el-col>
46
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
47
+    </el-row>
48
+
49
+    <el-table v-loading="loading" :data="recordList" @selection-change="handleSelectionChange">
50
+      <el-table-column type="selection" width="55" align="center" />
51
+      <!-- <el-table-column label="用户ID" align="center" prop="userId" />
52
+      <el-table-column label="用户code" align="center" prop="userCode" /> -->
53
+      <el-table-column label="用户姓名" align="center" prop="userName" />
54
+      <el-table-column label="班组名称" align="center" prop="attendanceTeamName" />
55
+      <!-- <el-table-column label="航站楼编码" align="center" prop="terminlCode" /> -->
56
+      <el-table-column label="工作区域" align="center" prop="terminlName" />
57
+
58
+      <!-- <el-table-column label="考勤班组ID" align="center" prop="attendanceTeamId" /> -->
59
+
60
+      <el-table-column label="维护时间" align="center" prop="attendanceDate" width="180">
61
+        <template #default="scope">
62
+          <span>{{ parseTime(scope.row.attendanceDate, '{y}-{m}-{d}') }}</span>
63
+        </template>
64
+      </el-table-column>
65
+      <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
66
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
67
+        <template #default="scope">
68
+          <!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
69
+            v-hasPermi="['attendance:record:edit']">修改</el-button> -->
70
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
71
+            v-hasPermi="['attendance:record:remove']">删除</el-button>
72
+        </template>
73
+      </el-table-column>
74
+    </el-table>
75
+
76
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
77
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
78
+
79
+    <!-- 添加或修改考勤班组成员对话框 -->
80
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
81
+      <el-form ref="recordRef" :model="form" :rules="rules" label-width="80px">
82
+        <el-form-item label="用户ID" prop="userId">
83
+          <el-input v-model="form.userId" placeholder="请输入用户ID" />
84
+        </el-form-item>
85
+        <el-form-item label="用户code" prop="userCode">
86
+          <el-input v-model="form.userCode" placeholder="请输入用户code" />
87
+        </el-form-item>
88
+        <el-form-item label="用户姓名" prop="userName">
89
+          <el-input v-model="form.userName" placeholder="请输入用户姓名" />
90
+        </el-form-item>
91
+        <el-form-item label="航站楼编码" prop="terminlCode">
92
+          <el-input v-model="form.terminlCode" placeholder="请输入航站楼编码" />
93
+        </el-form-item>
94
+        <el-form-item label="航站楼名称" prop="terminlName">
95
+          <el-input v-model="form.terminlName" placeholder="请输入航站楼名称" />
96
+        </el-form-item>
97
+        <el-form-item label="维护时间" prop="attendanceDate">
98
+          <el-date-picker clearable v-model="form.attendanceDate" type="date" value-format="YYYY-MM-DD"
99
+            placeholder="请选择维护时间">
100
+          </el-date-picker>
101
+        </el-form-item>
102
+        <el-form-item label="考勤班组ID" prop="attendanceTeamId">
103
+          <el-input v-model="form.attendanceTeamId" placeholder="请输入考勤班组ID" />
104
+        </el-form-item>
105
+        <el-form-item label="班组名称" prop="attendanceTeamName">
106
+          <el-input v-model="form.attendanceTeamName" placeholder="请输入班组名称" />
107
+        </el-form-item>
108
+        <el-form-item label="备注" prop="remark">
109
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
110
+        </el-form-item>
111
+      </el-form>
112
+      <template #footer>
113
+        <div class="dialog-footer">
114
+          <el-button type="primary" @click="submitForm">确 定</el-button>
115
+          <el-button @click="cancel">取 消</el-button>
116
+        </div>
117
+      </template>
118
+    </el-dialog>
119
+  </div>
120
+</template>
121
+
122
+<script setup name="Record">
123
+import { listRecord, getRecord, delRecord, addRecord, updateRecord } from "@/api/attendance/record"
124
+
125
+const { proxy } = getCurrentInstance()
126
+
127
+const recordList = ref([])
128
+const open = ref(false)
129
+const loading = ref(true)
130
+const showSearch = ref(true)
131
+const ids = ref([])
132
+const single = ref(true)
133
+const multiple = ref(true)
134
+const total = ref(0)
135
+const title = ref("")
136
+
137
+const data = reactive({
138
+  form: {},
139
+  queryParams: {
140
+    pageNum: 1,
141
+    pageSize: 10,
142
+    // userId: null,
143
+    userCode: null,
144
+    userName: null,
145
+  
146
+    terminlName: null,
147
+    attendanceDate: null,
148
+    attendanceDateRange: [],
149
+    attendanceTeamId: null,
150
+    attendanceTeamName: null,
151
+  },
152
+  rules: {
153
+    // userId: [
154
+    //   { required: true, message: "用户ID不能为空", trigger: "blur" }
155
+    // ],
156
+    userCode: [
157
+      { required: true, message: "用户code不能为空", trigger: "blur" }
158
+    ],
159
+    userName: [
160
+      { required: true, message: "用户姓名不能为空", trigger: "blur" }
161
+    ],
162
+    attendanceDate: [
163
+      { required: true, message: "维护时间不能为空", trigger: "blur" }
164
+    ],
165
+  }
166
+})
167
+
168
+const { queryParams, form, rules } = toRefs(data)
169
+
170
+/** 查询考勤班组成员列表 */
171
+function getList() {
172
+  loading.value = true
173
+
174
+  // 处理时间区间查询参数
175
+  const params = {
176
+    ...queryParams.value
177
+  }
178
+
179
+  // 如果有时间区间,设置开始日期和结束日期
180
+  if (params.attendanceDateRange && params.attendanceDateRange.length === 2) {
181
+    params.attendanceDateStart = params.attendanceDateRange[0]
182
+    params.attendanceDateEnd = params.attendanceDateRange[1]
183
+    // 移除时间区间参数,避免传递给后端
184
+    delete params.attendanceDateRange
185
+  }
186
+
187
+  listRecord(params).then(response => {
188
+    recordList.value = response.rows
189
+    total.value = response.total
190
+    loading.value = false
191
+  })
192
+}
193
+
194
+// 取消按钮
195
+function cancel() {
196
+  open.value = false
197
+  reset()
198
+}
199
+
200
+// 表单重置
201
+function reset() {
202
+  form.value = {
203
+    // userId: null,
204
+    userCode: null,
205
+    userName: null,
206
+    terminlCode: null,
207
+    terminlName: null,
208
+    attendanceDate: null,
209
+    attendanceTeamId: null,
210
+    attendanceTeamName: null,
211
+    createBy: null,
212
+    createTime: null,
213
+    updateBy: null,
214
+    updateTime: null,
215
+    remark: null
216
+  }
217
+  proxy.resetForm("recordRef")
218
+}
219
+
220
+/** 搜索按钮操作 */
221
+function handleQuery() {
222
+  queryParams.value.pageNum = 1
223
+  getList()
224
+}
225
+
226
+/** 重置按钮操作 */
227
+function resetQuery() {
228
+  proxy.resetForm("queryRef")
229
+  // 手动重置时间区间参数
230
+  queryParams.value.attendanceDateRange = []
231
+  handleQuery()
232
+}
233
+
234
+// 多选框选中数据
235
+function handleSelectionChange(selection) {
236
+  ids.value = selection.map(item => item.userId)
237
+  single.value = selection.length != 1
238
+  multiple.value = !selection.length
239
+}
240
+
241
+/** 新增按钮操作 */
242
+function handleAdd() {
243
+  reset()
244
+  open.value = true
245
+  title.value = "添加考勤班组成员"
246
+}
247
+
248
+/** 修改按钮操作 */
249
+function handleUpdate(row) {
250
+  reset()
251
+  const _userId = row.userId || ids.value
252
+  getRecord(_userId).then(response => {
253
+    form.value = response.data
254
+    open.value = true
255
+    title.value = "修改考勤班组成员"
256
+  })
257
+}
258
+
259
+/** 提交按钮 */
260
+function submitForm() {
261
+  proxy.$refs["recordRef"].validate(valid => {
262
+    if (valid) {
263
+      if (form.value.userId != null) {
264
+        updateRecord(form.value).then(response => {
265
+          proxy.$modal.msgSuccess("修改成功")
266
+          open.value = false
267
+          getList()
268
+        })
269
+      } else {
270
+        addRecord(form.value).then(response => {
271
+          proxy.$modal.msgSuccess("新增成功")
272
+          open.value = false
273
+          getList()
274
+        })
275
+      }
276
+    }
277
+  })
278
+}
279
+
280
+/** 删除按钮操作 */
281
+function handleDelete(row) {
282
+  const _userIds = row.userId || ids.value
283
+  proxy.$modal.confirm('是否确认删除考勤班组成员编号为"' + _userIds + '"的数据项?').then(function () {
284
+    return delRecord(_userIds)
285
+  }).then(() => {
286
+    getList()
287
+    proxy.$modal.msgSuccess("删除成功")
288
+  }).catch(() => { })
289
+}
290
+
291
+/** 导出按钮操作 */
292
+function handleExport() {
293
+  proxy.download('attendance/record/export', {
294
+    ...queryParams.value
295
+  }, `record_${new Date().getTime()}.xlsx`)
296
+}
297
+
298
+getList()
299
+</script>

+ 334 - 0
src/views/check/checkCorrection/components/Deatils.vue

@@ -0,0 +1,334 @@
1
+<template>
2
+  <el-dialog :title="title" v-model="open" width="1100px" append-to-body>
3
+    <div class="contentWrap" v-loading="loading">
4
+      <el-form ref="checkFormRef" :model="form" :rules="rules" scroll-to-error :scroll-into-view-options="{block: 'center'}" label-width="9em">
5
+        <el-card header="基本信息" style="margin-bottom: 10px;"  v-card-expand="{ isExpanded: true }">
6
+          <el-row :gutter="16">
7
+            <el-col :span="12">
8
+              <el-form-item label="任务编号" prop="taskCode">
9
+                <el-input :disabled="disabled" v-model="form.taskCode" :placeholder="disabled ? '-' : '请输入任务编号'" maxlength="30" />
10
+              </el-form-item>
11
+            </el-col>
12
+            <el-col :span="12">
13
+              <el-form-item label="检查人" prop="checkerName">
14
+                <el-input :disabled="disabled" v-model="form.checkerName" :placeholder="disabled ? '-' : '请输入检查人'" />
15
+              </el-form-item>
16
+            </el-col>
17
+            <el-col :span="12">
18
+              <el-form-item label="被检查人/科/班组" prop="checkedDepartmentName">
19
+                <el-input
20
+                  v-if=" form.checkedLevel === 'DEPARTMENT_LEVEL' "
21
+                  :disabled="disabled"
22
+                  :value="form.checkedDepartmentName"
23
+                  :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
24
+                <el-input
25
+                  v-else-if=" form.checkedLevel === 'TEAM_LEVEL' "
26
+                  :disabled="disabled"
27
+                  :value="form.checkedTeamName"
28
+                  :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
29
+                <el-input
30
+                  v-else-if=" form.checkedLevel === 'PERSONNEL_LEVEL' "
31
+                  :disabled="disabled"
32
+                  :value="form.checkedPersonnelName"
33
+                  :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
34
+                <el-input :disabled="disabled" v-else />
35
+              </el-form-item>
36
+            </el-col>
37
+            <el-col :span="12">
38
+              <el-form-item label="检查地点" prop="nickName">
39
+                <el-input
40
+                  :disabled="disabled"
41
+                  :value="viewText( form.terminlName, form.regionalName, form.channelName )"
42
+                  :placeholder="disabled ? '-' : '请输入检查地点'" />
43
+              </el-form-item>
44
+            </el-col>
45
+            <el-col :span="12">
46
+              <el-form-item label="检查时间" prop="checkTime">
47
+                <el-input :disabled="disabled" v-model="form.checkTime" :placeholder="disabled ? '-' : '请输入检查时间'" />
48
+              </el-form-item>
49
+            </el-col>
50
+          </el-row>
51
+        </el-card>
52
+
53
+        <el-card header="待整改任务" style="margin-bottom: 10px;" v-card-expand="{ isExpanded: true }">
54
+          <div>
55
+            <el-row :gutter="16">
56
+              <el-col :span="24" v-for=" item of groupHandler( form.checkProjectItemList ) " :key="item.id"
57
+                style="margin-bottom: 15px;">
58
+                <div class="title">{{ item.title }}</div>
59
+                <div v-for=" attr in item.list " :key="attr.index" style="padding: 0 30px; box-sizing: border-box;">
60
+                  <el-form-item :label="attr.projectName" label-width="auto"
61
+                    :prop="`checkProjectItemList.${attr.index}.scoreLevel`">
62
+                    <el-switch
63
+                      :disabled="disabled"
64
+                      v-model="attr.scoreLevel"
65
+                      active-value="QUALIFIED"
66
+                      inactive-value="UNQUALIFIED"
67
+                      inline-prompt
68
+                      active-text="符合"
69
+                      inactive-text="不符合"
70
+                      style="--el-switch-on-color: rgb(98, 111, 240); --el-switch-off-color: #ff4949" />
71
+                  </el-form-item>
72
+                  <div v-if=" attr.scoreLevel === 'UNQUALIFIED' " style="padding: 0 30px; box-sizing: border-box;">
73
+                    <el-form-item v-if="attr.checkUserList && attr.checkUserList.length" label="不合格人员" label-width="6em" prop="checkedPersonnelName">
74
+                      <el-input
75
+                        :disabled="disabled"
76
+                        :value="getUserName( attr.checkUserList )" />
77
+                    </el-form-item>
78
+                    <el-form-item v-if="attr.problemDescription" label="问题描述" label-width="6em"
79
+                      :prop="`checkProjectItemList.${attr.index}.problemDescription`">
80
+                      <el-input type="textarea" :disabled="disabled" v-model="attr.problemDescription" />
81
+                    </el-form-item>
82
+                  </div>
83
+                </div>
84
+              </el-col>
85
+              <el-col :span="24" v-if=" form.checkRecordBaseAttachmentList && form.checkRecordBaseAttachmentList.length ">
86
+                <div class="title">不合格照片</div>
87
+                <div class="imgList">
88
+                  <ImageUpload :modelValue="transformImglistData(form.checkRecordBaseAttachmentList)" :disabled="disabled"  />
89
+                </div>
90
+              </el-col>
91
+            </el-row>
92
+          </div>
93
+        </el-card>
94
+
95
+        <el-card header="整改要求" style="margin-bottom: 10px;"  v-card-expand="{ isExpanded: true }">
96
+          <el-row :gutter="16">
97
+            <el-col :span="12">
98
+              <el-form-item label="负责人" prop="responsibleUserName">
99
+                <el-input v-model="form.responsibleUserName" :disabled="disabled" :placeholder="disabled ? '-' : '请输入负责人'" />
100
+              </el-form-item>
101
+            </el-col>
102
+
103
+            <el-col :span="12">
104
+              <el-form-item label="整改期限" prop="rectificationDeadline">
105
+                <el-input v-model="form.rectificationDeadline" :disabled="disabled" :placeholder="disabled ? '-' : '请输入整改期限'" />
106
+              </el-form-item>
107
+            </el-col>
108
+            <el-col :span="24">
109
+              <el-form-item label="整改要求" prop="rectificationSuggestions">
110
+                <el-input type="textarea" v-model="form.rectificationSuggestions" :disabled="disabled" :placeholder="disabled ? '-' : '请输入整改要求'" />
111
+              </el-form-item>
112
+            </el-col>
113
+          </el-row>
114
+        </el-card>
115
+        <el-card header="整改详情" style="margin-bottom: 10px;"  v-card-expand="{ isExpanded: true }">
116
+          <el-row :gutter="16">
117
+            <el-col :span="24">
118
+              <el-form-item label="整改详情" label-width="6em" prop="rectificationDetails">
119
+                <el-input type="textarea" v-model="form.rectificationDetails" :disabled="disabled" :placeholder="'请输入整改要求'" />
120
+              </el-form-item>
121
+            </el-col>
122
+            <el-col :span="24" v-if="baseAttachmentList.length">
123
+              <div class="title">整改图片</div>
124
+              <div class="imgList">
125
+                <ImageUpload v-model="baseAttachmentList" :disabled="disabled"  />
126
+              </div>
127
+            </el-col>
128
+          </el-row>
129
+        </el-card>
130
+        <el-card header="审批历史" style="margin-bottom: 10px;"  v-card-expand="{ isExpanded: true }">
131
+          <el-table :data="historyInstanceList">
132
+            <el-table-column label="审批人" align="center" prop="operatorName" width="180" />
133
+            <el-table-column label="审批时间" align="center" prop="operationTime" />
134
+            <el-table-column label="审批状态" align="center" prop="comment" />
135
+          </el-table>
136
+        </el-card>
137
+      </el-form>
138
+    </div>
139
+    <template #footer>
140
+      <div class="dialog-footer">
141
+        <el-button @click="cancel">关闭</el-button>
142
+      </div>
143
+    </template>
144
+  </el-dialog>
145
+</template>
146
+
147
+<script setup>
148
+import { watch, ref, onMounted, reactive, onUnmounted, computed } from 'vue'
149
+import { getCheckCorrection, historyInstance} from "@/api/check/checkCorrection"
150
+import { listUser } from "@/api/system/user"
151
+const props = defineProps({
152
+  id: {
153
+    type: Number
154
+  }
155
+})
156
+
157
+const disabled = ref(true)
158
+const open = defineModel('show', ref(false))
159
+const title = ref('整改单详情')
160
+const form = ref({
161
+  checkRecordBaseAttachmentList: [],
162
+  baseAttachmentList: []
163
+})
164
+const loading = ref(false)
165
+const historyInstanceList = ref([])
166
+const listUserOption = ref([])
167
+const checkFormRef = ref(null)
168
+const getDetailInfo = () => {
169
+  loading.value = true
170
+  getCheckCorrection(props.id).then(res => {
171
+    checkFormRef.value.resetFields()
172
+    form.value = res.data
173
+    if (res.data.instanceId) {
174
+      return historyInstance(res.data.instanceId).then(res => {
175
+        historyInstanceList.value = res.rows
176
+        return
177
+      })
178
+    } else {
179
+      return historyInstanceList.value = []
180
+    }
181
+    
182
+  }).finally(() => {
183
+    loading.value = false
184
+  })
185
+}
186
+
187
+const viewText = (...rest) => {
188
+  if (Array.isArray(rest) && rest.length) {
189
+    return rest.reduce((cur, acc) => {
190
+      if (acc) {
191
+        cur += cur ? `/${acc}` : acc
192
+      }
193
+      return cur
194
+    }, '')
195
+  }
196
+  return '-'
197
+}
198
+
199
+const transformImglistData = (imgList) => {
200
+  if (Array.isArray(imgList) && imgList.length) {
201
+    return imgList.map(item => {
202
+      return {
203
+        ...item,
204
+        name: item.attachmentName,
205
+        url: item.attachmentUrl,
206
+      }
207
+    })
208
+  }
209
+  return []
210
+}
211
+
212
+const baseAttachmentList = computed({
213
+  get () {
214
+    return transformImglistData(form.value.baseAttachmentList)
215
+  },
216
+  set (newValue) {
217
+    form.value.baseAttachmentList = newValue.map(item => {
218
+      return {
219
+        attachmentName: item.name,
220
+        attachmentUrl: item.url
221
+      }
222
+    })
223
+  }
224
+})
225
+
226
+const groupHandler = (list) => {
227
+  if (Array.isArray(list) && list.length) {
228
+    return list.reduce((cur, acc, index) => {
229
+      const findIndex = cur.findIndex(item => item.categoryCodeOne === acc.categoryCodeOne && item.categoryCodeTwo === acc.categoryCodeTwo)
230
+      if (findIndex >= 0) {
231
+        cur[ findIndex ].list.push({ index, ...acc })
232
+      } else {
233
+        cur.push({
234
+          title: `${acc.categoryNameOne}/${acc.categoryNameTwo}`,
235
+          categoryCodeOne: acc.categoryCodeOne,
236
+          categoryCodeTwo: acc.categoryCodeTwo,
237
+          list: [ { index, ...acc } ]
238
+        })
239
+      }
240
+      return cur
241
+    }, [])
242
+  }
243
+  return []
244
+}
245
+const getUserInfo = () => {
246
+  listUser().then(res => {
247
+    listUserOption.value = res.rows.map((item) => ({
248
+      value: item.userId,
249
+      label: item.nickName
250
+    }))
251
+  })
252
+}
253
+
254
+const getUserName = (list) => {
255
+  if (Array.isArray(list) && list.length) {
256
+    return list.map(attr => {
257
+      return (listUserOption.value.find(item => item.value === attr.userId) || {}).label
258
+    }).filter(Boolean).join('、')
259
+  }
260
+  return '-'
261
+}
262
+
263
+const cancel = () => {
264
+  checkFormRef.value && checkFormRef.value.resetFields()
265
+  form.value = {}
266
+  open.value = false
267
+}
268
+
269
+watch(() => open.value, () => {
270
+  if (open.value) {
271
+    getDetailInfo()
272
+  } else {
273
+    form.value = {}
274
+  }
275
+})
276
+
277
+const rules = reactive({
278
+  rectificationDetails: [{ required: true, message: "请输入整改详情", trigger: "blur" }]
279
+})
280
+
281
+onMounted(() => {
282
+  getUserInfo()
283
+})
284
+onUnmounted(() => {
285
+  cancel()
286
+})
287
+</script>
288
+
289
+<style lang="less" scoped>
290
+.contentWrap {
291
+  height: 75vh;
292
+  overflow-y: auto;
293
+  overflow-x: hidden;
294
+  padding: 0 10px;
295
+}
296
+
297
+:deep(.el-card) {
298
+  .el-card__header {
299
+    font-weight: 600;
300
+    font-size: 16px;
301
+    color: #000;
302
+  }
303
+}
304
+
305
+.title {
306
+  font-weight: 600;
307
+  color: #000;
308
+  margin-bottom: 10px;
309
+}
310
+
311
+.imgList {
312
+  display: flex;
313
+  flex-wrap: wrap;
314
+  row-gap: 15px;
315
+  column-gap: 15px;
316
+
317
+  .img-item {
318
+    width: 220px;
319
+    height: 120px;
320
+    border: 1px dashed #d9d9d9;
321
+    border-radius: 6px;
322
+    padding: 2px;
323
+    box-sizing: border-box;
324
+
325
+    img {
326
+      width: 100%;
327
+      height: 100%;
328
+      object-fit: contain;
329
+      border: none;
330
+      outline: none;
331
+    }
332
+  }
333
+}
334
+</style>

+ 137 - 0
src/views/check/checkCorrection/index.vue

@@ -0,0 +1,137 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="100px">
4
+      <el-row :gutter="16">
5
+        <el-col :span="8">
6
+          <el-form-item label="任务标题" prop="taskName">
7
+            <el-input v-model="queryParams.taskName" placeholder="请输入任务标题" clearable style="width: 200px;" />
8
+          </el-form-item>
9
+        </el-col>
10
+        <el-col :span="8">
11
+          <el-form-item label="检查人" prop="checkerName">
12
+            <el-input v-model="queryParams.checkerName" placeholder="请输入检查人" clearable style="width: 200px;" />
13
+          </el-form-item>
14
+        </el-col>
15
+        <el-col :span="8">
16
+          <el-form-item label="检查时间" prop="checkTime">
17
+            <el-date-picker v-model="queryParams.checkTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择检查时间"
18
+              style="width: 200px;" clearable />
19
+          </el-form-item>
20
+        </el-col>
21
+        <el-col :span="24">
22
+          <el-form-item>
23
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
24
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
25
+          </el-form-item>
26
+        </el-col>
27
+      </el-row>
28
+    </el-form>
29
+
30
+    <el-table v-loading="loading" :data="checkRecordList">
31
+      <el-table-column label="任务编号" align="center" prop="taskCode" width="180" />
32
+      <el-table-column label="任务标题" align="center" prop="taskName" />
33
+      <el-table-column label="检查人" align="center" prop="checkerName" width="100" />
34
+      <el-table-column label="检查时间" align="center" prop="checkTime" width="180">
35
+        <template #default=" scope ">
36
+          <span>{{ parseTime( scope.row.checkTime, '{y}-{m}-{d}' ) }}</span>
37
+        </template>
38
+      </el-table-column>
39
+      <el-table-column label="被检查级别" align="center" prop="checkedLevel" width="100">
40
+        <template #default=" scope ">
41
+          <dict-tag :options="check_checked_level" :value="scope.row.checkedLevel" />
42
+        </template>
43
+      </el-table-column>
44
+      <el-table-column label="被检查人/班组/科" align="center" prop="checkedLevel">
45
+        <template #default=" scope ">
46
+          <div v-if=" scope.row.checkedLevel === 'DEPARTMENT_LEVEL' ">{{ scope.row.checkedDepartmentName }}</div>
47
+          <div v-if=" scope.row.checkedLevel === 'TEAM_LEVEL' ">{{ scope.row.checkedTeamName }}</div>
48
+          <div v-if=" scope.row.checkedLevel === 'PERSONNEL_LEVEL' ">{{ scope.row.checkedPersonnelName }}</div>
49
+        </template>
50
+      </el-table-column>
51
+      <el-table-column label="整改状态" align="center" prop="statusDesc" width="100" />
52
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
53
+        <template #default=" scope ">
54
+          <el-button link type="info" icon="View" @click="handleDetail( scope.row )"
55
+            v-hasPermi="[ 'check:checkRecord:query' ]">详情</el-button>
56
+        </template>
57
+      </el-table-column>
58
+    </el-table>
59
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
60
+      v-model:limit="queryParams.pageSize"
61
+      @pagination="getList" />
62
+    <Details v-model:show="show" :id="cellRecord.id"/>
63
+  </div>
64
+</template>
65
+
66
+<script setup name="CheckRecord">
67
+import { checkCorrection } from "@/api/check/checkCorrection"
68
+import Details from "./components/Deatils.vue"
69
+import { useDict } from '@/utils/dict'
70
+import { onMounted } from 'vue'
71
+const { check_checked_level } = useDict('check_checked_level')
72
+
73
+const checkRecordList = ref([])
74
+const loading = ref(false)
75
+const total = ref(0)
76
+const queryParams = ref({
77
+  taskName: undefined,
78
+  checkerName: undefined,
79
+  checkTime: undefined,
80
+  pageNum: 1,
81
+  pageSize: 10
82
+})
83
+
84
+/** 查询检查记录列表 */
85
+function getList () {
86
+  loading.value = true
87
+  checkCorrection(queryParams.value).then(response => {
88
+    checkRecordList.value = response.rows
89
+    total.value = response.total
90
+  }).finally(() => {
91
+    loading.value = false
92
+  })
93
+}
94
+const cellRecord = ref({})
95
+const show = ref(false)
96
+const handleDetail = (record) => {
97
+  cellRecord.value = record
98
+  show.value = true
99
+}
100
+
101
+const handleQuery = () => {
102
+  getList()
103
+}
104
+
105
+onMounted(() => {
106
+  getList()
107
+})
108
+</script>
109
+
110
+<style>
111
+.record-card {
112
+  margin-bottom: 12px;
113
+  padding: 8px 12px;
114
+  /* border-left: 4px solid #52c41a;    */
115
+  /* background: #f6ffed; */
116
+  border-radius: 4px;
117
+}
118
+
119
+.record-line {
120
+  display: flex;
121
+  margin-bottom: 4px;
122
+  font-size: 14px;
123
+  line-height: 1.5;
124
+}
125
+
126
+.label {
127
+  color: #666;
128
+  margin-right: 6px;
129
+  flex-shrink: 0;
130
+  width: 70px;
131
+}
132
+
133
+.value {
134
+  color: #333;
135
+  flex: 1;
136
+}
137
+</style>

+ 234 - 0
src/views/check/checkRecord/components/Details.vue

@@ -0,0 +1,234 @@
1
+<template>
2
+  <div class="contentWrap">
3
+    <el-form ref="checkRecordRef" :model="form" :rules="rules" label-width="9em" :disabled="disabled">
4
+      <el-card header="基本信息" style="margin-bottom: 10px;">
5
+        <el-row :gutter="16">
6
+          <el-col :span="12">
7
+            <el-form-item label="任务编号" prop="taskCode">
8
+              <el-input v-model="form.taskCode" :placeholder="disabled ? '-' : '请输入任务编号'" maxlength="30" />
9
+            </el-form-item>
10
+          </el-col>
11
+          <el-col :span="12">
12
+            <el-form-item label="检查人" prop="checkerName">
13
+              <el-input v-model="form.checkerName" :placeholder="disabled ? '-' : '请输入检查人'" />
14
+            </el-form-item>
15
+          </el-col>
16
+          <el-col :span="12">
17
+            <el-form-item label="被检查人/科/班组" prop="checkedDepartmentName">
18
+              <el-input
19
+                v-if=" form.checkedLevel === 'DEPARTMENT_LEVEL' "
20
+                :value="form.checkedDepartmentName"
21
+                :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
22
+              <el-input
23
+                v-else-if=" form.checkedLevel === 'TEAM_LEVEL' "
24
+                :value="form.checkedTeamName"
25
+                :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
26
+              <el-input
27
+                v-else-if=" form.checkedLevel === 'PERSONNEL_LEVEL' "
28
+                :value="form.checkedPersonnelName"
29
+                :placeholder="disabled ? '-' : '请输入被检查人/科/班组'" />
30
+              <el-input v-else />
31
+            </el-form-item>
32
+          </el-col>
33
+          <el-col :span="12">
34
+            <el-form-item label="检查地点" prop="nickName">
35
+              <el-input
36
+                :value="viewText( form.terminlName, form.regionalName, form.channelName )"
37
+                :placeholder="disabled ? '-' : '请输入检查地点'" />
38
+            </el-form-item>
39
+          </el-col>
40
+          <el-col :span="12">
41
+            <el-form-item label="检查时间" prop="checkTime">
42
+              <el-input v-model="form.checkTime" :placeholder="disabled ? '-' : '请输入检查时间'" />
43
+            </el-form-item>
44
+          </el-col>
45
+        </el-row>
46
+      </el-card>
47
+
48
+      <el-card header="巡检项目" style="margin-bottom: 10px;">
49
+        <div>
50
+          <el-row :gutter="16">
51
+            <el-col :span="24" v-for=" item of groupHandler( form.checkProjectItemList ) " :key="item.id"
52
+              style="margin-bottom: 15px;">
53
+              <div class="title">{{ item.title }}</div>
54
+              <div v-for=" attr in item.list " :key="attr.index" style="padding: 0 30px; box-sizing: border-box;">
55
+                <el-form-item :label="attr.projectName" label-width="auto"
56
+                  :prop="`checkProjectItemList.${attr.index}.scoreLevel`">
57
+                  <el-switch
58
+                    v-model="attr.scoreLevel"
59
+                    active-value="QUALIFIED"
60
+                    inactive-value="UNQUALIFIED"
61
+                    inline-prompt
62
+                    active-text="符合"
63
+                    inactive-text="不符合"
64
+                    style="--el-switch-on-color: rgb(98, 111, 240); --el-switch-off-color: #ff4949" />
65
+                </el-form-item>
66
+                <div v-if=" attr.scoreLevel === 'UNQUALIFIED' " style="padding: 0 30px; box-sizing: border-box;">
67
+                  <el-form-item v-if=" attr.checkUserList && attr.checkUserList.length " label="不合格人员" label-width="6em" prop="checkedPersonnelName">
68
+                    <el-input
69
+                      :value="getUserName( attr.checkUserList )" 
70
+                    />
71
+                  </el-form-item>
72
+                  <el-form-item v-if="attr.problemDescription" label="问题描述" label-width="6em"
73
+                    :prop="`checkProjectItemList.${attr.index}.problemDescription`">
74
+                    <el-input type="textarea" v-model="attr.problemDescription" />
75
+                  </el-form-item>
76
+                </div>
77
+              </div>
78
+            </el-col>
79
+            <el-col :span="24" v-if=" form.baseAttachmentList && form.baseAttachmentList.length ">
80
+              <div class="title">不合格照片</div>
81
+              <div class="imgList">
82
+                <ImageUpload :modelValue="transformImglistData( form.baseAttachmentList )" disabled />
83
+              </div>
84
+            </el-col>
85
+          </el-row>
86
+        </div>
87
+      </el-card>
88
+
89
+      <el-card header="整改要求" style="margin-bottom: 10px;">
90
+        <el-row :gutter="16">
91
+          <el-col :span="12">
92
+            <el-form-item label="负责人" prop="responsibleUserName">
93
+              <el-input v-model="form.responsibleUserName" :placeholder="disabled ? '-' : '请输入负责人'" />
94
+            </el-form-item>
95
+          </el-col>
96
+
97
+          <el-col :span="12">
98
+            <el-form-item label="整改期限" prop="rectificationDeadline">
99
+              <el-input v-model="form.rectificationDeadline" :placeholder="disabled ? '-' : '请输入整改期限'" />
100
+            </el-form-item>
101
+          </el-col>
102
+          <el-col :span="24">
103
+            <el-form-item label="整改要求" prop="rectificationSuggestions">
104
+              <el-input v-model="form.rectificationSuggestions" :placeholder="disabled ? '-' : '请输入整改要求'" />
105
+            </el-form-item>
106
+          </el-col>
107
+        </el-row>
108
+      </el-card>
109
+    </el-form>
110
+  </div>
111
+</template>
112
+
113
+<script setup>
114
+import { onMounted, reactive } from 'vue';
115
+import { listUser } from "@/api/system/user"
116
+const form = defineModel('form', reactive({}))
117
+const disabled = ref(true)
118
+
119
+const viewText = (...rest) => {
120
+  if (Array.isArray(rest) && rest.length) {
121
+    return rest.reduce((cur, acc) => {
122
+      if (acc) {
123
+        cur += cur ? `/${acc}` : acc
124
+      }
125
+      return cur
126
+    }, '')
127
+  }
128
+  return '-'
129
+}
130
+const listUserOption = ref([])
131
+const getUserInfo = () => {
132
+  listUser().then(res => {
133
+    listUserOption.value = res.rows.map((item) => ({
134
+      value: item.userId,
135
+      label: item.nickName
136
+    }))
137
+  })
138
+}
139
+
140
+const getUserName = (list) => {
141
+  if (Array.isArray(list) && list.length) {
142
+    return list.map(attr => {
143
+      return (listUserOption.value.find(item => item.value === attr.userId) || {}).label
144
+    }).filter(Boolean).join('、')
145
+  }
146
+  return '-'
147
+}
148
+
149
+const transformImglistData = (imgList) => {
150
+  if (Array.isArray(imgList) && imgList.length) {
151
+    return imgList.map(item => {
152
+      return {
153
+        ...item,
154
+        name: item.attachmentName,
155
+        url: item.attachmentUrl,
156
+      }
157
+    })
158
+  }
159
+  return []
160
+}
161
+
162
+onMounted(() => {
163
+  getUserInfo()
164
+})
165
+
166
+const groupHandler = (list) => {
167
+  if (Array.isArray(list) && list.length) {
168
+    return list.reduce((cur, acc, index) => {
169
+      const findIndex = cur.findIndex(item => item.categoryCodeOne === acc.categoryCodeOne && item.categoryCodeTwo === acc.categoryCodeTwo)
170
+      if (findIndex >= 0) {
171
+        cur[ findIndex ].list.push({ index, ...acc })
172
+      } else {
173
+        cur.push({
174
+          title: `${acc.categoryNameOne}/${acc.categoryNameTwo}`,
175
+          categoryCodeOne: acc.categoryCodeOne,
176
+          categoryCodeTwo: acc.categoryCodeTwo,
177
+          list: [ { index, ...acc } ]
178
+        })
179
+      }
180
+      return cur
181
+    }, [])
182
+  }
183
+  return []
184
+}
185
+
186
+const rules = reactive({})
187
+</script>
188
+
189
+<style lang="less" scoped>
190
+.contentWrap {
191
+  height: 75vh;
192
+  overflow-y: auto;
193
+  overflow-x: hidden;
194
+  padding: 0 10px;
195
+}
196
+
197
+:deep(.el-card) {
198
+  .el-card__header {
199
+    font-weight: 600;
200
+    font-size: 16px;
201
+    color: #000;
202
+  }
203
+}
204
+
205
+.title {
206
+  font-weight: 600;
207
+  color: #000;
208
+  margin-bottom: 10px;
209
+}
210
+
211
+.imgList {
212
+  display: flex;
213
+  flex-wrap: wrap;
214
+  row-gap: 15px;
215
+  column-gap: 15px;
216
+
217
+  .img-item {
218
+    width: 220px;
219
+    height: 120px;
220
+    border: 1px dashed #d9d9d9;
221
+    border-radius: 6px;
222
+    padding: 2px;
223
+    box-sizing: border-box;
224
+
225
+    img {
226
+      width: 100%;
227
+      height: 100%;
228
+      object-fit: contain;
229
+      border: none;
230
+      outline: none;
231
+    }
232
+  }
233
+}
234
+</style>

+ 390 - 0
src/views/check/checkRecord/index.vue

@@ -0,0 +1,390 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
4
+      <!-- <el-form-item label="检查单号" prop="documentCode">
5
+        <el-input
6
+          v-model="queryParams.documentCode"
7
+          placeholder="请输入检查单号"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item> -->
12
+
13
+      <!-- <el-form-item label="任务标题" prop="documentName">
14
+        <el-input v-model="queryParams.documentName" placeholder="请输入任务标题" clearable @keyup.enter="handleQuery" />
15
+      </el-form-item>
16
+
17
+      <el-form-item label="检查人" prop="checkerName">
18
+        <el-input v-model="queryParams.checkerName" placeholder="请输入检查人" clearable @keyup.enter="handleQuery" />
19
+      </el-form-item>
20
+
21
+      <el-form-item label="检查时间" prop="checkTime">
22
+        <el-date-picker clearable v-model="queryParams.checkTime" type="date" value-format="YYYY-MM-DD"
23
+          placeholder="请选择检查时间">
24
+        </el-date-picker>
25
+      </el-form-item> -->
26
+
27
+      <el-form-item label="任务标题" prop="taskName">
28
+        <el-input v-model="queryParams.taskName" placeholder="请输入任务标题" clearable style="width: 200px;" />
29
+      </el-form-item>
30
+
31
+      <el-form-item label="检查人" prop="checkerName">
32
+        <el-input v-model="queryParams.checkerName" placeholder="请输入检查人" clearable style="width: 200px;" />
33
+      </el-form-item>
34
+
35
+      <el-form-item label="检查时间" prop="checkTime">
36
+        <el-date-picker v-model="queryParams.checkTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择检查时间"
37
+          style="width: 200px;" clearable />
38
+      </el-form-item>
39
+      <!-- <el-form-item label="检查结果" prop="checkResult">
40
+         <el-select v-model="queryParams.checkResult" placeholder="请选择检查结果" clearable style="width: 120px">
41
+          <el-option
42
+            v-for="dict in check_result"
43
+            :key="dict.value"
44
+            :label="dict.label"
45
+            :value="dict.value"
46
+          />
47
+        </el-select>
48
+      </el-form-item> -->
49
+
50
+
51
+      <!-- <el-form-item label="严重程度" prop="severity">
52
+        <el-select v-model="queryParams.severity" placeholder="请选择严重程度" clearable style="width: 120px">
53
+          <el-option
54
+            v-for="dict in check_severity"
55
+            :key="dict.value"
56
+            :label="dict.label"
57
+            :value="dict.value"
58
+          />
59
+        </el-select>
60
+      </el-form-item>
61
+      <el-form-item label="整改状态" prop="correctionStatus">
62
+        <el-select v-model="queryParams.correctionStatus" placeholder="请选择整改状态" clearable style="width: 120px">
63
+          <el-option
64
+            v-for="dict in check_problem_corrections_status"
65
+            :key="dict.value"
66
+            :label="dict.label"
67
+            :value="dict.value"
68
+          />
69
+        </el-select>
70
+      </el-form-item> -->
71
+      <el-form-item>
72
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
73
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
74
+      </el-form-item>
75
+    </el-form>
76
+
77
+    <el-row :gutter="10" class="mb8">
78
+      <!-- <el-col :span="1.5">
79
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
80
+          v-hasPermi="['check:checkRecord:add']">新增</el-button>
81
+      </el-col>
82
+      <el-col :span="1.5">
83
+        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
84
+          v-hasPermi="['check:checkRecord:edit']">修改</el-button>
85
+      </el-col>
86
+      <el-col :span="1.5">
87
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
88
+          v-hasPermi="['check:checkRecord:remove']">删除</el-button>
89
+      </el-col> -->
90
+      <el-col :span="1.5">
91
+        <el-button type="warning" plain icon="Download" @click="handleExport"
92
+          v-hasPermi="['check:checkRecord:export']">导出</el-button>
93
+      </el-col>
94
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
95
+    </el-row>
96
+
97
+    <el-table v-loading="loading" :data="checkRecordList" @selection-change="handleSelectionChange">
98
+      <el-table-column type="selection" width="55" align="center" />
99
+      <el-table-column label="任务编号" align="center" prop="taskCode" width="180"/>
100
+      <el-table-column label="任务标题" align="center" prop="taskName" />
101
+      <el-table-column label="检查人" align="center" prop="checkerName" width="100"/>
102
+      <el-table-column label="检查时间" align="center" prop="checkTime" width="180">
103
+        <template #default="scope">
104
+          <span>{{ parseTime(scope.row.checkTime, '{y}-{m}-{d}') }}</span>
105
+        </template>
106
+      </el-table-column>
107
+      <el-table-column label="被检查级别" align="center" prop="checkedLevel" width="100">
108
+        <template #default="scope">
109
+          <dict-tag :options="check_checked_level" :value="scope.row.checkedLevel" />
110
+        </template>
111
+      </el-table-column>
112
+      <el-table-column label="被检查人/班组/科" align="center" prop="checkedLevel">
113
+        <template #default="scope">
114
+          <div v-if="scope.row.checkedLevel === 'DEPARTMENT_LEVEL'">{{ scope.row.checkedDepartmentName  }}</div>
115
+          <div v-if="scope.row.checkedLevel === 'TEAM_LEVEL'">{{ scope.row.checkedTeamName }}</div>
116
+          <div v-if="scope.row.checkedLevel === 'PERSONNEL_LEVEL'">{{ scope.row.checkedPersonnelName }}</div>
117
+        </template>  
118
+      </el-table-column>
119
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
120
+        <template #default="scope">
121
+          <!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
122
+            v-hasPermi="['check:checkRecord:edit']">修改</el-button>
123
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
124
+            v-hasPermi="['check:checkRecord:remove']">删除</el-button> -->
125
+          <el-button link type="info" icon="View" @click="handleDetail(scope.row)"
126
+            v-hasPermi="['check:checkRecord:query']">详情</el-button>
127
+        </template>
128
+      </el-table-column>
129
+    </el-table>
130
+
131
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
132
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
133
+
134
+    <!--详情 -->
135
+    <el-dialog :title="title" v-model="open" width="1100px" append-to-body>
136
+      <Details v-model:form="form" />
137
+      <template #footer>
138
+        <div class="dialog-footer">
139
+          <!-- <el-button type="primary" @click="submitForm">确 定</el-button> -->
140
+          <el-button @click="cancel">取 消</el-button>
141
+        </div>
142
+      </template>
143
+    </el-dialog>
144
+  </div>
145
+</template>
146
+
147
+<script setup name="CheckRecord">
148
+import { listCheckRecord, getCheckRecord, delCheckRecord, addCheckRecord, updateCheckRecord } from "@/api/check/checkRecord"
149
+import Details from './components/Details.vue'
150
+const { proxy } = getCurrentInstance()
151
+const { check_severity, check_checked_level, check_problem_corrections_status, check_result, check_record_type } = proxy.useDict('check_severity', 'check_checked_level', 'check_problem_corrections_status', 'check_result', 'check_record_type')
152
+
153
+const checkRecordList = ref([])
154
+const open = ref(false)
155
+const loading = ref(true)
156
+const showSearch = ref(true)
157
+const ids = ref([])
158
+const single = ref(true)
159
+const multiple = ref(true)
160
+const total = ref(0)
161
+const title = ref("")
162
+const positiveList = ref([])
163
+const negativeList = ref([])
164
+
165
+const data = reactive({
166
+  form: {},
167
+  queryParams: {
168
+    pageNum: 1,
169
+    pageSize: 10,
170
+    checkResult: null,
171
+    problemDescription: null,
172
+    attachmentUrl: null,
173
+    checkedLevel: null,
174
+    checkTime: null,
175
+    documentCode: null,
176
+    documentName: null,
177
+    rectificationSuggestions: null,
178
+    scoreTotal: null,
179
+    comprehensiveEvaluation: null,
180
+    rectificationDeadline: null,
181
+    checkerId: null,
182
+    checkerName: null,
183
+    type: null,
184
+    severity: null,
185
+    isNeedCorrection: null,
186
+    correctionStatus: null,
187
+    checkResultDesc: null,
188
+    checkedLevelDesc: null,
189
+    typeDesc: null,
190
+    severityDesc: null,
191
+    correctionStatusDesc: null,
192
+    attachmentId: null,
193
+    score: null,
194
+    attachmentName: null
195
+  }
196
+})
197
+
198
+const { queryParams, form } = toRefs(data)
199
+
200
+/** 查询检查记录列表 */
201
+function getList() {
202
+  loading.value = true
203
+  listCheckRecord(queryParams.value).then(response => {
204
+    checkRecordList.value = response.rows
205
+    total.value = response.total
206
+    loading.value = false
207
+  })
208
+}
209
+
210
+// 取消按钮
211
+function cancel() {
212
+  open.value = false
213
+  reset()
214
+}
215
+
216
+// 表单重置
217
+function reset() {
218
+  form.value = {
219
+    tenantId: null,
220
+    revision: null,
221
+    createBy: null,
222
+    createTime: null,
223
+    updateBy: null,
224
+    updateTime: null,
225
+    checkResult: null,
226
+    problemDescription: null,
227
+    attachmentUrl: null,
228
+    remark: null,
229
+    checkedLevel: null,
230
+    checkTime: null,
231
+    id: null,
232
+    documentCode: null,
233
+    documentName: null,
234
+    rectificationSuggestions: null,
235
+    scoreTotal: null,
236
+    comprehensiveEvaluation: null,
237
+    rectificationDeadline: null,
238
+    checkerId: null,
239
+    checkerName: null,
240
+    type: null,
241
+    severity: null,
242
+    isNeedCorrection: null,
243
+    correctionStatus: null,
244
+    checkResultDesc: null,
245
+    checkedLevelDesc: null,
246
+    typeDesc: null,
247
+    severityDesc: null,
248
+    correctionStatusDesc: null,
249
+    attachmentId: null,
250
+    score: null,
251
+    attachmentName: null
252
+  }
253
+  proxy.resetForm("checkRecordRef")
254
+}
255
+
256
+/** 搜索按钮操作 */
257
+function handleQuery() {
258
+  queryParams.value.pageNum = 1
259
+  getList()
260
+}
261
+
262
+/** 重置按钮操作 */
263
+function resetQuery() {
264
+  proxy.resetForm("queryRef")
265
+  handleQuery()
266
+}
267
+
268
+// 多选框选中数据
269
+function handleSelectionChange(selection) {
270
+  ids.value = selection.map(item => item.id)
271
+  single.value = selection.length != 1
272
+  multiple.value = !selection.length
273
+}
274
+
275
+/** 新增按钮操作 */
276
+function handleAdd() {
277
+  reset()
278
+  open.value = true
279
+  title.value = "添加检查记录"
280
+}
281
+
282
+/** 查看详情 */
283
+function handleDetail(row) {
284
+  reset()
285
+  const _id = row.id || ids.value
286
+  getCheckRecord(_id).then(response => {
287
+    form.value = response.data
288
+    const list = response.data.checkProjectItemList || []
289
+    // todo
290
+    positiveList.value = list.filter(i => i.evaluationType === '1')
291
+    negativeList.value = list.filter(i => i.evaluationType === '0')
292
+    open.value = true
293
+    title.value = "巡检单详情"
294
+  })
295
+}
296
+
297
+/** 修改 */
298
+function handleUpdate(row) {
299
+  reset()
300
+  const _id = row.id || ids.value
301
+  getCheckRecord(_id).then(response => {
302
+    form.value = response.data
303
+    open.value = true
304
+    title.value = "修改"
305
+  })
306
+}
307
+
308
+
309
+const viewText = (...rest) => {
310
+  if (Array.isArray(rest) && rest.length) {
311
+    return rest.reduce((cur, acc) => {
312
+      if (acc) {
313
+        cur += cur ? `/${acc}` : acc
314
+      }
315
+      return cur
316
+    }, '')
317
+  }
318
+  return '-'
319
+}
320
+
321
+/** 提交按钮 */
322
+function submitForm() {
323
+  proxy.$refs["checkRecordRef"].validate(valid => {
324
+    if (valid) {
325
+      if (form.value.id != null) {
326
+        updateCheckRecord(form.value).then(response => {
327
+          proxy.$modal.msgSuccess("修改成功")
328
+          open.value = false
329
+          getList()
330
+        })
331
+      } else {
332
+        addCheckRecord(form.value).then(response => {
333
+          proxy.$modal.msgSuccess("新增成功")
334
+          open.value = false
335
+          getList()
336
+        })
337
+      }
338
+    }
339
+  })
340
+}
341
+
342
+/** 删除按钮操作 */
343
+function handleDelete(row) {
344
+  const _ids = row.id || ids.value
345
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
346
+    return delCheckRecord(_ids)
347
+  }).then(() => {
348
+    getList()
349
+    proxy.$modal.msgSuccess("删除成功")
350
+  }).catch(() => { })
351
+}
352
+
353
+/** 导出按钮操作 */
354
+function handleExport() {
355
+  proxy.download('check/checkRecord/export', {
356
+    ...queryParams.value
357
+  }, `checkRecord_${new Date().getTime()}.xlsx`)
358
+}
359
+
360
+getList()
361
+</script>
362
+
363
+<style>
364
+.record-card {
365
+  margin-bottom: 12px;
366
+  padding: 8px 12px;
367
+  /* border-left: 4px solid #52c41a;    */
368
+  /* background: #f6ffed; */
369
+  border-radius: 4px;
370
+}
371
+
372
+.record-line {
373
+  display: flex;
374
+  margin-bottom: 4px;
375
+  font-size: 14px;
376
+  line-height: 1.5;
377
+}
378
+
379
+.label {
380
+  color: #666;
381
+  margin-right: 6px;
382
+  flex-shrink: 0;
383
+  width: 70px;
384
+}
385
+
386
+.value {
387
+  color: #333;
388
+  flex: 1;
389
+}
390
+</style>

+ 522 - 0
src/views/check/checkTask/index copy.vue

@@ -0,0 +1,522 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
4
+
5
+      <el-form-item label="任务名称" prop="taskName">
6
+        <el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable style="width: 200px;" />
7
+      </el-form-item>
8
+      <el-form-item label="被检查级别" prop="checkedLevel">
9
+        <el-select v-model="queryParams.checkedLevel" placeholder="请选择被检查级别" clearable style="width: 200px;">
10
+          <el-option v-for="dict in check_checked_level" :key="dict.value" :label="dict.label" :value="dict.value" />
11
+        </el-select>
12
+      </el-form-item>
13
+      <el-form-item label="任务状态" prop="status">
14
+        <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable style="width: 200px;">
15
+          <el-option v-for="dict in check_task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
16
+        </el-select>
17
+      </el-form-item>
18
+
19
+
20
+      <el-form-item>
21
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
22
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
23
+      </el-form-item>
24
+    </el-form>
25
+
26
+    <el-row :gutter="10" class="mb8">
27
+
28
+      <el-col :span="1.5">
29
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
30
+          v-hasPermi="['check:checkRecord:add']">新增</el-button>
31
+      </el-col>
32
+      <el-col :span="1.5">
33
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
34
+      </el-col>
35
+
36
+      <!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> -->
37
+    </el-row>
38
+
39
+    <el-table v-loading="loading" :data="checkRecordList" @selection-change="handleSelectionChange">
40
+      <el-table-column type="selection" width="55" align="center" />
41
+      <el-table-column label="任务编号" align="center" prop="taskCode" />
42
+      <el-table-column label="任务名称" align="center" prop="taskName" />
43
+      <el-table-column label="开始时间" align="center" prop="checkStartTime" />
44
+      <el-table-column label="结束时间" align="center" prop="checkEndTime" />
45
+      <el-table-column label="任务状态" align="center" prop="statusDesc">
46
+        <!-- <template #default="scope">
47
+          <div class="color-div">
48
+            <div class="round" :style="{ backgroundColor: scope.row.statusDesc === '已结束' ? '#6BC77F' : '#5EA4FF' }">
49
+            </div>
50
+            <div>{{ scope.row.statusDesc }}</div>
51
+          </div>
52
+  
53
+        </template> -->
54
+      </el-table-column>
55
+      <el-table-column label="发布人" align="center" prop="createBy" />
56
+      <el-table-column label="发布日期" align="center" prop="createTime">
57
+        <template #default="scope">
58
+          {{ moment(scope.row.createTime).format('YYYY-MM-DD') }}
59
+        </template>
60
+      </el-table-column>
61
+      <el-table-column label="被检查级别" align="center" prop="checkedLevelDesc">
62
+        <!-- <template #default="scope">
63
+          <dict-tag :options="check_checked_level" :value="scope.row.checkedLevel" />
64
+        </template> -->
65
+      </el-table-column>
66
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
67
+        <template #default="scope">
68
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"
69
+            v-hasPermi="['check:checkRecord:query']">详情</el-button>
70
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
71
+            v-hasPermi="['check:checkRecord:remove']">删除</el-button>
72
+        </template>
73
+      </el-table-column>
74
+    </el-table>
75
+
76
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
77
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
78
+
79
+    <!--详情 -->
80
+    <el-dialog :title="title" v-model="open" width="1000px" append-to-body>
81
+      <el-form ref="checkRecordRef" :model="form" :rules="rules" label-width="100px">
82
+        <el-row>
83
+          <el-col :span="12" v-if="title !== '巡检任务新建'">
84
+            <el-form-item label="任务编号" prop="taskCode">
85
+              <el-input v-model="form.taskCode" placeholder="请输入任务编号" :disabled="true" />
86
+            </el-form-item>
87
+          </el-col>
88
+          <el-col :span="12">
89
+            <el-form-item label="任务名称" prop="taskName">
90
+              <el-input v-model="form.taskName" placeholder="请输入任务名称" :disabled="formDisabled" />
91
+            </el-form-item>
92
+          </el-col>
93
+          <el-col :span="12">
94
+            <el-form-item label="任务时间" prop="timeRange">
95
+              <div style="display: flex; align-items: center;">
96
+                <el-date-picker v-model="form.timeRange" type="daterange" range-separator="至" start-placeholder="开始日期"
97
+                  end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 80%" :disabled="formDisabled" />
98
+                <span v-if="form.timeRange && form.timeRange.length === 2" style="margin-left: 10px">
99
+                  共 {{ calculateDayDiff(form.timeRange[0], form.timeRange[1]) }} 天
100
+                </span>
101
+              </div>
102
+            </el-form-item>
103
+          </el-col>
104
+          <el-col :span="12">
105
+            <el-form-item label="检查级别" prop="checkLevel">
106
+              <el-select v-model="form.checkLevel" placeholder="请选择检查级别" style="width: 100%"
107
+                @change="handleCheckLevelChange" :disabled="formDisabled">
108
+                <el-option v-for="dict in check_level" :key="dict.value" :label="dict.label" :value="dict.value" />
109
+              </el-select>
110
+            </el-form-item>
111
+          </el-col>
112
+          <el-col :span="12">
113
+            <el-form-item label="被检查级别" prop="checkedLevel">
114
+              <el-select v-model="form.checkedLevel" placeholder="请选择被检查级别" :disabled="true" style="width: 100%">
115
+                <el-option v-for="dict in check_checked_level" :key="dict.value" :label="dict.label"
116
+                  :value="dict.value" />
117
+              </el-select>
118
+            </el-form-item>
119
+          </el-col>
120
+
121
+
122
+          <el-col :span="12">
123
+            <el-form-item label="任务类型" prop="checkCategory">
124
+              <el-select v-model="form.checkCategory" placeholder="请选择任务类型" style="width: 100%"
125
+                :disabled="formDisabled">
126
+                <el-option v-for="dict in check_task_category" :key="dict.value" :label="dict.label"
127
+                  :value="dict.value" />
128
+              </el-select>
129
+            </el-form-item>
130
+          </el-col>
131
+          <el-col :span="12">
132
+            <el-form-item label="执行频率" prop="ruleType" :required="true">
133
+              <div style="display: flex;width: 100%;">
134
+                <el-select v-model="form.ruleType" placeholder="请选择执行频率" style="width: 100%" :disabled="formDisabled">
135
+                  <el-option v-for="dict in check_rule_type" :key="dict.value" :label="dict.label"
136
+                    :value="dict.value" />
137
+                </el-select>
138
+                <el-input-number v-model="form.ruleTypeNum" :default-value="1" :min="1" style="margin-left: 10px"
139
+                  :disabled="formDisabled" />
140
+                <span style="margin-left: 10px">次</span>
141
+              </div>
142
+            </el-form-item>
143
+          </el-col>
144
+          <el-col :span="12">
145
+            <el-form-item label="是否科内自查" prop="isSelfCheck">
146
+              <el-radio-group v-model="form.isSelfCheck" :disabled="formDisabled">
147
+                <el-radio :value="1">是</el-radio>
148
+                <el-radio :value="0">否</el-radio>
149
+              </el-radio-group>
150
+            </el-form-item>
151
+          </el-col>
152
+        </el-row>
153
+        <el-row :gutter="20">
154
+          <el-col :span="24">
155
+            <el-form-item label="任务简介" prop="description">
156
+              <el-input v-model="form.description" type="textarea" placeholder="请输入任务简介" :disabled="formDisabled" />
157
+            </el-form-item>
158
+          </el-col>
159
+        </el-row>
160
+        <el-row :gutter="20">
161
+          <el-col :span="24">
162
+            <el-form-item label="检查项目" prop="checkProjectItemList">
163
+              <div v-for="(item, index) in checkItemsList" :key="index" style="margin-bottom: 20px;">
164
+                <el-tree :ref="`tree_${index}`" :data="[item]" :props="{ children: 'list', label: 'name' }"
165
+                  show-checkbox node-key="code" :default-expand-all="true" :expand-on-click-node="false"
166
+                  :default-checked-keys="checkedCodes" :disabled="formDisabled">
167
+                  <template #default="{ node, data }">
168
+                    <span>{{ node.label }}</span>
169
+                  </template>
170
+                </el-tree>
171
+              </div>
172
+            </el-form-item>
173
+          </el-col>
174
+        </el-row>
175
+      </el-form>
176
+      <template #footer>
177
+        <div class="dialog-footer">
178
+          <el-button v-if="!formDisabled" type="primary" @click="submitForm">确 定</el-button>
179
+          <el-button @click="cancel">取 消</el-button>
180
+        </div>
181
+      </template>
182
+    </el-dialog>
183
+  </div>
184
+</template>
185
+
186
+<script setup name="CheckTask">
187
+import { listCheckTask, getCheckTask, delCheckTask, addCheckTask, getCheckProjectItemList } from "@/api/check/checkTask"
188
+import { computed } from "vue"
189
+import moment from 'moment'
190
+const { proxy } = getCurrentInstance()
191
+const { check_checked_level, check_task_status, check_level, check_rule_type, check_task_category } = proxy.useDict('check_checked_level', 'check_task_status', 'check_level', 'check_rule_type', 'check_task_category')
192
+
193
+
194
+/** 计算日期差 */
195
+const calculateDayDiff = (startDate, endDate) => {
196
+  const start = new Date(startDate)
197
+  const end = new Date(endDate)
198
+  const diffTime = Math.abs(end - start)
199
+  return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
200
+}
201
+
202
+const checkRecordList = ref([])
203
+const open = ref(false)
204
+const loading = ref(true)
205
+const showSearch = ref(true)
206
+const ids = ref([])
207
+const single = ref(true)
208
+const multiple = ref(true)
209
+const total = ref(0)
210
+const title = ref("")
211
+
212
+
213
+const formDisabled = computed(() => {
214
+  return title.value === '巡检任务详情'
215
+})
216
+
217
+const data = reactive({
218
+  form: {
219
+    isSelfCheck: 0,
220
+    timeRange: [],
221
+    ruleTypeNum: 1,
222
+    checkProjectItemList: []
223
+  },
224
+  checkItemsList: [],
225
+  checkedCodes: [],//回显选中的节点
226
+  queryParams: {
227
+    pageNum: 1,
228
+    pageSize: 10,
229
+    checkResult: null,
230
+    problemDescription: null,
231
+    attachmentUrl: null,
232
+    checkedLevel: null,
233
+    documentCode: null,
234
+    taskName: null,
235
+    rectificationSuggestions: null,
236
+    comprehensiveEvaluation: null,
237
+    rectificationDeadline: null,
238
+    correctionStatus: null,
239
+    checkResultDesc: null,
240
+    correctionStatusDesc: null,
241
+    attachmentId: null,
242
+    score: null,
243
+    attachmentName: null
244
+  },
245
+  rules: {
246
+    taskCode: [
247
+      { required: false, message: "请输入任务编号", trigger: "blur" }
248
+    ],
249
+    taskName: [
250
+      { required: true, message: "请输入任务名称", trigger: "blur" }
251
+    ],
252
+    timeRange: [
253
+      { required: true, message: "请选择任务时间范围", trigger: "change" }
254
+    ],
255
+    checkLevel: [
256
+      { required: true, message: "请选择检查级别", trigger: "change" }
257
+    ],
258
+    checkedLevel: [
259
+      { required: true, message: "请选择被检查级别", trigger: "change" }
260
+    ],
261
+    ruleType: [
262
+      { required: true, message: "请选择执行频率", trigger: "change" }
263
+    ],
264
+    ruleTypeNum: [
265
+      { required: true, message: "请输入频率值", trigger: "change" },
266
+    ],
267
+    checkCategory: [
268
+      { required: true, message: "请选择任务类型", trigger: "change" }
269
+    ],
270
+    description: [
271
+      { required: true, message: "请输入任务简介", trigger: "blur" }
272
+    ],
273
+  }
274
+})
275
+
276
+const { queryParams, form, rules, checkItemsList, checkedCodes } = toRefs(data)
277
+
278
+const handleCheckLevelChange = (val) => {
279
+  if ('STATION_LEVEL' == val) {
280
+    form.value.checkedLevel = 'DEPARTMENT_LEVEL'
281
+  }
282
+  if ('DEPARTMENT_LEVEL' == val) {
283
+    form.value.checkedLevel = 'TEAM_LEVEL'
284
+  }
285
+  if ('TEAM_LEVEL' == val) {
286
+    form.value.checkedLevel = 'PERSONNEL_LEVEL'
287
+  }
288
+  getCheckItem(val)
289
+}
290
+
291
+function getCheckItem(level) {
292
+  getCheckProjectItemList(level).then(response => {
293
+    let res = response.data
294
+    let disabledList = res.map(item => ({
295
+      ...item,
296
+      disabled: true,
297
+      list: item.list?.map(child => ({
298
+        ...child,
299
+        disabled: true,
300
+        list: child.list?.map(grandChild => ({
301
+          ...grandChild,
302
+          disabled: true,
303
+        }))
304
+      }))
305
+    }))
306
+    checkItemsList.value = formDisabled.value ? disabledList : res;
307
+    checkedCodes.value = res.flatMap(item => item.list?.flatMap(child => child.list?.map(grandChild => grandChild.code)) || [])
308
+
309
+  })
310
+
311
+}
312
+
313
+
314
+/** 查询检查记录列表 */
315
+function getList() {
316
+  loading.value = true
317
+  listCheckTask(queryParams.value).then(response => {
318
+    checkRecordList.value = response.rows
319
+    total.value = response.total
320
+    loading.value = false
321
+  })
322
+}
323
+
324
+// 取消按钮
325
+function cancel() {
326
+  open.value = false
327
+  reset()
328
+}
329
+
330
+// 表单重置
331
+function reset() {
332
+  form.value = {
333
+    isSelfCheck: 0,
334
+    timeRange: [],
335
+    ruleTypeNum: 1,
336
+    checkProjectItemList: []
337
+  }
338
+  proxy.resetForm("checkRecordRef")
339
+}
340
+
341
+/** 搜索按钮操作 */
342
+function handleQuery() {
343
+  queryParams.value.pageNum = 1
344
+  getList()
345
+}
346
+
347
+/** 重置按钮操作 */
348
+function resetQuery() {
349
+  proxy.resetForm("queryRef")
350
+  handleQuery()
351
+}
352
+
353
+// 多选框选中数据
354
+function handleSelectionChange(selection) {
355
+  ids.value = selection.map(item => item.id)
356
+  single.value = selection.length != 1
357
+  multiple.value = !selection.length
358
+}
359
+
360
+/** 新增按钮操作 */
361
+function handleAdd() {
362
+  reset()
363
+  open.value = true
364
+  title.value = "巡检任务新建"
365
+  getCheckItem()
366
+}
367
+
368
+/** 查看详情 */
369
+function handleDetail(row) {
370
+  reset()
371
+  const _id = row.id || ids.value
372
+  getCheckTask(_id).then(response => {
373
+    let res = response.data;
374
+
375
+    form.value = {
376
+      ...res,
377
+      timeRange: [res.checkStartTime, res.checkEndTime]
378
+    }
379
+    const list = response.data.checkProjectItemList || [];
380
+
381
+
382
+
383
+    // 根据checkProjectItemList回显el-tree选中状态
384
+    checkedCodes.value = list.map(item => item.projectCode);
385
+
386
+
387
+    open.value = true
388
+    title.value = "巡检任务详情"
389
+    getCheckItem(res.checkLevel)
390
+  })
391
+}
392
+
393
+
394
+
395
+/** 提交按钮 */
396
+function submitForm() {
397
+  proxy.$refs["checkRecordRef"].validate(valid => {
398
+    if (valid) {
399
+      const selectedItems = []
400
+      const treeElements = document.querySelectorAll('.el-tree')
401
+      treeElements.forEach((tree, index) => {
402
+        const treeInstance = proxy.$refs[`tree_${index}`][0]
403
+
404
+        const checkedNodes = treeInstance.getCheckedNodes(true)
405
+
406
+        checkedNodes.forEach(node => {
407
+          const path = []
408
+
409
+          // 从checkItemsList中查找当前节点及其父节点
410
+          const findNodeAndParents = (items, targetCode) => {
411
+            for (const item of items) {
412
+              if (item.code === targetCode) {
413
+                path.unshift({
414
+                  code: item.code,
415
+                  name: item.name
416
+                })
417
+                return true
418
+              }
419
+              if (item.list && item.list.length > 0) {
420
+                if (findNodeAndParents(item.list, targetCode)) {
421
+                  path.unshift({
422
+                    code: item.code,
423
+                    name: item.name
424
+                  })
425
+                  return true
426
+                }
427
+              }
428
+            }
429
+            return false
430
+          }
431
+
432
+          findNodeAndParents(checkItemsList.value, node.code)
433
+
434
+          selectedItems.push({
435
+            projectCode: node.code,
436
+            projectName: node.name,
437
+            categoryCodeOne: path[0]?.code || '',
438
+            categoryNameOne: path[0]?.name || '',
439
+            categoryCodeTwo: path[1]?.code || '',
440
+            categoryNameTwo: path[1]?.name || '',
441
+          })
442
+        })
443
+      })
444
+
445
+      let res = {
446
+        ...form.value,
447
+        checkStartTime: form.value.timeRange[0],
448
+        checkEndTime: form.value.timeRange[1],
449
+        checkLevelDesc: check_level.value.find(item => item.value == form.value.checkLevel)?.label,
450
+        checkedLevelDesc: check_level.value.find(item => item.value == form.value.checkedLevel)?.label,
451
+        checkCategoryDesc: check_task_category.value.find(item => item.value == form.value.checkCategory)?.label,
452
+        ruleTypeDesc: check_rule_type.value.find(item => item.value == form.value.ruleType)?.label,
453
+        checkProjectItemList: selectedItems
454
+      }
455
+      console.log(res, "res")
456
+      debugger
457
+      addCheckTask(res).then(response => {
458
+        proxy.$modal.msgSuccess("新增成功")
459
+        open.value = false
460
+        getList()
461
+      })
462
+    }
463
+  })
464
+}
465
+
466
+/** 删除按钮操作 */
467
+function handleDelete(row) {
468
+  const _ids = row.id || ids.value
469
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
470
+    return delCheckTask(_ids)
471
+  }).then(() => {
472
+    getList()
473
+    proxy.$modal.msgSuccess("删除成功")
474
+  }).catch(() => { })
475
+}
476
+
477
+
478
+getList()
479
+</script>
480
+
481
+<style scoped>
482
+.record-card {
483
+  margin-bottom: 12px;
484
+  padding: 8px 12px;
485
+  /* border-left: 4px solid #52c41a;    */
486
+  /* background: #f6ffed; */
487
+  border-radius: 4px;
488
+}
489
+
490
+.round {
491
+  width: 8px;
492
+  height: 8px;
493
+  border-radius: 50%;
494
+  margin-right: 5px;
495
+}
496
+
497
+.color-div {
498
+  display: flex;
499
+  flex-direction: row;
500
+  align-items: center;
501
+  justify-content: center;
502
+}
503
+
504
+.record-line {
505
+  display: flex;
506
+  margin-bottom: 4px;
507
+  font-size: 14px;
508
+  line-height: 1.5;
509
+}
510
+
511
+.label {
512
+  color: #666;
513
+  margin-right: 6px;
514
+  flex-shrink: 0;
515
+  width: 70px;
516
+}
517
+
518
+.value {
519
+  color: #333;
520
+  flex: 1;
521
+}
522
+</style>

+ 579 - 0
src/views/check/checkTask/index.vue

@@ -0,0 +1,579 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
4
+
5
+      <el-form-item label="任务名称" prop="taskName">
6
+        <el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable style="width: 200px;" />
7
+      </el-form-item>
8
+      <el-form-item label="被检查级别" prop="checkedLevel">
9
+        <el-select v-model="queryParams.checkedLevel" placeholder="请选择被检查级别" clearable style="width: 200px;">
10
+          <el-option v-for="dict in check_checked_level" :key="dict.value" :label="dict.label" :value="dict.value" />
11
+        </el-select>
12
+      </el-form-item>
13
+      <el-form-item label="任务状态" prop="status">
14
+        <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable style="width: 200px;">
15
+          <el-option v-for="dict in check_task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
16
+        </el-select>
17
+      </el-form-item>
18
+
19
+
20
+      <el-form-item>
21
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
22
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
23
+      </el-form-item>
24
+    </el-form>
25
+
26
+    <el-row :gutter="10" class="mb8">
27
+
28
+      <el-col :span="1.5">
29
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
30
+          v-hasPermi="['check:checkRecord:add']">新增</el-button>
31
+      </el-col>
32
+      <el-col :span="1.5">
33
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
34
+      </el-col>
35
+
36
+      <!-- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> -->
37
+    </el-row>
38
+
39
+    <el-table v-loading="loading" :data="checkRecordList" @selection-change="handleSelectionChange">
40
+      <el-table-column type="selection" width="55" align="center" />
41
+      <el-table-column label="任务编号" align="center" prop="taskCode" />
42
+      <el-table-column label="任务名称" align="center" prop="taskName" />
43
+      <el-table-column label="开始时间" align="center" prop="checkStartTime" />
44
+      <el-table-column label="结束时间" align="center" prop="checkEndTime" />
45
+      <el-table-column label="任务状态" align="center" prop="statusDesc">
46
+        <!-- <template #default="scope">
47
+          <div class="color-div">
48
+            <div class="round" :style="{ backgroundColor: scope.row.statusDesc === '已结束' ? '#6BC77F' : '#5EA4FF' }">
49
+            </div>
50
+            <div>{{ scope.row.statusDesc }}</div>
51
+          </div>
52
+  
53
+        </template> -->
54
+      </el-table-column>
55
+      <el-table-column label="发布人" align="center" prop="createBy" />
56
+      <el-table-column label="发布日期" align="center" prop="createTime">
57
+        <template #default="scope">
58
+          {{ moment(scope.row.createTime).format('YYYY-MM-DD') }}
59
+        </template>
60
+      </el-table-column>
61
+      <el-table-column label="被检查级别" align="center" prop="checkedLevelDesc">
62
+        <!-- <template #default="scope">
63
+          <dict-tag :options="check_checked_level" :value="scope.row.checkedLevel" />
64
+        </template> -->
65
+      </el-table-column>
66
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
67
+        <template #default="scope">
68
+          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"
69
+            v-hasPermi="['check:checkRecord:query']">详情</el-button>
70
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
71
+            v-hasPermi="['check:checkRecord:remove']">删除</el-button>
72
+        </template>
73
+      </el-table-column>
74
+    </el-table>
75
+
76
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
77
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
78
+
79
+    <!--详情 -->
80
+    <el-dialog :title="title" v-model="open" width="1000px" append-to-body>
81
+      <el-form ref="checkRecordRef" :model="form" :rules="rules" label-width="100px">
82
+        <el-row>
83
+          <el-col :span="12" v-if="title !== '巡检任务新建'">
84
+            <el-form-item label="任务编号" prop="taskCode">
85
+              <el-input v-model="form.taskCode" placeholder="请输入任务编号" :disabled="true" />
86
+            </el-form-item>
87
+          </el-col>
88
+          <el-col :span="12">
89
+            <el-form-item label="任务名称" prop="taskName">
90
+              <el-input v-model="form.taskName" placeholder="请输入任务名称" :disabled="formDisabled" />
91
+            </el-form-item>
92
+          </el-col>
93
+          <el-col :span="12">
94
+            <el-form-item label="任务时间" prop="timeRange">
95
+              <div style="display: flex; align-items: center;">
96
+                <el-date-picker v-model="form.timeRange" type="daterange" range-separator="至" start-placeholder="开始日期"
97
+                  end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 80%" :disabled="formDisabled" />
98
+                <span v-if="form.timeRange && form.timeRange.length === 2" style="margin-left: 10px">
99
+                  共 {{ calculateDayDiff(form.timeRange[0], form.timeRange[1]) }} 天
100
+                </span>
101
+              </div>
102
+            </el-form-item>
103
+          </el-col>
104
+          <el-col :span="12">
105
+            <el-form-item label="检查级别" prop="checkLevel">
106
+              <el-select v-model="form.checkLevel" placeholder="请选择检查级别" style="width: 100%"
107
+                @change="handleCheckLevelChange" :disabled="formDisabled">
108
+                <el-option v-for="dict in check_level" :key="dict.value" :label="dict.label" :value="dict.value" />
109
+              </el-select>
110
+            </el-form-item>
111
+          </el-col>
112
+          <el-col :span="12">
113
+            <el-form-item label="被检查级别" prop="checkedLevel">
114
+              <el-select v-model="form.checkedLevel" placeholder="请选择被检查级别" :disabled="true" style="width: 100%">
115
+                <el-option v-for="dict in check_checked_level" :key="dict.value" :label="dict.label"
116
+                  :value="dict.value" />
117
+              </el-select>
118
+            </el-form-item>
119
+          </el-col>
120
+
121
+
122
+          <el-col :span="12">
123
+            <el-form-item label="任务类型" prop="checkCategory">
124
+              <el-select v-model="form.checkCategory" placeholder="请选择任务类型" style="width: 100%"
125
+                :disabled="formDisabled">
126
+                <el-option v-for="dict in check_task_category" :key="dict.value" :label="dict.label"
127
+                  :value="dict.value" />
128
+              </el-select>
129
+            </el-form-item>
130
+          </el-col>
131
+          <el-col :span="12">
132
+            <el-form-item label="执行频率" prop="ruleType" :required="true">
133
+              <div style="display: flex;width: 100%;">
134
+                <el-select v-model="form.ruleType" placeholder="请选择执行频率" style="width: 100%" :disabled="true">
135
+                  <el-option v-for="dict in check_rule_type" :key="dict.value" :label="dict.label"
136
+                    :value="dict.value" />
137
+                </el-select>
138
+                <el-input-number v-model="form.ruleTypeNum" :default-value="1" :min="1" style="margin-left: 10px"
139
+                  :disabled="formDisabled" />
140
+                <span style="margin-left: 10px">次</span>
141
+              </div>
142
+            </el-form-item>
143
+          </el-col>
144
+          <el-col :span="12">
145
+            <el-form-item label="是否科内自查" prop="isSelfCheck">
146
+              <el-radio-group v-model="form.isSelfCheck" :disabled="formDisabled">
147
+                <el-radio :value="1">是</el-radio>
148
+                <el-radio :value="0">否</el-radio>
149
+              </el-radio-group>
150
+            </el-form-item>
151
+          </el-col>
152
+          <el-col :span="12" v-if="form.isSelfCheck === 1">
153
+            <el-form-item label="自查科室" prop="selfCheckDeptId" :required="true">
154
+              <el-select v-model="form.selfCheckDeptId" placeholder="请选择自查科室" style="width: 100%" :disabled="formDisabled">
155
+                <el-option v-for="dept in deptOptions" :key="dept.deptId" :label="dept.deptName" :value="dept.deptId" />
156
+              </el-select>
157
+            </el-form-item>
158
+          </el-col>
159
+        </el-row>
160
+        <el-row :gutter="20">
161
+          <el-col :span="24">
162
+            <el-form-item label="任务简介" prop="description">
163
+              <el-input v-model="form.description" type="textarea" placeholder="请输入任务简介" :disabled="formDisabled" />
164
+            </el-form-item>
165
+          </el-col>
166
+        </el-row>
167
+        <el-row :gutter="20">
168
+          <el-col :span="24">
169
+            <el-form-item label="检查项目" prop="checkProjectItemList">
170
+              <el-tree style="width: 100%;" ref="tree" :data="checkItemsList"
171
+                :props="{ children: 'list', label: 'name' }" show-checkbox node-key="code" :default-expand-all="true"
172
+                :expand-on-click-node="false" :default-checked-keys="checkedCodes" :disabled="formDisabled">
173
+                <template #default="{ node, data }">
174
+                  <span>{{ node.label }}</span>
175
+                </template>
176
+              </el-tree>
177
+            </el-form-item>
178
+          </el-col>
179
+        </el-row>
180
+      </el-form>
181
+      <template #footer>
182
+        <div class="dialog-footer">
183
+          <el-button v-if="!formDisabled" type="primary" @click="submitForm">确 定</el-button>
184
+          <el-button @click="cancel">取 消</el-button>
185
+        </div>
186
+      </template>
187
+    </el-dialog>
188
+  </div>
189
+</template>
190
+
191
+<script setup name="CheckTask">
192
+import { listCheckTask, getCheckTask, delCheckTask, addCheckTask, getCheckProjectItemList } from "@/api/check/checkTask"
193
+import { listDept } from "@/api/system/dept"
194
+import { computed } from "vue"
195
+import moment from 'moment'
196
+
197
+const { proxy } = getCurrentInstance()
198
+const { check_checked_level, check_task_status, check_level, check_rule_type, check_task_category } = proxy.useDict('check_checked_level', 'check_task_status', 'check_level', 'check_rule_type', 'check_task_category')
199
+
200
+
201
+/** 计算日期差 */
202
+const calculateDayDiff = (startDate, endDate) => {
203
+  const start = new Date(startDate)
204
+  const end = new Date(endDate)
205
+  const diffTime = Math.abs(end - start)
206
+  return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
207
+}
208
+
209
+const checkRecordList = ref([])
210
+const open = ref(false)
211
+const loading = ref(true)
212
+const showSearch = ref(true)
213
+const ids = ref([])
214
+const single = ref(true)
215
+const multiple = ref(true)
216
+const total = ref(0)
217
+const title = ref("")
218
+const deptOptions = ref([])
219
+
220
+
221
+const formDisabled = computed(() => {
222
+  return title.value === '巡检任务详情'
223
+})
224
+
225
+const data = reactive({
226
+  form: {
227
+    isSelfCheck: 0,
228
+    selfCheckDeptId: "",
229
+    timeRange: [],
230
+    ruleType: "DAY",
231
+    ruleTypeNum: 1,
232
+    checkProjectItemList: []
233
+  },
234
+  checkItemsList: [],
235
+  checkedCodes: [],//回显选中的节点
236
+  queryParams: {
237
+    pageNum: 1,
238
+    pageSize: 10,
239
+    checkResult: null,
240
+    problemDescription: null,
241
+    attachmentUrl: null,
242
+    checkedLevel: null,
243
+    documentCode: null,
244
+    taskName: null,
245
+    rectificationSuggestions: null,
246
+    comprehensiveEvaluation: null,
247
+    rectificationDeadline: null,
248
+    correctionStatus: null,
249
+    checkResultDesc: null,
250
+    correctionStatusDesc: null,
251
+    attachmentId: null,
252
+    score: null,
253
+    attachmentName: null
254
+  },
255
+  rules: {
256
+    taskCode: [
257
+      { required: false, message: "请输入任务编号", trigger: "blur" }
258
+    ],
259
+    taskName: [
260
+      { required: true, message: "请输入任务名称", trigger: "blur" }
261
+    ],
262
+    timeRange: [
263
+      { required: true, message: "请选择任务时间范围", trigger: "change" }
264
+    ],
265
+    checkLevel: [
266
+      { required: true, message: "请选择检查级别", trigger: "change" }
267
+    ],
268
+    checkedLevel: [
269
+      { required: true, message: "请选择被检查级别", trigger: "change" }
270
+    ],
271
+    ruleType: [
272
+      { required: true, message: "请选择执行频率", trigger: "change" }
273
+    ],
274
+    ruleTypeNum: [
275
+      { required: true, message: "请输入频率值", trigger: "change" },
276
+    ],
277
+    checkCategory: [
278
+      { required: true, message: "请选择任务类型", trigger: "change" }
279
+    ],
280
+    description: [
281
+      { required: true, message: "请输入任务简介", trigger: "blur" }
282
+    ],
283
+    selfCheckDeptId: [
284
+      { required: true, message: "请选择自查科室", trigger: "change" }
285
+    ],
286
+  }
287
+})
288
+
289
+const { queryParams, form, rules, checkItemsList, checkedCodes } = toRefs(data)
290
+
291
+const handleCheckLevelChange = (val) => {
292
+  if ('STATION_LEVEL' == val) {
293
+    form.value.checkedLevel = 'DEPARTMENT_LEVEL'
294
+  }
295
+  if ('DEPARTMENT_LEVEL' == val) {
296
+    form.value.checkedLevel = 'TEAM_LEVEL'
297
+  }
298
+  if ('TEAM_LEVEL' == val) {
299
+    form.value.checkedLevel = 'PERSONNEL_LEVEL'
300
+  }
301
+  getCheckItem(val)
302
+}
303
+
304
+function getCheckItem(level) {
305
+  if (!level) {
306
+    return
307
+  }
308
+  getCheckProjectItemList(level).then(response => {
309
+    let res = response.data
310
+
311
+    if (formDisabled.value) {
312
+      // 详情查看模式:只保留被选中的节点,并禁用所有节点
313
+      const filteredList = res.map(item => {
314
+        // 过滤二级节点
315
+        const filteredChildren = item.list?.map(child => {
316
+          // 过滤三级节点
317
+          const filteredGrandChildren = child.list?.filter(grandChild =>
318
+            checkedCodes.value.includes(grandChild.code)
319
+          )
320
+
321
+          // 如果三级节点有选中的,保留该二级节点
322
+          if (filteredGrandChildren && filteredGrandChildren.length > 0) {
323
+            return {
324
+              ...child,
325
+              disabled: true,
326
+              list: filteredGrandChildren.map(grandChild => ({
327
+                ...grandChild,
328
+                disabled: true,
329
+              }))
330
+            }
331
+          }
332
+          return null
333
+        }).filter(Boolean)
334
+
335
+        // 如果二级节点有选中的,保留该一级节点
336
+        if (filteredChildren && filteredChildren.length > 0) {
337
+          return {
338
+            ...item,
339
+            disabled: true,
340
+            list: filteredChildren
341
+          }
342
+        }
343
+        return null
344
+      }).filter(Boolean)
345
+
346
+      checkItemsList.value = filteredList
347
+    } else {
348
+      // 编辑模式:显示所有节点
349
+      checkItemsList.value = res
350
+      checkedCodes.value = res.flatMap(item => item.list?.flatMap(child => child.list?.map(grandChild => grandChild.code)) || [])
351
+    }
352
+  })
353
+}
354
+
355
+
356
+/** 获取科室列表 */
357
+function getDeptList() {
358
+  listDept().then(response => {
359
+    deptOptions.value = response.data.filter(item => item.deptType === 'DEPARTMENT') || []
360
+  })
361
+}
362
+
363
+/** 查询检查记录列表 */
364
+function getList() {
365
+  loading.value = true
366
+  listCheckTask(queryParams.value).then(response => {
367
+    checkRecordList.value = response.rows
368
+    total.value = response.total
369
+    loading.value = false
370
+  })
371
+}
372
+
373
+getList()
374
+getDeptList()
375
+
376
+// 取消按钮
377
+function cancel() {
378
+  open.value = false
379
+  reset()
380
+}
381
+
382
+// 表单重置
383
+function reset() {
384
+  form.value = {
385
+    isSelfCheck: 0,
386
+    timeRange: [],
387
+    ruleType: "DAY",
388
+    ruleTypeNum: 1,
389
+    checkProjectItemList: []
390
+  }
391
+  checkItemsList.value = []
392
+  checkedCodes.value = []
393
+  proxy.resetForm("checkRecordRef")
394
+}
395
+
396
+/** 搜索按钮操作 */
397
+function handleQuery() {
398
+  queryParams.value.pageNum = 1
399
+  getList()
400
+}
401
+
402
+/** 重置按钮操作 */
403
+function resetQuery() {
404
+  proxy.resetForm("queryRef")
405
+  handleQuery()
406
+}
407
+
408
+// 多选框选中数据
409
+function handleSelectionChange(selection) {
410
+  ids.value = selection.map(item => item.id)
411
+  single.value = selection.length != 1
412
+  multiple.value = !selection.length
413
+}
414
+
415
+/** 新增按钮操作 */
416
+function handleAdd() {
417
+  reset()
418
+  open.value = true
419
+  title.value = "巡检任务新建"
420
+  getCheckItem()
421
+}
422
+
423
+/** 查看详情 */
424
+function handleDetail(row) {
425
+  reset()
426
+  const _id = row.id || ids.value
427
+  getCheckTask(_id).then(response => {
428
+    let res = response.data;
429
+
430
+    form.value = {
431
+      ...res,
432
+      timeRange: [res.checkStartTime, res.checkEndTime]
433
+    }
434
+    const list = response.data.checkProjectItemList || [];
435
+
436
+
437
+
438
+    // 根据checkProjectItemList回显el-tree选中状态
439
+    checkedCodes.value = list.map(item => item.projectCode);
440
+    console.log(checkedCodes.value, "checkedCodes.value")
441
+
442
+    open.value = true
443
+    title.value = "巡检任务详情"
444
+    getCheckItem(res.checkLevel)
445
+  })
446
+}
447
+
448
+
449
+
450
+/** 提交按钮 */
451
+function submitForm() {
452
+  proxy.$refs["checkRecordRef"].validate(valid => {
453
+    if (valid) {
454
+      const selectedItems = []
455
+
456
+      const treeInstance = proxy.$refs.tree
457
+
458
+      const checkedNodes = treeInstance.getCheckedNodes(true)
459
+
460
+      checkedNodes.forEach(node => {
461
+        const path = []
462
+
463
+        // 从checkItemsList中查找当前节点及其父节点
464
+        const findNodeAndParents = (items, targetCode) => {
465
+          for (const item of items) {
466
+            if (item.code === targetCode) {
467
+              path.unshift({
468
+                code: item.code,
469
+                name: item.name
470
+              })
471
+              return true
472
+            }
473
+            if (item.list && item.list.length > 0) {
474
+              if (findNodeAndParents(item.list, targetCode)) {
475
+                path.unshift({
476
+                  code: item.code,
477
+                  name: item.name
478
+                })
479
+                return true
480
+              }
481
+            }
482
+          }
483
+          return false
484
+        }
485
+
486
+        findNodeAndParents(checkItemsList.value, node.code)
487
+
488
+        selectedItems.push({
489
+          projectCode: node.code,
490
+          projectName: node.name,
491
+          categoryCodeOne: path[0]?.code || '',
492
+          categoryNameOne: path[0]?.name || '',
493
+          categoryCodeTwo: path[1]?.code || '',
494
+          categoryNameTwo: path[1]?.name || '',
495
+        })
496
+      })
497
+
498
+
499
+      let res = {
500
+        ...form.value,
501
+        checkStartTime: form.value.timeRange[0],
502
+        checkEndTime: form.value.timeRange[1],
503
+        checkLevelDesc: check_level.value.find(item => item.value == form.value.checkLevel)?.label,
504
+        checkedLevelDesc: check_checked_level.value.find(item => item.value == form.value.checkedLevel)?.label,
505
+        checkCategoryDesc: check_task_category.value.find(item => item.value == form.value.checkCategory)?.label,
506
+        selfCheckDeptName: deptOptions.value.find(item => item.deptId == form.value.selfCheckDeptId)?.deptName || '',
507
+        ruleTypeDesc: check_rule_type.value.find(item => item.value == form.value.ruleType)?.label,
508
+        checkProjectItemList: selectedItems
509
+      }
510
+      console.log(res, "res")
511
+
512
+      addCheckTask(res).then(response => {
513
+        proxy.$modal.msgSuccess("新增成功")
514
+        open.value = false
515
+        getList()
516
+      }).finally(() => {
517
+        reset()
518
+      })
519
+    }
520
+  })
521
+}
522
+
523
+/** 删除按钮操作 */
524
+function handleDelete(row) {
525
+  const _ids = row.id || ids.value
526
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
527
+    return delCheckTask(_ids)
528
+  }).then(() => {
529
+    getList()
530
+    proxy.$modal.msgSuccess("删除成功")
531
+  }).catch(() => { })
532
+}
533
+
534
+
535
+getList()
536
+</script>
537
+
538
+<style scoped>
539
+.record-card {
540
+  margin-bottom: 12px;
541
+  padding: 8px 12px;
542
+  /* border-left: 4px solid #52c41a;    */
543
+  /* background: #f6ffed; */
544
+  border-radius: 4px;
545
+}
546
+
547
+.round {
548
+  width: 8px;
549
+  height: 8px;
550
+  border-radius: 50%;
551
+  margin-right: 5px;
552
+}
553
+
554
+.color-div {
555
+  display: flex;
556
+  flex-direction: row;
557
+  align-items: center;
558
+  justify-content: center;
559
+}
560
+
561
+.record-line {
562
+  display: flex;
563
+  margin-bottom: 4px;
564
+  font-size: 14px;
565
+  line-height: 1.5;
566
+}
567
+
568
+.label {
569
+  color: #666;
570
+  margin-right: 6px;
571
+  flex-shrink: 0;
572
+  width: 70px;
573
+}
574
+
575
+.value {
576
+  color: #333;
577
+  flex: 1;
578
+}
579
+</style>

+ 116 - 0
src/views/dataBigScreen/abilityPortrait/components/dataDisplay.vue

@@ -0,0 +1,116 @@
1
+<template>
2
+	<view class="data_display">
3
+		<view class="bottom">
4
+			<view class="left">
5
+				<view class="title">{{ props.data.title }}</view>
6
+				<view class="left_num">{{ props.data.current_month }}</view>
7
+				<view class="left_compare">
8
+					<view :class="props.data.status ? 'left_compare_green' : 'left_compare_red'">
9
+						<img :src="props.data.status ? '/src/assets/icons/shang.png' : '/src/assets/icons/xia.png'" alt="" />
10
+						<view>较上月{{ props.data.status ? '增长' : '下降' }}</view>
11
+						<view style="margin-top: 4px">{{ props.data.percentage }}%</view>
12
+					</view>
13
+				</view>
14
+			</view>
15
+			<view class="right">
16
+				<img v-if="props.data.title == '安检员总数'" src="/src/assets/icons/duoren.png" alt="" />
17
+				<img v-else-if="props.data.title == '违规事件数'" src="/src/assets/icons/danger.png" alt="" />
18
+				<img v-else-if="props.data.title == '平均综合评分'" src="/src/assets/icons/wujiao.png" alt="" />
19
+				<img v-else src="/src/assets/icons/sousuo.png" alt="" />
20
+			</view>
21
+		</view>
22
+	</view>
23
+</template>
24
+
25
+<script setup lang="ts">
26
+	import { ref } from 'vue';
27
+	import * as echarts from 'echarts';
28
+	const props = defineProps({
29
+	  data: { type: Object, default: {} },
30
+	});
31
+</script>
32
+
33
+<style lang="scss" scoped>
34
+	.data_display {
35
+		background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
36
+		margin-right: 10px;
37
+		color: #fff;
38
+		flex: 1;
39
+		&:nth-child(1) {
40
+			margin-left: 10px;
41
+		}
42
+		.top {
43
+			position: relative;
44
+			display: flex;
45
+			justify-content: space-between;
46
+			align-items: center;
47
+			flex-wrap: nowrap;
48
+			flex-direction: row;
49
+			z-index: 1;
50
+			align-content: flex-start;
51
+			background: url(/src/assets/images/titlebg-4340cf1c.png);
52
+			background-size: 100% 100%;
53
+			font-weight: 400;
54
+			font-size: 14px;
55
+			color: #fbffff;
56
+			padding: 8px 20px;
57
+			margin-bottom: 8px;
58
+		}
59
+		.bottom {
60
+			width: 100%;
61
+			display: flex;
62
+			justify-content: space-between;
63
+			align-items: center;
64
+			padding: 0 10px;
65
+			position: relative;
66
+			.left {
67
+				display: flex;
68
+				flex-direction: column;
69
+				margin-left: 40px;
70
+				.title {
71
+					margin-bottom: 5px;
72
+				}
73
+				.left_num {
74
+					font-size: 18px;
75
+					font-weight: 800;
76
+				}
77
+				.left_compare {
78
+					margin-top: 3px;
79
+					font-size: 13px;
80
+					padding-bottom: 8px;
81
+					.left_compare_green {
82
+						display: flex;
83
+						align-items: center;
84
+						color: greenyellow;
85
+					}
86
+					.left_compare_red {
87
+						display: flex;
88
+						align-items: center;
89
+						color: orangered;
90
+					}
91
+					img {
92
+						width: 15px;
93
+						height: 15px;
94
+						padding-top: 2px;
95
+					}
96
+				}
97
+			}
98
+			.right {
99
+				width: 40px;
100
+				height: 40px;
101
+				background: #4a565b;
102
+				position: absolute;
103
+				right: 20px;
104
+				top: 8px;
105
+				border-radius: 6px;
106
+				display: flex;
107
+				justify-content: center;
108
+				align-items: center;
109
+				img {
110
+					width: 15px;
111
+					height: 15px;
112
+				}
113
+			}
114
+		}
115
+	}
116
+</style>

+ 951 - 0
src/views/dataBigScreen/abilityPortrait/index.vue

@@ -0,0 +1,951 @@
1
+<template>
2
+	<view class="abilityPortrait">
3
+		<view class="root">
4
+			<DataDisplay v-for="(item, index) in data.list" :data="item" :key="index" />
5
+		</view>
6
+		<view class="top">
7
+			<view class="top_left">
8
+				<view class="_title">
9
+					<view class="title">能力画像</view>
10
+					<view class="filter_tag">
11
+						<el-select
12
+							v-model="selectVal.selectValue"
13
+							class="m-2"
14
+							placeholder="按时间筛选"
15
+							style="width: 240px;"
16
+						>
17
+							<el-option
18
+								v-for="item in selectData.selectOptions"
19
+								:key="item.value"
20
+								:label="item.label"
21
+								:value="item.value"
22
+							/>
23
+						</el-select>
24
+						<span v-for="(item, index) in activeList" :key="index" @click="changeActive(index)" :class="activeName == index ? 'label active' : 'label'">{{ item }}</span>
25
+					</view>
26
+				</view>
27
+				<view class="top_cont">
28
+					<view ref="top_Echart" class="top_Echart"></view>
29
+				</view>
30
+			</view>
31
+			<view class="top_right">
32
+				<view class="_title">
33
+					<view class="title">综合能力排名Top5</view>
34
+					<view class="filter_tag">
35
+						<el-select
36
+							v-model="selectVal.schemeValue"
37
+							class="m-2"
38
+							placeholder="选择筛选条件"
39
+							style="width: 240px"
40
+						>
41
+							<el-option
42
+								v-for="item in selectData.schemeOptions"
43
+								:key="item.value"
44
+								:label="item.label"
45
+								:value="item.value"
46
+							/>
47
+						</el-select>
48
+						<span class="label" @click="dialogVisible = true">自定义权重</span>
49
+					</view>
50
+				</view>
51
+				<view class="top_cont">
52
+					<view ref="echart_right" class="top_Echart"></view>
53
+				</view>
54
+			</view>
55
+		</view>
56
+		<view class="bottom">
57
+			<view class="bottom_left">
58
+				<view class="_title">
59
+					<view class="title">不同能力排名Top10</view>
60
+					<view class="filter_tag">
61
+						<el-select
62
+							v-model="selectVal.capacityValue"
63
+							class="m-2"
64
+							placeholder="选择筛选条件"
65
+							style="width: 240px"
66
+						>
67
+							<el-option
68
+								v-for="item in selectData.capacityOptions"
69
+								:key="item.value"
70
+								:label="item.label"
71
+								:value="item.value"
72
+							/>
73
+						</el-select>
74
+					</view>
75
+				</view>
76
+				<view class="bottom_cont">
77
+					<view class="cont_title">
78
+						<view class="serial_num">排名</view>
79
+						<view class="name">姓名</view>
80
+						<view class="progress">占比</view>
81
+						<view class="fraction">分数</view>
82
+					</view>
83
+					<view class="user_ranking">
84
+						<vue3-scroll-seamless
85
+							:classOptions="classOptions"
86
+							:dataList="data.capacityList"
87
+							class="scroll-wrap"
88
+						>
89
+							<view class="ranking_item" v-for="(item, index) in data.capacityList" :key="index">
90
+								<view class="serial_num" :style="index == 0 ? 'background: #ff3232' : (index == 1 ? 'background: #ff9800' : (index == 2 ? 'background: #ffd200' : 'background: #5470c6'))">{{ index + 1 }}</view>
91
+								<view class="name">{{ item.name }}</view>
92
+								<view class="progress"><el-progress :percentage="100" :format="format" /></view>
93
+								<view class="fraction">{{ item.fraction }}</view>
94
+							</view>
95
+						</vue3-scroll-seamless>
96
+					</view>
97
+				</view>
98
+			</view>
99
+			<view class="bottom_right">
100
+				<view class="_title">
101
+					<view class="title">综合能力分析</view>
102
+				</view>
103
+				<view class="bottom_cont">
104
+					<view ref="center_EchartAnalysis" class="bottom_Echart"></view>
105
+				</view>
106
+			</view>
107
+		</view>
108
+		<el-dialog v-model="dialogVisible" title="自定义权重设置" width="30%">
109
+			<view v-for="(item, index) in data.sliderList" :key="index">
110
+				<view>{{ item.label }}</view>
111
+				<view style="display: flex;align-items: center;">
112
+					<el-slider v-model="item.value" />
113
+					<view style="width: 55px;text-align: right;">{{ item.value || 0 }}%</view>
114
+				</view>
115
+			</view>
116
+			<template #footer>
117
+				<span class="dialog-footer">
118
+					<el-button @click="dialogVisible = false">取消</el-button>
119
+					<el-button type="primary" @click="handleConfirm">保存设置</el-button>
120
+				</span>
121
+			</template>
122
+		</el-dialog>
123
+	</view>
124
+</template>
125
+
126
+<script setup lang="ts">
127
+	import { reactive, ref, onMounted, onUnmounted } from 'vue';
128
+	import * as echarts from 'echarts';
129
+	import DataDisplay from './components/dataDisplay.vue';
130
+	const format = (percentage) => (percentage === 100 ? '' : ``);
131
+	const data = reactive({
132
+		capacityList: [
133
+			{
134
+				name: '程海超',
135
+				fraction: '98'
136
+			},
137
+			{
138
+				name: '邓海阳',
139
+				fraction: '95.3'
140
+			},
141
+			{
142
+				name: '陈涵亮',
143
+				fraction: '94'
144
+			},
145
+			{
146
+				name: '万浩波',
147
+				fraction: '92.5'
148
+			},
149
+			{
150
+				name: '徐涵衍',
151
+				fraction: '91'
152
+			},
153
+			{
154
+				name: '陈浩',
155
+				fraction: '90.8'
156
+			},
157
+			{
158
+				name: '宋鹏海',
159
+				fraction: '90.5'
160
+			},
161
+			{
162
+				name: '徐鸿哲',
163
+				fraction: '90.2'
164
+			},
165
+			{
166
+				name: '张雅',
167
+				fraction: '89.8'
168
+			},
169
+			{
170
+				name: '宋明宇',
171
+				fraction: '89.5'
172
+			},
173
+		],
174
+		list: [
175
+			{
176
+				title: '安检员总数',
177
+				percentage: '2',
178
+				current_month: '2100',
179
+				status: true
180
+			},
181
+			{
182
+				title: '平均综合评分',
183
+				percentage: '1.2',
184
+				current_month: '96.3',
185
+				status: true
186
+			},
187
+			{
188
+				title: '查获物品总数',
189
+				percentage: '1.2',
190
+				current_month: '5013',
191
+				status: true
192
+			},
193
+			{
194
+				title: '违规事件数',
195
+				percentage: '0.2',
196
+				current_month: '42',
197
+				status: false
198
+			}
199
+		],
200
+		sliderList: [{
201
+			label: '查获能力权重',
202
+			value: ''
203
+			},
204
+		 {
205
+		 	label: '业务知识学握程度权重',
206
+		 	value: ''
207
+		 },
208
+		 {
209
+		 	label: '设备熟练程度权重',
210
+		 	value: ''
211
+		 },
212
+		 {
213
+		 	label: '工作态度权重',
214
+		 	value: ''
215
+		 },
216
+		 {
217
+		 	label: '按章作业规范度权重',
218
+		 	value: ''
219
+		 }]
220
+	});
221
+	const selectData = reactive({
222
+		selectOptions: [
223
+			{
224
+				value: '1',
225
+				label: '近一周'
226
+			},
227
+			{
228
+				value: '2',
229
+				label: '近三个月'
230
+			},
231
+			{
232
+				value: '3',
233
+				label: '近一年'
234
+			},
235
+		],
236
+		capacityOptions: [
237
+			{
238
+				value: '0',
239
+				label: '综合能力'
240
+			},
241
+			{
242
+				value: '1',
243
+				label: '查获能力'
244
+			},
245
+			{
246
+				value: '2',
247
+				label: '作风建设'
248
+			},
249
+			{
250
+				value: '3',
251
+				label: '操作能力'
252
+			},
253
+			{
254
+				value: '4',
255
+				label: '业务知识'
256
+			},
257
+			{
258
+				value: '5',
259
+				label: '应急能力'
260
+			},
261
+		],
262
+		schemeOptions: [
263
+			{
264
+				value: '1',
265
+				label: '默认权重方案'
266
+			}
267
+		]
268
+	});
269
+	const classOptions = {
270
+	  limitMoveNum: 6,
271
+	};
272
+	const activeList = reactive(['科', '班组', '个人']);
273
+	const selectVal = reactive({
274
+		selectValue: '',
275
+		capacityValue: '0',
276
+		schemeValue: '1'
277
+	});
278
+	const activeName = ref('0');
279
+	const chart = ref(null);
280
+	const top_Echart = ref(null);
281
+	const chartOption = ref({
282
+		legend: {
283
+			data: ['旅检一科', '旅检二科', '旅检三科', '旅检四科'],
284
+			right: '10%',
285
+			top: '10%',
286
+			orient: 'vertical',
287
+			textStyle: {
288
+				color: '#fff',
289
+			},
290
+		},
291
+		radar: {
292
+			indicator: [
293
+				{ name: '查获能力', max: 100 },
294
+				{ name: '作风建设', max: 100 },
295
+				{ name: '操作能力', max: 100 },
296
+				{ name: '应急能力', max: 100 },
297
+				{ name: '业务知识', max: 100 },
298
+			],
299
+			center: ['45%', '50%'],
300
+			name: {
301
+				// 修改指标名称的文本样式
302
+				textStyle: {
303
+					color: '#fff', // 设置文字颜色为红色
304
+					fontSize: 12, // 设置文字大小
305
+					fontWeight: 'bold' // 设置文字粗细
306
+				}
307
+			}
308
+		},
309
+		series: [
310
+			{
311
+				name: 'Budget vs spending',
312
+				type: 'radar',
313
+				data: [
314
+					{
315
+						value: [80, 70, 75, 80, 80, 80],
316
+						name: '旅检一科',
317
+						label: {
318
+							show: true,
319
+							color: '#fff',
320
+						},
321
+						areaStyle: {
322
+							color: '#a9bdff',
323
+						},
324
+						itemStyle: {
325
+							color: 'blue',
326
+						},
327
+					},
328
+					{
329
+						value: [8, 50, 25, 70, 60, 80],
330
+						name: '旅检二科',
331
+						label: {
332
+							show: true,
333
+							color: '#fff',
334
+						},
335
+						areaStyle: {
336
+							color: '#ffff7f',
337
+						},
338
+						itemStyle: {
339
+							color: 'yellow',
340
+						},
341
+					},
342
+					{
343
+						value: [30, 40, 15, 40, 70, 60],
344
+						name: '旅检三科',
345
+						label: {
346
+							show: true,
347
+							color: '#fff',
348
+						},
349
+						areaStyle: {
350
+							color: '#ff557f',
351
+						},
352
+						itemStyle: {
353
+							color: 'red',
354
+						},
355
+					},
356
+					{
357
+						value: [21, 10, 55, 20, 60, 50],
358
+						name: '旅检四科',
359
+						label: {
360
+							show: true,
361
+							color: '#fff',
362
+						},
363
+						areaStyle: {
364
+							color: '#aaff7f',
365
+						},
366
+						itemStyle: {
367
+							color: 'green',
368
+						},
369
+					},
370
+				],
371
+			},
372
+		],
373
+	});
374
+	
375
+	const chartRight = ref(null);
376
+	const echart_right = ref(null);
377
+	const chartRightOption = ref({
378
+		xAxis: {
379
+			type: 'category',
380
+			data: ['1', '2', '3', '4', '5'],
381
+			axisLabel: {
382
+				color: '#fff'
383
+			},
384
+			categoryGap: '50%',
385
+		},
386
+		yAxis: {
387
+			type: 'value',
388
+			axisLabel: {
389
+				color: '#fff'
390
+			},
391
+		},
392
+		series: [
393
+			{
394
+				data: [95, 93, 90, 88, 85],
395
+				type: 'bar',
396
+				showBackground: true,
397
+				backgroundStyle: {
398
+					color: 'rgba(180, 180, 180, 0.2)'
399
+				}
400
+			}
401
+		]
402
+	});
403
+	
404
+	const chartAnalysis = ref(null);
405
+	const dialogVisible = ref(false)
406
+	const center_EchartAnalysis = ref(null);
407
+	const chartAnalysisOption = ref({
408
+		tooltip: {
409
+			trigger: 'item'
410
+		},
411
+		legend: {
412
+			left: 'center',
413
+			top: 15,
414
+			textStyle: {
415
+				color: '#fff',
416
+			},
417
+			selectedMode: false
418
+		},
419
+		series: [
420
+			{
421
+				name: '占比',
422
+				type: 'pie',
423
+				radius: ['40%', '70%'],
424
+				center:['50%','60%'],
425
+				avoidLabelOverlap: false,
426
+				label: {
427
+					show: false,
428
+					position: 'center'
429
+				},
430
+				labelLine: {
431
+					show: false
432
+				},
433
+				data: [
434
+					{ value: 735, name: '优秀(90-100)' },
435
+					{ value: 1048, name: '良好(80-89)' },
436
+					{ value: 580, name: '中等(70-79)' },
437
+					{ value: 484, name: '需提升(<70)' },
438
+				]
439
+			}
440
+		]
441
+	});
442
+	
443
+	const initChart = () => {
444
+		chart.value = echarts.init(top_Echart.value, 'macarons');
445
+		chart.value.setOption(chartOption.value);
446
+		chartRight.value = echarts.init(echart_right.value, 'macarons');
447
+		chartRight.value.setOption(chartRightOption.value);
448
+		chartAnalysis.value = echarts.init(center_EchartAnalysis.value, 'macarons');
449
+		chartAnalysis.value.setOption(chartAnalysisOption.value);
450
+	};
451
+	
452
+	const changeActive = (ind) => {
453
+		if (ind == 0) {
454
+			chartOption.value = {
455
+				legend: {
456
+					data: ['旅检一科', '旅检二科', '旅检三科', '旅检四科'],
457
+					right: '10%',
458
+					top: '10%',
459
+					orient: 'vertical',
460
+					textStyle: {
461
+						color: '#fff',
462
+					},
463
+				},
464
+				radar: {
465
+					indicator: [
466
+						{ name: '查获能力', max: 100 },
467
+						{ name: '作风建设', max: 100 },
468
+						{ name: '操作能力', max: 100 },
469
+						{ name: '应急能力', max: 100 },
470
+						{ name: '业务知识', max: 100 },
471
+					],
472
+					center: ['45%', '50%'],
473
+					name: {
474
+						// 修改指标名称的文本样式
475
+						textStyle: {
476
+							color: '#fff', // 设置文字颜色为红色
477
+							fontSize: 12, // 设置文字大小
478
+							fontWeight: 'bold' // 设置文字粗细
479
+						}
480
+					}
481
+				},
482
+				series: [
483
+					{
484
+						name: 'Budget vs spending',
485
+						type: 'radar',
486
+						data: [
487
+							{
488
+								value: [80, 70, 75, 80, 80, 80],
489
+								name: '旅检一科',
490
+								label: {
491
+									show: true,
492
+									color: '#fff',
493
+								},
494
+								areaStyle: {
495
+									color: '#a9bdff',
496
+								},
497
+								itemStyle: {
498
+									color: 'blue',
499
+								},
500
+							},
501
+							{
502
+								value: [8, 50, 25, 70, 60, 80],
503
+								name: '旅检二科',
504
+								label: {
505
+									show: true,
506
+									color: '#fff',
507
+								},
508
+								areaStyle: {
509
+									color: '#ffff7f',
510
+								},
511
+								itemStyle: {
512
+									color: 'yellow',
513
+								},
514
+							},
515
+							{
516
+								value: [30, 40, 15, 40, 70, 60],
517
+								name: '旅检三科',
518
+								label: {
519
+									show: true,
520
+									color: '#fff',
521
+								},
522
+								areaStyle: {
523
+									color: '#ff557f',
524
+								},
525
+								itemStyle: {
526
+									color: 'red',
527
+								},
528
+							},
529
+							{
530
+								value: [21, 10, 55, 20, 60, 50],
531
+								name: '旅检四科',
532
+								label: {
533
+									show: true,
534
+									color: '#fff',
535
+								},
536
+								areaStyle: {
537
+									color: '#aaff7f',
538
+								},
539
+								itemStyle: {
540
+									color: 'green',
541
+								},
542
+							},
543
+						],
544
+					},
545
+				],
546
+			}
547
+		} else if (ind == 1) {
548
+			chartOption.value = {
549
+				legend: {
550
+					data: ['李攀班', '李士强班', '程莎莎班', '李延班'],
551
+					right: '10%',
552
+					top: '10%',
553
+					orient: 'vertical',
554
+					textStyle: {
555
+						color: '#fff',
556
+					},
557
+				},
558
+				radar: {
559
+					indicator: [
560
+						{ name: '查获能力', max: 100 },
561
+						{ name: '作风建设', max: 100 },
562
+						{ name: '操作能力', max: 100 },
563
+						{ name: '应急能力', max: 100 },
564
+						{ name: '业务知识', max: 100 },
565
+					],
566
+					center: ['45%', '50%'],
567
+					name: {
568
+						// 修改指标名称的文本样式
569
+						textStyle: {
570
+							color: '#fff', // 设置文字颜色为红色
571
+							fontSize: 12, // 设置文字大小
572
+							fontWeight: 'bold' // 设置文字粗细
573
+						}
574
+					}
575
+				},
576
+				series: [
577
+					{
578
+						name: 'Budget vs spending',
579
+						type: 'radar',
580
+						data: [
581
+							{
582
+								value: [10, 70, 25, 40, 10, 60],
583
+								name: '李攀班',
584
+								label: {
585
+									show: true,
586
+									color: '#fff',
587
+								},
588
+								areaStyle: {
589
+									color: '#a9bdff',
590
+								},
591
+								itemStyle: {
592
+									color: 'blue',
593
+								},
594
+							},
595
+							{
596
+								value: [18, 30, 75, 40, 38, 80],
597
+								name: '李士强班',
598
+								label: {
599
+									show: true,
600
+									color: '#fff',
601
+								},
602
+								areaStyle: {
603
+									color: '#ffff7f',
604
+								},
605
+								itemStyle: {
606
+									color: 'yellow',
607
+								},
608
+							},
609
+							{
610
+								value: [80, 60, 25, 30, 50, 60],
611
+								name: '程莎莎班',
612
+								label: {
613
+									show: true,
614
+									color: '#fff',
615
+								},
616
+								areaStyle: {
617
+									color: '#ff557f',
618
+								},
619
+								itemStyle: {
620
+									color: 'red',
621
+								},
622
+							},
623
+							{
624
+								value: [22, 15, 35, 10, 65, 50],
625
+								name: '李延班',
626
+								label: {
627
+									show: true,
628
+									color: '#fff',
629
+								},
630
+								areaStyle: {
631
+									color: '#aaff7f',
632
+								},
633
+								itemStyle: {
634
+									color: 'green',
635
+								},
636
+							},
637
+						],
638
+					},
639
+				],
640
+			}
641
+		} else {
642
+			chartOption.value = {
643
+				legend: {
644
+					data: ['李攀', '李士强'],
645
+					right: '10%',
646
+					top: '10%',
647
+					orient: 'vertical',
648
+					textStyle: {
649
+						color: '#fff',
650
+					},
651
+				},
652
+				radar: {
653
+					indicator: [
654
+						{ name: '查获能力', max: 100 },
655
+						{ name: '作风建设', max: 100 },
656
+						{ name: '操作能力', max: 100 },
657
+						{ name: '应急能力', max: 100 },
658
+						{ name: '业务知识', max: 100 },
659
+					],
660
+					center: ['45%', '50%'],
661
+					name: {
662
+						// 修改指标名称的文本样式
663
+						textStyle: {
664
+							color: '#fff', // 设置文字颜色为红色
665
+							fontSize: 12, // 设置文字大小
666
+							fontWeight: 'bold' // 设置文字粗细
667
+						}
668
+					}
669
+				},
670
+				series: [
671
+					{
672
+						name: 'Budget vs spending',
673
+						type: 'radar',
674
+						data: [
675
+							{
676
+								value: [20, 70, 55, 80, 50, 30],
677
+								name: '李攀',
678
+								label: {
679
+									show: true,
680
+									color: '#fff',
681
+								},
682
+								areaStyle: {
683
+									color: '#a9bdff',
684
+								},
685
+								itemStyle: {
686
+									color: 'blue',
687
+								},
688
+							},
689
+							{
690
+								value: [48, 50, 20, 70, 30, 80],
691
+								name: '李士强',
692
+								label: {
693
+									show: true,
694
+									color: '#fff',
695
+								},
696
+								areaStyle: {
697
+									color: '#ffff7f',
698
+								},
699
+								itemStyle: {
700
+									color: 'yellow',
701
+								},
702
+							}
703
+						],
704
+					},
705
+				],
706
+			}
707
+		}
708
+		chart.value.setOption(chartOption.value);
709
+		chartRight.value = echarts.init(echart_right.value, 'macarons');
710
+		activeName.value = ind;
711
+	};
712
+	
713
+	// 确认编辑权重
714
+	const handleConfirm = () => {
715
+	  dialogVisible.value = false;
716
+	};
717
+	
718
+	onMounted(() => {
719
+		initChart();
720
+		window.addEventListener('resize', () => {
721
+			chart.value.resize();
722
+			chartAnalysis.value.resize();
723
+			chartRight.value.resize();
724
+		});
725
+	});
726
+	
727
+	onUnmounted(() => {
728
+		if (!chart.value) {
729
+			return;
730
+		}
731
+		chart.value.dispose();
732
+		chart.value = null;
733
+		if (!chartAnalysis.value) {
734
+			return;
735
+		}
736
+		chartAnalysis.value.dispose();
737
+		chartAnalysis.value = null;
738
+		if (!chartRight.value) {
739
+			return;
740
+		}
741
+		chartRight.value.dispose();
742
+		chartRight.value = null;
743
+	});
744
+</script>
745
+
746
+<style lang="scss" scoped>
747
+	::v-deep .el-select__wrapper {
748
+		background-color: rgba(0, 0, 0, .3);
749
+	}
750
+	::v-deep .el-select__placeholder {
751
+		color: #fff;
752
+	}
753
+	::v-deep .is-transparent {
754
+		color: #a8abb2;
755
+	}
756
+	::v-deep .demo-progress .el-progress--line {
757
+	  margin-bottom: 15px;
758
+	  width: 100%;
759
+	}
760
+	.el-progress {
761
+		width: 100%;
762
+		background: #fff;
763
+	}
764
+	.abilityPortrait {
765
+		width: 100%;
766
+		position: fixed;
767
+		height: 100%;
768
+		background: url('../../../assets/images/bg-99b6904c.png');
769
+		background-size: 100% 100%;
770
+		overflow-y: auto;
771
+		padding-bottom: 150px;
772
+		.root {
773
+			width: calc(100% - 200px);
774
+			display: flex;
775
+			justify-content: space-between;
776
+			margin-top: 10px;
777
+		}
778
+		._title {
779
+			width: 100%;
780
+			display: flex;
781
+			justify-content: space-between;
782
+			align-items: center;
783
+			.title {
784
+				position: relative;
785
+				display: flex;
786
+				justify-content: space-between;
787
+				align-items: center;
788
+				flex-wrap: nowrap;
789
+				flex-direction: row;
790
+				z-index: 1;
791
+				align-content: flex-start;
792
+				background: url(/src/assets/images/titlebg-4340cf1c.png);
793
+				background-size: 100% 100%;
794
+				font-weight: 400;
795
+				font-size: 14px;
796
+				color: #fbffff;
797
+				padding: 8px 20px;
798
+				margin-bottom: 8px;
799
+			}
800
+			.filter_tag {
801
+				padding-right: 15px;
802
+				.label {
803
+					display: inline-flex;
804
+					align-items: center;
805
+					justify-content: center;
806
+					font-size: 12px;
807
+					background: #11134b;
808
+					padding: 4px 8px;
809
+					margin: -4px 0;
810
+					border-radius: 4px;
811
+					color: #fbffff;
812
+					cursor: pointer;
813
+				}
814
+				.active {
815
+					background: #171b75;
816
+				}
817
+			}
818
+		}
819
+		.top {
820
+			width: calc(100% - 220px);
821
+			display: flex;
822
+			justify-content: space-between;
823
+			margin: 10px;
824
+			.top_left {
825
+				width: 49%;
826
+				margin-right: 1%;
827
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
828
+				display: inline-block;
829
+				.top_cont {
830
+					width: 100%;
831
+					display: flex;
832
+					justify-content: center;
833
+					.top_Echart {
834
+						width: 100%;
835
+						height: 320px;
836
+					}
837
+				}
838
+			}
839
+			.top_right {
840
+				width: 50%;
841
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
842
+				display: inline-block;
843
+				.top_cont {
844
+					width: 100%;
845
+					display: flex;
846
+					justify-content: center;
847
+					.top_Echart {
848
+						width: 100%;
849
+						height: 320px;
850
+					}
851
+				}
852
+			}
853
+		}
854
+		.bottom {
855
+			width: calc(100% - 220px);
856
+			display: flex;
857
+			justify-content: space-between;
858
+			margin: 10px;
859
+			.bottom_left {
860
+				width: 49%;
861
+				margin-right: 1%;
862
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
863
+				display: inline-block;
864
+				.bottom_cont {
865
+					width: 100%;
866
+					.cont_title {
867
+						width: 80%;
868
+						margin-left: 10%;
869
+						height: 30px;
870
+						display: flex;
871
+						justify-content: space-between;
872
+						align-items: center;
873
+						color: #fff;
874
+						border-bottom: 1px solid #ffffff40;
875
+						padding-bottom: 15px;
876
+						margin-top: 10px;
877
+						.serial_num {
878
+							width: 40px;
879
+						}
880
+						.progress {
881
+							width: 60%;
882
+							margin-left: 20px;
883
+							text-align: center;
884
+						}
885
+						.name {
886
+							width: 20%;
887
+							padding-left: 15px;
888
+							text-align: center;
889
+						}
890
+						.fraction {
891
+							width: 15%;
892
+							text-align: right;
893
+						}
894
+					}
895
+					.user_ranking {
896
+						width: 88%;
897
+						height: 244px;
898
+						margin-left: 10%;
899
+						padding: 0 10px;
900
+						overflow: hidden;
901
+						display: inline-block;
902
+						color: #fff;
903
+						.ranking_item {
904
+							width: 92%;
905
+							height: 40px;
906
+							display: flex;
907
+							align-items: center;
908
+							border-bottom: 1px solid #ffffff40;
909
+							.serial_num {
910
+								width: 20px;
911
+								height: 20px;
912
+								border-radius: 50%;
913
+								text-align: center;
914
+								line-height: 20px;
915
+								background: red;
916
+								font-size: 13px;
917
+							}
918
+							.progress {
919
+								width: 60%;
920
+								margin-left: 20px;
921
+							}
922
+							.name {
923
+								width: 20%;
924
+								padding-left: 25px;
925
+								text-align: center;
926
+							}
927
+							.fraction {
928
+								width: 15%;
929
+								text-align: right;
930
+							}
931
+						}
932
+					}
933
+				}
934
+			}
935
+			.bottom_right {
936
+				width: 50%;
937
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
938
+				display: inline-block;
939
+				.bottom_cont {
940
+					width: 100%;
941
+					display: flex;
942
+					justify-content: center;
943
+					.bottom_Echart {
944
+						width: 100%;
945
+						height: 280px;
946
+					}
947
+				}
948
+			}
949
+		}
950
+	}
951
+</style>

+ 80 - 0
src/views/dataBigScreen/attendance/AttendanceStatus.vue

@@ -0,0 +1,80 @@
1
+<template>
2
+  <dashboard-item-container title="出勤状态分布">
3
+    <div ref="pieChart" style="width: 100%; height: 100%" />
4
+  </dashboard-item-container>
5
+</template>
6
+
7
+<script setup>
8
+  import { onMounted, ref } from 'vue';
9
+  import { useEcharts } from '@/hooks/chart.js';
10
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
11
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
12
+
13
+  const color = {
14
+    NORMAL: 'rgb(144, 238, 144)',
15
+    OTHER: 'rgb(255, 255, 102)',
16
+    LEAVE_ABSENCE: 'rgb(173, 216, 230)',
17
+    LEAVE_COMPENCOMPENSATORY: 'rgb(135, 206, 250)',
18
+    LEAVE_ANNUAL: 'rgb(255, 165, 0)',
19
+    LEAVE_SICK: 'rgb(255, 0, 0)',
20
+    LATE: 'rgb(255, 99, 71)',
21
+    EARLY: 'rgb(255, 127, 80)',
22
+    ABSENTEE: 'rgb(128, 0, 0)',
23
+  };
24
+  const pieChart = ref(null);
25
+  const chartOption = ref({
26
+    legend: {
27
+      orient: 'vertical',
28
+      right: 10,
29
+      top: '20%',
30
+      itemWidth: 10,
31
+      itemHeight: 10,
32
+      icon: 'circle',
33
+      textStyle: {
34
+        color: '#fbffff',
35
+      },
36
+    },
37
+    tooltip: {
38
+      show: false,
39
+    },
40
+    series: [
41
+      {
42
+        type: 'pie',
43
+        center: ['30%', '50%'],
44
+        radius: ['40%', '60%'],
45
+        avoidLabelOverlap: false,
46
+        label: {
47
+          show: false,
48
+          position: 'center',
49
+        },
50
+        emphasis: {
51
+          disable: false,  //是否关闭扇区高亮效果
52
+          scale: false,    //扇区是否缩放
53
+          scaleSize: 0,
54
+        },
55
+        labelLine: {
56
+          show: false,
57
+        },
58
+        data: [],
59
+      },
60
+    ],
61
+  });
62
+  const { setOption } = useEcharts(pieChart);
63
+
64
+  onMounted(() => {
65
+    executeSqlByKeyAndParam('attendance_status_distribution', {}).then(res => {
66
+      chartOption.value.series[0].data = res.data.map(el => {
67
+        return {
68
+          itemStyle: { color: color[el.status] },
69
+          value: el.count,
70
+          name: el.dict_label + ' ' + el.count + '人 ' + el.percentage + '%',
71
+        };
72
+      });
73
+      setOption?.(chartOption.value);
74
+    });
75
+  });
76
+</script>
77
+
78
+<style lang="less" scoped>
79
+
80
+</style>

+ 90 - 0
src/views/dataBigScreen/attendance/AttendanceStatusTime.vue

@@ -0,0 +1,90 @@
1
+<template>
2
+  <dashboard-item-container title="不同时间段出勤情况">
3
+    <div class="attendance-status-time">
4
+      <template v-for="item in list" style="width: 100%">
5
+        <div class="top">
6
+          {{ item.shift_name }}{{ item.start_time }}-{{ item.end_time }}
7
+          <span class="day">{{ item.normal_percentage }}%</span>
8
+        </div>
9
+        <el-progress
10
+          :color="item.color"
11
+          :percentage="item.normal_percentage"
12
+          :show-text="false"
13
+          :stroke-width="10"
14
+          :text-inside="true"
15
+        />
16
+      </template>
17
+    </div>
18
+
19
+    <p style="padding: 0 20px; color: #fbffff;">各时间段缺勤人员分布</p>
20
+    <div class="personnel-distribution-container">
21
+      <div v-for="item in list" class="personnel-distribution">
22
+        <span>{{ item.shift_name }}</span>
23
+        <span class="personnel">{{ item.normal_count }}人</span>
24
+      </div>
25
+    </div>
26
+  </dashboard-item-container>
27
+</template>
28
+
29
+<script setup>
30
+  import { onMounted, ref } from 'vue';
31
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
32
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
33
+
34
+  let list = ref({
35
+    shift1: { color: '#165DFF' },
36
+    shift2: { color: '#00B42A' },
37
+    shift3: { color: '#FF7D00' },
38
+  });
39
+
40
+  onMounted(() => {
41
+    executeSqlByKeyAndParam('attendance_at_different_time_periods', {}).then(res => {
42
+      res.data.forEach(el => {
43
+        list.value[el.shift_id] = { ...list.value[el.shift_id], ...el };
44
+      });
45
+    });
46
+  });
47
+</script>
48
+
49
+<style lang="less" scoped>
50
+  .attendance-status-time {
51
+    padding: 20px;
52
+    font-size: 12px;
53
+    color: #fbffff;
54
+  }
55
+
56
+  .top {
57
+    display: flex;
58
+    justify-content: space-between;
59
+    margin: 8px 0;
60
+
61
+    .day {
62
+      color: #00B42A;
63
+    }
64
+  }
65
+
66
+  .personnel-distribution-container {
67
+    display: flex;
68
+    align-items: center;
69
+    justify-content: space-between;
70
+    color: #fbffff;
71
+  }
72
+
73
+  .personnel-distribution {
74
+    width: 33.33%;
75
+    display: flex;
76
+    align-items: center;
77
+    justify-content: center;
78
+    flex-flow: column;
79
+
80
+    span {
81
+      color: rgba(251, 255, 255, .6)
82
+    }
83
+
84
+    .personnel {
85
+      margin: 8px 0;
86
+      font-size: 18px;
87
+      color: #fbffff;
88
+    }
89
+  }
90
+</style>

+ 65 - 0
src/views/dataBigScreen/attendance/AttendanceTrend.vue

@@ -0,0 +1,65 @@
1
+<template>
2
+  <dashboard-item-container title="出勤趋势分析">
3
+    <div ref="lineChart" style="width: 100%; height: 100%" />
4
+  </dashboard-item-container>
5
+</template>
6
+
7
+<script setup>
8
+  import { onMounted, ref } from 'vue';
9
+  import { useEcharts } from '@/hooks/chart.js';
10
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
11
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
12
+
13
+  const lineChart = ref(null);
14
+  const color = [
15
+    '#5470c6',
16
+    '#91cc75',
17
+    '#fac858',
18
+    '#ee6666',
19
+    '#fc8452',
20
+  ];
21
+  const chartOption = ref({
22
+    color: ['#00B42A'],
23
+    xAxis: {
24
+      type: 'category',
25
+      data: [],
26
+      axisLine: {
27
+        lineStyle: {
28
+          color: 'rgba(251, 255, 255, .6)',
29
+        },
30
+      },
31
+      axisLabel: {
32
+        rotate: 60,
33
+      },
34
+    },
35
+    yAxis: {
36
+      type: 'value',
37
+      axisLine: {
38
+        lineStyle: {
39
+          color: 'rgba(251, 255, 255, .6)',
40
+        },
41
+      },
42
+    },
43
+    series: [
44
+      {
45
+        symbol: 'circle',
46
+        symbolSize: 6,
47
+        data: [],
48
+        type: 'line',
49
+      },
50
+    ],
51
+  });
52
+  const { setOption } = useEcharts(lineChart);
53
+
54
+  onMounted(() => {
55
+    executeSqlByKeyAndParam('attendance_trend_analysis', {}).then(res => {
56
+      chartOption.value.xAxis.data = res.data.map(el => el.day);
57
+      chartOption.value.series[0].data = res.data.map(el => el.count_sum);
58
+      setOption?.(chartOption.value);
59
+    });
60
+  });
61
+</script>
62
+
63
+<style lang="less" scoped>
64
+
65
+</style>

+ 78 - 0
src/views/dataBigScreen/attendance/ContinuousAttendance.vue

@@ -0,0 +1,78 @@
1
+<template>
2
+  <scroll-seamless class="dashboard-bg" :list="list" title="连续出勤天数">
3
+    <template #default="{ row, index }">
4
+      <span class="ranking">
5
+        <i :class="{ first: index === 0, second: index === 1, third: index === 2}" class="index">{{ index + 1 }}</i>
6
+      </span>
7
+      <div style="width: 100%">
8
+        <div class="top">
9
+          {{ row.user_name }}
10
+          <span class="day">{{ row.attendance_days }}天</span>
11
+        </div>
12
+        <el-progress
13
+          :percentage="row.attendance_days / 30 * 100"
14
+          :show-text="false"
15
+          :stroke-width="10"
16
+          :text-inside="true"
17
+          color="#00B42A"
18
+        />
19
+      </div>
20
+    </template>
21
+  </scroll-seamless>
22
+</template>
23
+
24
+<script setup>
25
+  import { onMounted, ref } from 'vue';
26
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
27
+  import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
28
+
29
+  let list = ref([]);
30
+
31
+  onMounted(() => {
32
+    executeSqlByKeyAndParam('continuous_attendance_days', {}).then(res => {
33
+      list.value = res.data;
34
+    });
35
+  });
36
+</script>
37
+
38
+<style lang="less" scoped>
39
+
40
+  .ranking {
41
+    width: 40px;
42
+
43
+    .index {
44
+      display: inline-flex;
45
+      justify-content: center;
46
+      align-items: center;
47
+      width: 16px;
48
+      height: 16px;
49
+      background: #5470c6;
50
+      border-radius: 4px;
51
+      font-style: normal;
52
+
53
+      &.first {
54
+        background: #ff3232;
55
+      }
56
+
57
+      &.second {
58
+        background: #ff9800;
59
+      }
60
+
61
+      &.third {
62
+        background: #ffd200;
63
+      }
64
+    }
65
+  }
66
+
67
+
68
+  .top {
69
+    display: flex;
70
+    justify-content: space-between;
71
+    margin-bottom: 8px;
72
+    color: #fbffff;
73
+
74
+    .day {
75
+      color: #00B42A;
76
+    }
77
+  }
78
+</style>

+ 68 - 0
src/views/dataBigScreen/attendance/DepartmentAttendance.vue

@@ -0,0 +1,68 @@
1
+<template>
2
+  <dashboard-item-container title="科室平均工作时长">
3
+    <template #right-menu>
4
+      <div class="dashboard-right-menu">
5
+        <span class="active">今日</span>
6
+        <span>本周</span>
7
+        <span>本月</span>
8
+      </div>
9
+    </template>
10
+    <div ref="barChart" style="width: 100%; height: 100%" />
11
+  </dashboard-item-container>
12
+</template>
13
+
14
+<script setup>
15
+  import { onMounted, ref } from 'vue';
16
+  import { useEcharts } from '@/hooks/chart.js';
17
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
18
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
19
+
20
+  const barChart = ref(null);
21
+  const chartOption = ref({
22
+    color: ['#165DFF'],
23
+    grid: [{ bottom: '20' }, { top: '50' }],
24
+    legend: {
25
+      icon: 'circle',
26
+      textStyle: {
27
+        color: '#fbffff',
28
+      },
29
+    },
30
+    xAxis: {
31
+      type: 'category',
32
+      gridIndex: 1,
33
+      axisLine: {
34
+        lineStyle: {
35
+          color: 'rgba(251, 255, 255, .6)',
36
+        },
37
+      },
38
+    },
39
+    yAxis: {
40
+      gridIndex: 1,
41
+      axisLine: {
42
+        lineStyle: {
43
+          color: 'rgba(251, 255, 255, .6)',
44
+        },
45
+      },
46
+    },
47
+    dataset: {
48
+      source: [],
49
+    },
50
+    series: [
51
+      { type: 'bar' },
52
+    ],
53
+  });
54
+  const { setOption } = useEcharts(barChart);
55
+
56
+  onMounted(() => {
57
+    executeSqlByKeyAndParam('attendance_statistics_of_departments', {}).then(res => {
58
+      chartOption.value.dataset.source = res.data.map(el => {
59
+        return [el.dept_name, el.pass_count];
60
+      });
61
+      setOption?.(chartOption.value);
62
+    });
63
+  });
64
+</script>
65
+
66
+<style lang="less" scoped>
67
+
68
+</style>

+ 50 - 0
src/views/dataBigScreen/attendance/DepartmentDetails.vue

@@ -0,0 +1,50 @@
1
+<template>
2
+  <scroll-seamless :list="list" class="dashboard-bg" title="科室详情">
3
+    <template #thead>
4
+      <span class="item">科室</span>
5
+      <span class="item">总人数</span>
6
+      <span class="item">出勤人数</span>
7
+      <span class="item">出勤率</span>
8
+      <span class="item">迟到</span>
9
+      <span class="item">请假</span>
10
+      <span class="item">矿工</span>
11
+    </template>
12
+    <template #default="{ row, index }">
13
+      <span class="item">{{ row.dept_name }}</span>
14
+      <span class="item">{{ row.total_count }}</span>
15
+      <span class="item">{{ row.normal_count }}</span>
16
+      <span class="item">{{ row.normal_percentage }}</span>
17
+      <span class="item">{{ row.late_count }}</span>
18
+      <span class="item">{{ row.leave_count }}</span>
19
+      <span class="item">{{ row.absentee_count }}</span>
20
+    </template>
21
+  </scroll-seamless>
22
+</template>
23
+
24
+<script setup>
25
+  import { onMounted, ref } from 'vue';
26
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
27
+  import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
28
+  import SvgIcon from '@/components/SvgIcon/index.vue';
29
+
30
+  let list = ref([]);
31
+  onMounted(() => {
32
+    executeSqlByKeyAndParam('department_details', {}).then(res => {
33
+      list.value = res.data;
34
+    });
35
+  });
36
+</script>
37
+
38
+<style lang="less" scoped>
39
+  .item {
40
+    display: inline-flex;
41
+    align-items: center;
42
+    justify-content: center;
43
+    width: 14.28%;
44
+    white-space: nowrap;
45
+
46
+    &.ranking {
47
+      width: 10%;
48
+    }
49
+  }
50
+</style>

+ 61 - 0
src/views/dataBigScreen/attendance/LeaveType.vue

@@ -0,0 +1,61 @@
1
+<template>
2
+  <dashboard-item-container title="请假类型占比">
3
+    <div ref="pieChart" style="width: 100%; height: 100%" />
4
+  </dashboard-item-container>
5
+</template>
6
+
7
+<script setup>
8
+  import { onMounted, ref } from 'vue';
9
+  import { useEcharts } from '@/hooks/chart.js';
10
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
11
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
12
+
13
+  const pieChart = ref(null);
14
+  const chartOption = ref({
15
+    color: ['#00B42A', '#722ED1', '#F53F3F', '#165DFF', '#FF7D00'],
16
+    legend: {
17
+      bottom: 10,
18
+      itemWidth: 10,
19
+      itemHeight: 10,
20
+      icon: 'circle',
21
+      textStyle: {
22
+        color: '#fbffff',
23
+      },
24
+    },
25
+    tooltip: {
26
+      show: false,
27
+    },
28
+    series: [
29
+      {
30
+        type: 'pie',
31
+        avoidLabelOverlap: false,
32
+        label: {
33
+          show: false,
34
+          position: 'center',
35
+        },
36
+        emphasis: {
37
+          disable: false,  //是否关闭扇区高亮效果
38
+          scale: false,    //扇区是否缩放
39
+          scaleSize: 0,
40
+        },
41
+        labelLine: {
42
+          show: false,
43
+        },
44
+        data: [],
45
+      },
46
+    ],
47
+  });
48
+  const { setOption } = useEcharts(pieChart);
49
+
50
+  onMounted(() => {
51
+    executeSqlByKeyAndParam('proportion_of_leave_types', {}).then(res => {
52
+      chartOption.value.series[0].data = res.data.map(el => {
53
+        return {
54
+          value: el.count,
55
+          name: el.dict_label,
56
+        };
57
+      });
58
+      setOption?.(chartOption.value);
59
+    });
60
+  });
61
+</script>

+ 112 - 0
src/views/dataBigScreen/attendance/NumberOfAbsences.vue

@@ -0,0 +1,112 @@
1
+<template>
2
+  <scroll-seamless :list="list" class="dashboard-bg" title="最近缺勤人员">
3
+    <template #thead>
4
+      <span class="item">姓名</span>
5
+      <span class="item">科室</span>
6
+      <span class="item">缺勤类型</span>
7
+      <span class="item">开始时间</span>
8
+      <span class="item">预计返回</span>
9
+      <span class="item ranking">操作</span>
10
+    </template>
11
+    <template #default="{ row, index }">
12
+      <span class="item">{{ row.user_name }}</span>
13
+      <span class="item">{{ row.dept_name }}</span>
14
+      <div class="item status">
15
+        <span :class="row.status">
16
+          {{ row.dict_label }}
17
+        </span>
18
+      </div>
19
+      <span class="item num">{{ row.check_in_time }}</span>
20
+      <span class="item">{{ row.check_out_time || '-' }}</span>
21
+      <span class="item ranking">
22
+        <svg-icon icon-class="phone" style="fill: #2D5CF6" />
23
+      </span>
24
+    </template>
25
+  </scroll-seamless>
26
+</template>
27
+
28
+<script setup>
29
+  import { onMounted, ref } from 'vue';
30
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
31
+  import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
32
+  import SvgIcon from '@/components/SvgIcon/index.vue';
33
+
34
+  let list = ref([]);
35
+  onMounted(() => {
36
+    executeSqlByKeyAndParam('recently_absent_personnel', {}).then(res => {
37
+      list.value = res.data;
38
+    });
39
+  });
40
+</script>
41
+
42
+<style lang="less" scoped>
43
+  .item {
44
+    display: inline-flex;
45
+    align-items: center;
46
+    justify-content: center;
47
+    width: 18%;
48
+
49
+    &.ranking {
50
+      width: 10%;
51
+    }
52
+
53
+    &.status {
54
+      span {
55
+        padding: 4px 8px;
56
+        border-radius: 20px;
57
+      }
58
+    }
59
+
60
+    .NORMAL {
61
+      background: rgba(144, 238, 144, .4);
62
+      color: rgba(144, 238, 144, 1);
63
+    }
64
+
65
+    .OTHER {
66
+      background: rgba(255, 255, 102, .4);
67
+      color: rgba(255, 255, 102, 1);
68
+    }
69
+
70
+    // 事假
71
+    .LEAVE_ABSENCE {
72
+      background: rgba(255, 165, 0, .4);
73
+      color: rgba(255, 165, 0, 1);
74
+    }
75
+
76
+    // 调休
77
+    .LEAVE_COMPENCOMPENSATORY {
78
+      background: rgba(173, 216, 230, .4);
79
+      color: rgba(173, 216, 230, 1);
80
+    }
81
+
82
+    // 年假
83
+    .LEAVE_ANNUAL {
84
+      background: rgba(135, 206, 250, .4);
85
+      color: rgba(135, 206, 250, 1);
86
+    }
87
+
88
+    // 病假
89
+    .LEAVE_SICK {
90
+      background: rgba(255, 0, 0, .4);
91
+      color: rgba(255, 0, 0, 1);
92
+    }
93
+
94
+    // 迟到
95
+    .LATE {
96
+      background: rgba(255, 99, 71, .4);
97
+      color: rgba(255, 99, 71, 1);
98
+    }
99
+
100
+    // 早退
101
+    .EARLY {
102
+      background: rgba(255, 127, 80, .4);
103
+      color: rgba(255, 127, 80, 1);
104
+    }
105
+
106
+    // 旷工
107
+    .ABSENTEE {
108
+      background: rgba(128, 0, 0, .4);
109
+      color: rgba(128, 0, 0, 1);
110
+    }
111
+  }
112
+</style>

+ 56 - 0
src/views/dataBigScreen/attendance/OvertimeHours.vue

@@ -0,0 +1,56 @@
1
+<template>
2
+  <dashboard-item-container title="排班时长统计">
3
+    <template #right-menu>
4
+      <div class="dashboard-right-menu">
5
+        <span>今日</span>
6
+        <span>本周</span>
7
+        <span class="active">本月</span>
8
+      </div>
9
+    </template>
10
+    <div ref="barChart" style="width: 100%; height: 100%" />
11
+  </dashboard-item-container>
12
+</template>
13
+
14
+<script setup>
15
+  import { onMounted, ref } from 'vue';
16
+  import { useEcharts } from '@/hooks/chart.js';
17
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
18
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
19
+
20
+  const barChart = ref(null);
21
+  const chartOption = ref({
22
+    color: ['#165DFF'],
23
+    xAxis: {
24
+      type: 'category',
25
+      axisLabel: {
26
+        rotate: 20,
27
+      },
28
+      axisLine: {
29
+        lineStyle: {
30
+          color: 'rgba(251, 255, 255, .6)',
31
+        },
32
+      },
33
+    },
34
+    yAxis: {
35
+      axisLine: {
36
+        lineStyle: {
37
+          color: 'rgba(251, 255, 255, .6)',
38
+        },
39
+      },
40
+    },
41
+    series: [{ type: 'bar', data: [] }],
42
+  });
43
+  const { setOption } = useEcharts(barChart);
44
+
45
+  onMounted(() => {
46
+    executeSqlByKeyAndParam('overtime_hours_statistics', {}).then(res => {
47
+      chartOption.value.xAxis.data = res.data.map(el => el.dept_name);
48
+      chartOption.value.series[0].data = res.data.map(el => el.over_duration_hour);
49
+      setOption?.(chartOption.value);
50
+    });
51
+  });
52
+</script>
53
+
54
+<style lang="less" scoped>
55
+
56
+</style>

+ 123 - 0
src/views/dataBigScreen/attendance/TotalContainer.vue

@@ -0,0 +1,123 @@
1
+<template>
2
+  <div class="total">
3
+    <div class="count-inner dashboard-bg">
4
+      <div class="item-title">总安检员数量</div>
5
+      <div class="count">
6
+        <vue-count-to :duration="2600" :end-val="totalNum.total_count" :start-val="0" class="card-panel-num" />
7
+      </div>
8
+      <div class="tips">
9
+        <span class="icon"><svg-icon icon-class="top" /> 较上月+12人</span>
10
+      </div>
11
+    </div>
12
+    <div class="count-inner dashboard-bg">
13
+      <div class="item-title">今日出勤人数</div>
14
+      <div class="count">
15
+        <vue-count-to :duration="2600" :end-val="totalNum.pass_count" :start-val="0" class="card-panel-num" />
16
+      </div>
17
+      <div class="tips">
18
+        <span class="icon"><svg-icon icon-class="top" /> 较昨日+8人</span>
19
+      </div>
20
+    </div>
21
+    <div class="count-inner dashboard-bg">
22
+      <div class="item-title">缺勤认数</div>
23
+      <div class="count">
24
+        <vue-count-to :duration="2600" :end-val="totalNum.fail_count" :start-val="0" class="card-panel-num warn" />
25
+      </div>
26
+      <div class="tips">
27
+        <span class="icon danger"><svg-icon icon-class="top" /> 较昨日+5人</span>
28
+      </div>
29
+    </div>
30
+    <div class="count-inner dashboard-bg">
31
+      <div class="item-title">出勤率</div>
32
+      <div class="count">
33
+        <vue-count-to :duration="2600" :end-val="totalNum.pass_percentage" :start-val="0" class="card-panel-num" />
34
+      </div>
35
+      <div class="tips">
36
+        <span class="icon danger down"><svg-icon icon-class="top" /> 较昨日-2.1%</span>
37
+      </div>
38
+    </div>
39
+  </div>
40
+</template>
41
+<script setup>
42
+  import { onMounted, ref } from 'vue';
43
+  import VueCountTo from '@/components/VueCountTo/vue-countTo.vue';
44
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
45
+  import SvgIcon from '@/components/SvgIcon/index.vue';
46
+
47
+  let totalNum = ref({
48
+    total: 0,
49
+    total_major: 0,
50
+    total_conceal: 0,
51
+  });
52
+
53
+  onMounted(() => {
54
+    // 查获总量+重大违禁品+故意隐匿数量
55
+    executeSqlByKeyAndParam('total_number_and_attendance_of_security_personnel', {}).then(res => {
56
+      totalNum.value = res.data[0];
57
+    });
58
+  });
59
+</script>
60
+
61
+<style lang="less" scoped>
62
+  .total {
63
+    display: flex;
64
+    justify-content: space-between;
65
+
66
+    .count-inner {
67
+      width: calc(25% - 8px);
68
+      padding: 20px;
69
+      border: 1px solid #363F4C;
70
+      border-radius: 8px;
71
+      color: #6D7F83;
72
+
73
+      .item-title {
74
+        margin-bottom: 8px;
75
+      }
76
+
77
+      .tips {
78
+        color: #9499A0;
79
+        font-size: 12px;
80
+        margin-top: 16px;
81
+
82
+        .icon {
83
+          margin-right: 8px;
84
+          color: #10B981;
85
+
86
+          .svg-icon {
87
+            fill: #10B981;
88
+            transform: rotate(-90deg);
89
+          }
90
+
91
+          &.danger {
92
+            color: #EF4444;
93
+
94
+            .svg-icon {
95
+              fill: #EF4444;
96
+            }
97
+          }
98
+
99
+          &.down {
100
+            .svg-icon {
101
+              transform: rotate(90deg);
102
+            }
103
+          }
104
+        }
105
+      }
106
+    }
107
+  }
108
+
109
+  :deep(.card-panel-num) {
110
+    margin: 8px 0;
111
+    font-size: 20px;
112
+    color: #fbffff;
113
+
114
+    &.warn {
115
+      color: #FE7C01;
116
+    }
117
+  }
118
+
119
+  .echarts {
120
+    flex: 1;
121
+    width: 100%;
122
+  }
123
+</style>

+ 118 - 0
src/views/dataBigScreen/attendance/WorkingHoursAndSeizureVolume.vue

@@ -0,0 +1,118 @@
1
+<template>
2
+  <dashboard-item-container title="工作时长和查获量的散点图表">
3
+    <div ref="scatterChart" style="width: 100%; height: 100%" />
4
+  </dashboard-item-container>
5
+</template>
6
+
7
+<script setup>
8
+  import { onMounted, ref } from 'vue';
9
+  import { useEcharts } from '@/hooks/chart.js';
10
+  import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
11
+
12
+  const color = ['#00B42A', '#165DFF'];
13
+  const scatterChart = ref(null);
14
+  const chartOption = ref({
15
+    color,
16
+    legend: {
17
+      bottom: 5,
18
+      textStyle: {
19
+        color: 'rgba(251, 255, 255, .6)',
20
+      },
21
+    },
22
+    xAxis: {
23
+      axisLine: {
24
+        lineStyle: {
25
+          color: 'rgba(251, 255, 255, .6)',
26
+        },
27
+      },
28
+    },
29
+    yAxis: {
30
+      axisLine: {
31
+        lineStyle: {
32
+          color: 'rgba(251, 255, 255, .6)',
33
+        },
34
+      },
35
+    },
36
+    tooltip: {
37
+      show: false,
38
+    },
39
+    series: [
40
+      {
41
+        name: '工作时长',
42
+        type: 'scatter',
43
+        symbolSize: 20,
44
+        data: [
45
+          [10.0, 8.04],
46
+          [8.07, 6.95],
47
+          [13.0, 7.58],
48
+          [9.05, 8.81],
49
+          [11.0, 8.33],
50
+          [14.0, 7.66],
51
+          [13.4, 6.81],
52
+          [10.0, 6.33],
53
+          [14.0, 8.96],
54
+          [12.5, 6.82],
55
+          [9.15, 7.2],
56
+          [11.5, 7.2],
57
+          [3.03, 4.23],
58
+          [12.2, 7.83],
59
+          [2.02, 4.47],
60
+          [1.05, 3.33],
61
+          [4.05, 4.96],
62
+          [6.03, 7.24],
63
+          [12.0, 6.26],
64
+          [12.0, 8.84],
65
+          [7.08, 5.82],
66
+          [5.02, 5.68],
67
+        ],
68
+      },
69
+      {
70
+        name: '查获量',
71
+        type: 'scatter',
72
+        symbolSize: 20,
73
+        data: [
74
+          [10.0, 18.04],
75
+          [8.07, 16.95],
76
+          [13.0, 17.58],
77
+          [9.05, 18.81],
78
+          [11.0, 18.33],
79
+          [14.0, 17.66],
80
+          [13.4, 16.81],
81
+          [10.0, 16.33],
82
+          [14.0, 18.96],
83
+          [12.5, 6.82],
84
+          [9.15, 17.2],
85
+          [11.5, 7.2],
86
+          [3.03, 14.23],
87
+          [12.2, 17.83],
88
+          [2.02, 14.47],
89
+          [1.05, 13.33],
90
+          [4.05, 14.96],
91
+          [6.03, 17.24],
92
+          [12.0, 16.26],
93
+          [12.0, 8.84],
94
+          [7.08, 15.82],
95
+          [5.02, 15.68],
96
+        ],
97
+      },
98
+    ],
99
+  });
100
+  const { setOption } = useEcharts(scatterChart);
101
+
102
+  onMounted(() => {
103
+    // executeSqlByKeyAndParam('proportion_of_leave_types', {}).then(res => {
104
+    //   chartOption.value.series[0].data = res.data.map(el => {
105
+    //     return {
106
+    //       value: el.count,
107
+    //       name: el.dict_label,
108
+    //     };
109
+    //   });
110
+    //   setOption?.(chartOption.value);
111
+    // });
112
+    setOption?.(chartOption.value);
113
+  });
114
+</script>
115
+
116
+<style lang="less" scoped>
117
+
118
+</style>

+ 103 - 0
src/views/dataBigScreen/attendance/index.vue

@@ -0,0 +1,103 @@
1
+<template>
2
+  <dashboard-container hide>
3
+    <div class="container">
4
+      <div class="total-container wow bounceInDown">
5
+        <total-container />
6
+      </div>
7
+      <div class="chart-container statistics-container">
8
+        <div class="w66-container wow slideInLeft">
9
+          <!-- 科室平均工作时长 -->
10
+          <department-attendance />
11
+        </div>
12
+        <div class="w33-container ml16 wow slideInRight">
13
+          <!-- 出勤趋势分析 -->
14
+          <attendance-trend />
15
+        </div>
16
+      </div>
17
+      <div class="chart-container staff-container">
18
+        <div class="w33-container wow slideInLeft">
19
+          <!-- 出勤状态分布 -->
20
+          <attendance-status />
21
+        </div>
22
+        <div class="w66-container wow slideInRight">
23
+          <!-- 最近缺勤人员 -->
24
+          <number-of-absences />
25
+        </div>
26
+      </div>
27
+      <div class="chart-container department-container">
28
+        <div class="w33-container wow slideInLeft">
29
+          <!-- 科室详情 -->
30
+          <department-details />
31
+        </div>
32
+        <div class="w33-container wow fadeIn">
33
+          <!-- 工作时长和查获量的散点图表 -->
34
+          <working-hours-and-seizure-volume />
35
+        </div>
36
+        <div class="w33-container wow slideInRight">
37
+          <!-- 连续出勤天数 -->
38
+          <continuous-attendance />
39
+        </div>
40
+      </div>
41
+      <div class="chart-container proportion-container">
42
+        <div class="w33-container wow slideInLeft">
43
+          <!-- 排班时长统计 -->
44
+          <overtime-hours />
45
+        </div>
46
+        <div class="w33-container wow fadeIn">
47
+          <!-- 请假类型占比 -->
48
+          <leave-type />
49
+        </div>
50
+        <div class="w33-container wow slideInRight">
51
+          <!-- 不同时间段出勤情况 -->
52
+          <attendance-status-time />
53
+        </div>
54
+      </div>
55
+    </div>
56
+  </dashboard-container>
57
+</template>
58
+
59
+<script setup>
60
+  import DashboardContainer from '@/views/dataBigScreen/dashboard/components/DashboardContainer.vue';
61
+  import TotalContainer from '@/views/dataBigScreen/attendance/TotalContainer.vue';
62
+  import DepartmentAttendance from '@/views/dataBigScreen/attendance/DepartmentAttendance.vue';
63
+  import AttendanceTrend from '@/views/dataBigScreen/attendance/AttendanceTrend.vue';
64
+  import AttendanceStatus from '@/views/dataBigScreen/attendance/AttendanceStatus.vue';
65
+  import NumberOfAbsences from '@/views/dataBigScreen/attendance/NumberOfAbsences.vue';
66
+  import DepartmentDetails from '@/views/dataBigScreen/attendance/DepartmentDetails.vue';
67
+  import ContinuousAttendance from '@/views/dataBigScreen/attendance/ContinuousAttendance.vue';
68
+  import OvertimeHours from '@/views/dataBigScreen/attendance/OvertimeHours.vue';
69
+  import LeaveType from '@/views/dataBigScreen/attendance/LeaveType.vue';
70
+  import AttendanceStatusTime from '@/views/dataBigScreen/attendance/AttendanceStatusTime.vue';
71
+  import WorkingHoursAndSeizureVolume from '@/views/dataBigScreen/attendance/WorkingHoursAndSeizureVolume.vue';
72
+</script>
73
+
74
+<style lang="scss" scoped>
75
+  .container {
76
+    width: 100%;
77
+    padding: 20px;
78
+
79
+    .chart-container {
80
+      display: flex;
81
+      height: 400px;
82
+      margin: 16px 0;
83
+
84
+      .w66-container {
85
+        width: 66.66%;
86
+      }
87
+
88
+      .w33-container {
89
+        width: calc(33.33% - 16px);
90
+        margin-right: 16px;
91
+
92
+        &.ml16 {
93
+          margin: 0 0 0 16px !important;
94
+        }
95
+
96
+        &:nth-last-child(1) {
97
+          width: 33.33%;
98
+          margin: 0;
99
+        }
100
+      }
101
+    }
102
+  }
103
+</style>

+ 102 - 0
src/views/dataBigScreen/dashboard-work/AbnormalSituationRanking.vue

@@ -0,0 +1,102 @@
1
+<template>
2
+  <scroll-seamless :list="list" title="异常情况排名">
3
+    <template #default="{ row, index }">
4
+      <span class="item ranking">
5
+        <i :class="{ first: index === 0, second: index === 1, third: index === 2}" class="index">{{ index + 1 }}</i>
6
+      </span>
7
+      <span class="item title">{{ row.item_name }}</span>
8
+      <el-progress
9
+        :percentage="row.fail_percentage"
10
+        :show-text="false"
11
+        :stroke-width="10"
12
+        :text-inside="true"
13
+        color="#409eff"
14
+      />
15
+
16
+      <span class="item num">异常{{ row.fail_count }}次</span>
17
+      <span class="item pie">异常率{{ row.fail_percentage }}%</span>
18
+    </template>
19
+  </scroll-seamless>
20
+</template>
21
+
22
+<script setup>
23
+  import { onMounted, ref } from 'vue';
24
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
25
+  import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
26
+
27
+  const classOptions = {
28
+    limitMoveNum: 7,
29
+  };
30
+
31
+  let list = ref([]);
32
+
33
+  onMounted(() => {
34
+    executeSqlByKeyAndParam('abnormal_situation_ranking', {}).then(res => {
35
+      list.value = res.data;
36
+    });
37
+  });
38
+</script>
39
+
40
+<style lang="less" scoped>
41
+
42
+  .item {
43
+    &.ranking {
44
+      overflow: hidden;
45
+      text-overflow: ellipsis;
46
+      white-space: nowrap;
47
+      margin-right: 8px;
48
+    }
49
+
50
+    &.title {
51
+      width: 80px;
52
+      overflow: hidden;
53
+      text-overflow: ellipsis;
54
+      white-space: nowrap;
55
+      margin-right: 8px;
56
+    }
57
+
58
+    .index {
59
+      display: inline-flex;
60
+      justify-content: center;
61
+      align-items: center;
62
+      width: 16px;
63
+      height: 16px;
64
+      background: #5470c6;
65
+      border-radius: 4px;
66
+      font-style: normal;
67
+
68
+      &.first {
69
+        background: #ff3232;
70
+      }
71
+
72
+      &.second {
73
+        background: #ff9800;
74
+      }
75
+
76
+      &.third {
77
+        background: #ffd200;
78
+      }
79
+    }
80
+  }
81
+
82
+  :deep(.el-progress) {
83
+    flex: 1;
84
+  }
85
+
86
+  :deep(.el-progress-bar__outer) {
87
+    background-color: #164B7F;
88
+  }
89
+
90
+  .num {
91
+    width: 50px;
92
+    margin-left: 12px;
93
+    white-space: nowrap;
94
+  }
95
+
96
+  .pie {
97
+    width: 80px;
98
+    margin-left: 12px;
99
+    color: #73c0de;
100
+    white-space: nowrap;
101
+  }
102
+</style>

+ 114 - 0
src/views/dataBigScreen/dashboard-work/AbnormalTeam.vue

@@ -0,0 +1,114 @@
1
+<template>
2
+  <div class="tips">
3
+    <div v-for="item in list" :class="item.level" class="tips-item">
4
+      <div class="title-content">
5
+        {{ item.title }}
6
+        <span>{{ item.levelName }}</span>
7
+      </div>
8
+      <span class="container">{{ item.content }}</span>
9
+      <div class="footer">
10
+        <span>{{ item.time }}</span>
11
+        <span class="btn">处理</span>
12
+      </div>
13
+    </div>
14
+  </div>
15
+</template>
16
+
17
+<script setup>
18
+  import { onMounted, onUnmounted, ref } from 'vue';
19
+
20
+  const list = ref([
21
+    {
22
+      title: '安检员操作不规范',
23
+      content: 'T3航站楼B区3号通道,行李检查未按标准流程操作',
24
+      time: '5分钟前',
25
+      level: 'high',
26
+      levelName: '紧急',
27
+    },
28
+    {
29
+      title: '安检员仪容仪表不达标',
30
+      content: 'T1航站楼A区1号通道,安检员着装不整齐',
31
+      time: '12分钟前',
32
+      level: 'middle',
33
+      levelName: '中等',
34
+    },
35
+  ]);
36
+  onMounted(() => {
37
+
38
+  });
39
+
40
+  onUnmounted(() => {
41
+
42
+  });
43
+</script>
44
+
45
+<style lang="less" scoped>
46
+  .tips {
47
+    height: calc(100% - 50px);
48
+    padding: 10px 20px;
49
+
50
+    &:hover {
51
+      overflow: auto;
52
+    }
53
+  }
54
+
55
+  .tips-item {
56
+    display: flex;
57
+    flex-flow: column;
58
+    width: 100%;
59
+    padding: 12px;
60
+    border: 1px solid #5B3F49;
61
+    border-radius: 8px;
62
+    background: #423B48;
63
+    font-size: 16px;
64
+    color: #fbffff;
65
+
66
+    &:not(:last-child) {
67
+      margin-bottom: 12px;
68
+    }
69
+
70
+    .title-content {
71
+      display: flex;
72
+      justify-content: space-between;
73
+      align-items: center;
74
+
75
+      span {
76
+        display: inline-flex;
77
+        background: #64404B;
78
+        border-radius: 10px;
79
+        padding: 4px 8px;
80
+        color: #DD5754;
81
+        font-size: 12px;
82
+      }
83
+    }
84
+
85
+    &.middle {
86
+      border-color: #545041;
87
+      background: #444543;
88
+
89
+      .title-content {
90
+        span {
91
+          color: #E0A63F;
92
+          background: #665A3E;
93
+        }
94
+      }
95
+    }
96
+
97
+    .container {
98
+      margin: 12px 0;
99
+      font-size: 14px;
100
+    }
101
+
102
+    .footer {
103
+      font-size: 12px;
104
+      color: #8D909B;
105
+      display: flex;
106
+      justify-content: space-between;
107
+      align-items: center;
108
+
109
+      .btn {
110
+        color: #314FAA;
111
+      }
112
+    }
113
+  }
114
+</style>

+ 130 - 0
src/views/dataBigScreen/dashboard-work/DateTotal.vue

@@ -0,0 +1,130 @@
1
+<template>
2
+  <div style="width: 100%; height: 100%; display: flex; flex-flow: column;">
3
+    <el-date-picker
4
+      v-model="calendarDate"
5
+      :clearable="false"
6
+      :picker-options="pickerOptions"
7
+      end-placeholder="结束日期"
8
+      range-separator="至"
9
+      start-placeholder="开始日期"
10
+      type="daterange"
11
+      unlink-panels
12
+      value-format="YYYY-MM-DD"
13
+    >
14
+    </el-date-picker>
15
+    <div class="total">
16
+      <div class="count-inner">
17
+        <div class="item-title">抽查次数</div>
18
+        <div class="count">
19
+          <vue-count-to :duration="2600" :end-val="totalNum.total_count" :start-val="0" class="card-panel-num" />
20
+        </div>
21
+      </div>
22
+      <div class="count-inner">
23
+        <div class="item-title">发现问题数</div>
24
+        <div class="count">
25
+          <vue-count-to :duration="2600" :end-val="totalNum.fail_count" :start-val="0" class="card-panel-num" />
26
+        </div>
27
+      </div>
28
+      <div class="count-inner">
29
+        <div class="item-title">异常率</div>
30
+        <div class="count">
31
+          <vue-count-to :duration="2600" :end-val="totalNum.fail_percentage" :start-val="0" class="card-panel-num" />
32
+          <span class="card-panel-num">%</span>
33
+        </div>
34
+      </div>
35
+    </div>
36
+  </div>
37
+</template>
38
+
39
+<script setup>
40
+  import { onMounted, onUnmounted, ref, watchEffect } from 'vue';
41
+  import * as echarts from 'echarts';
42
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
43
+  import VueCountTo from '@/components/VueCountTo/vue-countTo.vue';
44
+
45
+  const pickerOptions = {
46
+    shortcuts: [{
47
+      text: '最近一周',
48
+      onClick(picker) {
49
+        const end = new Date();
50
+        const start = new Date();
51
+        start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
52
+        picker.$emit('pick', [start, end]);
53
+      },
54
+    }, {
55
+      text: '最近一个月',
56
+      onClick(picker) {
57
+        const end = new Date();
58
+        const start = new Date();
59
+        start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
60
+        picker.$emit('pick', [start, end]);
61
+      },
62
+    }, {
63
+      text: '最近三个月',
64
+      onClick(picker) {
65
+        const end = new Date();
66
+        const start = new Date();
67
+        start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
68
+        picker.$emit('pick', [start, end]);
69
+      },
70
+    }],
71
+  };
72
+  let calendarDate = ref(['2023-03-01', '2023-03-30']);
73
+  let totalNum = ref({
74
+    total_count: 0,
75
+    fail_count: 0,
76
+    fail_percentage: 0,
77
+  });
78
+
79
+  watchEffect(() => {
80
+    const [startDate, endDate] = calendarDate.value || ['2023-03-01', '2023-03-30'];
81
+    executeSqlByKeyAndParam('select_time_range_and_number_of_spot_checks', { startDate, endDate }).then(res => {
82
+      totalNum.value = res.data[0];
83
+    });
84
+  });
85
+</script>
86
+
87
+<style scoped>
88
+  .total {
89
+    display: flex;
90
+    justify-content: space-between;
91
+    margin: 10px 10px 20px;
92
+  }
93
+
94
+  :deep(.el-date-editor) {
95
+    background: inherit;
96
+    flex: 0 0 40px;
97
+    width: 100%;
98
+    color: #fbffff;
99
+
100
+    .el-range-input, .el-range-separator {
101
+      color: #fbffff;
102
+    }
103
+  }
104
+
105
+  .item-title {
106
+    position: relative;
107
+    display: flex;
108
+    z-index: 1;
109
+    align-content: flex-start;
110
+    background: url('../../../assets/images/titlebg-4340cf1c.png');
111
+    background-size: 100% 100%;
112
+    font-weight: 400;
113
+    font-size: 14px;
114
+    color: #fbffff;
115
+    padding: 8px 20px 8px 12px;
116
+    border-radius: 8px;
117
+    white-space: nowrap;
118
+  }
119
+
120
+  .count-inner {
121
+    display: flex;
122
+    align-items: center;
123
+    justify-content: center;
124
+  }
125
+
126
+  :deep(.card-panel-num) {
127
+    font-size: 20px;
128
+    color: #fbffff;
129
+  }
130
+</style>

+ 81 - 0
src/views/dataBigScreen/dashboard-work/DeliberateConcealment.vue

@@ -0,0 +1,81 @@
1
+<template>
2
+  <div class="deliberate-concealment">
3
+    <div class="thead">
4
+      <span style="width: 12%">排名</span>
5
+      <span>隐匿类别</span>
6
+      <span>隐匿物品</span>
7
+      <span>隐匿数量</span>
8
+      <span>风险等级</span>
9
+    </div>
10
+    <vue3-scroll-seamless
11
+      :classOptions="classOptions"
12
+      :dataList="list"
13
+      class="scroll-wrap"
14
+    >
15
+      <div v-for="(item, index) of list" :key="index" class="tbody">
16
+        <span :class="{ main: index < 3 }" class="index" style="width: 12%">{{ index + 1 }}</span>
17
+        <span>{{ item.item_name }}</span>
18
+        <span>{{ item.category_name }}</span>
19
+        <span class="num">{{ item.total_quantity }}</span>
20
+        <span
21
+          :class="{ low: item.danger_level === 'LOW', medium: item.danger_level === 'MEDIUM', high: item.danger_level === 'HIGH'}"
22
+        >
23
+          {{ item.dict_label }}
24
+        </span>
25
+      </div>
26
+    </vue3-scroll-seamless>
27
+  </div>
28
+</template>
29
+
30
+<script setup>
31
+  import { onMounted, ref } from 'vue';
32
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
33
+
34
+  const classOptions = {
35
+    limitMoveNum: 6,
36
+  };
37
+
38
+  let list = ref([]);
39
+
40
+  onMounted(() => {
41
+    executeSqlByKeyAndParam('intentionally_concealing_the_situation', {}).then(res => {
42
+      list.value = res.data;
43
+    });
44
+  });
45
+</script>
46
+
47
+<style lang="less" scoped>
48
+  .deliberate-concealment {
49
+    height: calc(100% - 50px);
50
+    overflow: hidden;
51
+  }
52
+
53
+  .thead, .tbody {
54
+    span {
55
+      display: inline-flex;
56
+      align-items: center;
57
+      justify-content: center;
58
+      color: #fbffff;
59
+      font-size: 12px;
60
+      padding: 0 4px;
61
+      width: 22%;
62
+
63
+      &.low, &.num {
64
+        color: #91cc75;
65
+      }
66
+
67
+      &.medium {
68
+        color: #fac858;
69
+      }
70
+
71
+      &.high {
72
+        color: #ee6666;
73
+      }
74
+    }
75
+  }
76
+
77
+  .scroll-wrap {
78
+    margin: 0 auto;
79
+    overflow: hidden;
80
+  }
81
+</style>

+ 78 - 0
src/views/dataBigScreen/dashboard-work/DistributionOfSeizureResults.vue

@@ -0,0 +1,78 @@
1
+<template>
2
+  <div ref="distributionOfSeizureResultsChart" class="echarts" />
3
+</template>
4
+
5
+<script setup>
6
+  import { onMounted, onUnmounted, ref } from 'vue';
7
+  import * as echarts from 'echarts';
8
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
9
+
10
+  const chart = ref(null);
11
+  const distributionOfSeizureResultsChart = ref(null);
12
+  const chartOption = ref({
13
+    color: [
14
+      '#3C516A',
15
+      new echarts.graphic.LinearGradient(0, 1, 0, 0, [{ offset: 0, color: '#0AE8FE' }, {
16
+        offset: 1,
17
+        color: '#47B3FE',
18
+      }], false),
19
+    ],
20
+    series: [
21
+      {
22
+        type: 'pie',
23
+        height: '60%',
24
+        center: ['25%', '50%'],
25
+        label: {
26
+          show: false,
27
+        },
28
+        data: [],
29
+      },
30
+      {
31
+        type: 'pie',
32
+        height: '60%',
33
+        center: ['75%', '50%'],
34
+        label: {
35
+          show: false,
36
+        },
37
+        data: [],
38
+      },
39
+    ],
40
+  });
41
+
42
+  const initChart = () => {
43
+    chart.value = echarts.init(distributionOfSeizureResultsChart.value, 'macarons');
44
+    chart.value.setOption(chartOption.value);
45
+  };
46
+
47
+  onMounted(() => {
48
+    executeSqlByKeyAndParam('category_distribution', {}).then(res => {
49
+      chartOption.value.series[0].data = res.data.map((el, idx) => {
50
+        return {
51
+          value: el.total_quantity,
52
+          name: el.category_name,
53
+        };
54
+      });
55
+      chartOption.value.series[1].data = chartOption.value.series[0].data;
56
+      initChart();
57
+    });
58
+
59
+    window.addEventListener('resize', () => {
60
+      chart.value.resize();
61
+    });
62
+  });
63
+
64
+  onUnmounted(() => {
65
+    if (!chart.value) {
66
+      return;
67
+    }
68
+    chart.value.dispose();
69
+    chart.value = null;
70
+  });
71
+</script>
72
+
73
+<style scoped>
74
+  .echarts {
75
+    height: 100%;
76
+    width: 100%;
77
+  }
78
+</style>

+ 88 - 0
src/views/dataBigScreen/dashboard-work/InspectionStaff.vue

@@ -0,0 +1,88 @@
1
+<template>
2
+  <scroll-seamless :list="list" title="巡检人员表现排名">
3
+    <template #thead>
4
+      <span class="item ranking">排名</span>
5
+      <span class="item">巡检员</span>
6
+      <span class="item">巡检次数</span>
7
+      <span class="item">发现问题数</span>
8
+      <span class="item">状态</span>
9
+    </template>
10
+    <template #default="{ row, index }">
11
+      <span class="item ranking">
12
+        <i :class="{ first: index === 0, second: index === 1, third: index === 2}" class="index">{{ index + 1 }}</i>
13
+      </span>
14
+      <span class="item">{{ row.user_name }}</span>
15
+      <span class="item">{{ row.total_count }}</span>
16
+      <span class="item num">{{ row.fail_count }}</span>
17
+      <div class="item">
18
+        <span :class="{ done: row.result === '优秀', wait: row.result === '不及格' }">
19
+          {{ row.result }}
20
+        </span>
21
+      </div>
22
+    </template>
23
+  </scroll-seamless>
24
+</template>
25
+
26
+<script setup>
27
+  import { onMounted, ref } from 'vue';
28
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
29
+  import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
30
+
31
+  let list = ref([]);
32
+
33
+  onMounted(() => {
34
+    executeSqlByKeyAndParam('performance_ranking_of_inspection_personnel', {}).then(res => {
35
+      list.value = res.data;
36
+    });
37
+  });
38
+</script>
39
+
40
+<style lang="less" scoped>
41
+  .item {
42
+    display: inline-flex;
43
+    align-items: center;
44
+    justify-content: center;
45
+    width: 22.5%;
46
+
47
+    &.ranking {
48
+      width: 10%;
49
+    }
50
+
51
+    .index {
52
+      display: inline-flex;
53
+      justify-content: center;
54
+      align-items: center;
55
+      width: 16px;
56
+      height: 16px;
57
+      background: #5470c6;
58
+      border-radius: 4px;
59
+      font-style: normal;
60
+
61
+      &.first {
62
+        background: #ff3232;
63
+      }
64
+
65
+      &.second {
66
+        background: #ff9800;
67
+      }
68
+
69
+      &.third {
70
+        background: #ffd200;
71
+      }
72
+    }
73
+
74
+    .done {
75
+      padding: 4px 8px;
76
+      background: rgba(29, 71, 70, .5);
77
+      color: #12A979;
78
+      border-radius: 20px;
79
+    }
80
+
81
+    .wait {
82
+      padding: 4px 8px;
83
+      background: rgba(76, 72, 51, .5);
84
+      color: #E7B226;
85
+      border-radius: 20px;
86
+    }
87
+  }
88
+</style>

+ 85 - 0
src/views/dataBigScreen/dashboard-work/LineChart.vue

@@ -0,0 +1,85 @@
1
+<template>
2
+  <div ref="lineChart" class="echarts" />
3
+</template>
4
+
5
+<script setup>
6
+  import { onMounted, ref } from 'vue';
7
+  import * as echarts from 'echarts';
8
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
9
+  import { useEcharts } from '@/hooks/chart.js';
10
+
11
+  const lineChart = ref(null);
12
+  const chartOption = ref({
13
+    grid: {
14
+      left: '30',
15
+      top: '30',
16
+      right: '30',
17
+      bottom: '70',
18
+      containLabel: true,
19
+    },
20
+    xAxis: {
21
+      data: [],
22
+      axisLine: {
23
+        lineStyle: {
24
+          color: '#fbffff',
25
+        },
26
+      },
27
+      axisLabel: {
28
+        interval: 3,
29
+        rotate: 25,
30
+      },
31
+    },
32
+    yAxis: {
33
+      type: 'value',
34
+      axisLine: {
35
+        lineStyle: {
36
+          color: '#fbffff',
37
+        },
38
+      },
39
+    },
40
+    series: [
41
+      {
42
+        type: 'line',
43
+        symbol: 'none',
44
+        smooth: true,
45
+        itemStyle: {
46
+          color: '#fc8452',
47
+          borderDashOffset: 20,
48
+        },
49
+        endLabel: {
50
+          show: false,
51
+        },
52
+        areaStyle: {
53
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
54
+            {
55
+              offset: 0,
56
+              color: 'rgb(255, 70, 131)',
57
+            },
58
+            {
59
+              offset: 1,
60
+              color: 'rgb(255, 158, 68)',
61
+            },
62
+          ]),
63
+        },
64
+        data: [],
65
+      },
66
+    ],
67
+  });
68
+  const { setOption } = useEcharts(lineChart);
69
+
70
+  onMounted(() => {
71
+    const date = new Date('2023-03-15');
72
+    executeSqlByKeyAndParam('trend_of_abnormal_inspection_rate', { date: date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() }).then(res => {
73
+      chartOption.value.series[0].data = res.data.map(el => el.fail_count);
74
+      chartOption.value.xAxis.data = res.data.map(el => el.day);
75
+      setOption?.(chartOption.value);
76
+    });
77
+  });
78
+</script>
79
+
80
+<style scoped>
81
+  .echarts {
82
+    height: 100%;
83
+    width: 100%;
84
+  }
85
+</style>

+ 248 - 0
src/views/dataBigScreen/dashboard-work/SamplingPartySituation.vue

@@ -0,0 +1,248 @@
1
+<template>
2
+  <div style="width: 100%; height: calc(100% - 50px)">
3
+    <div ref="samplingPartySituationChartBar" class="echarts" />
4
+    <div ref="samplingPartySituationChart" class="echarts" />
5
+  </div>
6
+</template>
7
+
8
+<script setup>
9
+  import { onMounted, onUnmounted, ref } from 'vue';
10
+  import * as echarts from 'echarts';
11
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
12
+  import { useEcharts } from '@/hooks/chart.js';
13
+
14
+  const samplingPartySituationChartBar = ref(null);
15
+  const chartBarOption = ref({
16
+    grid: {
17
+      left: '20',
18
+      top: '40',
19
+      right: '20',
20
+      bottom: '0',
21
+      containLabel: true,
22
+    },
23
+    xAxis: {
24
+      type: 'category',
25
+      data: ['班组级', '科级', '站级'],
26
+      axisLine: {
27
+        lineStyle: {
28
+          color: '#fbffff',
29
+        },
30
+      },
31
+    },
32
+    yAxis: {
33
+      type: 'value',
34
+      name: '抽查次数',
35
+      axisLine: {
36
+        lineStyle: {
37
+          color: '#fbffff',
38
+        },
39
+      },
40
+    },
41
+    series: [
42
+      {
43
+        data: [],
44
+        type: 'bar',
45
+      },
46
+    ],
47
+  });
48
+  const { setOption: setSamplingPartySituationBarOption } = useEcharts(samplingPartySituationChartBar);
49
+
50
+  const color = {
51
+    colorA: {
52
+      type: 'linear',
53
+      x: 1,
54
+      y: 0,
55
+      x2: 0,
56
+      y2: 1,
57
+      colorStops: [
58
+        {
59
+          offset: 0,
60
+          color: '#3AF5C3', // 0% 处的颜色
61
+        },
62
+        {
63
+          offset: 1,
64
+          color: '#3DF1AB', // 100% 处的颜色
65
+        },
66
+      ],
67
+      global: false, // 缺省为 false
68
+    },
69
+    colorB: {
70
+      type: 'linear',
71
+      x: 1,
72
+      y: 0,
73
+      x2: 0,
74
+      y2: 1,
75
+      colorStops: [
76
+        {
77
+          offset: 0,
78
+          color: '#14C5CE', // 0% 处的颜色
79
+        },
80
+        {
81
+          offset: 1,
82
+          color: '#09AEE8', // 100% 处的颜色
83
+        },
84
+      ],
85
+      global: false, // 缺省为 false
86
+    },
87
+    colorC: {
88
+      type: 'linear',
89
+      x: 1,
90
+      y: 0,
91
+      x2: 0,
92
+      y2: 1,
93
+      colorStops: [
94
+        {
95
+          offset: 0,
96
+          color: '#FF664E', // 0% 处的颜色
97
+        },
98
+        {
99
+          offset: 1,
100
+          color: '#FF4056', // 100% 处的颜色
101
+        },
102
+      ],
103
+      global: false, // 缺省为 false
104
+    },
105
+  };
106
+  const ops = ref({
107
+    radius: '60%',
108
+    startAngle: 90,
109
+    endAngle: -270,
110
+    pointer: {
111
+      show: false,
112
+    },
113
+    splitLine: {
114
+      show: false,
115
+    },
116
+    axisTick: {
117
+      show: false,
118
+    },
119
+    axisLabel: {
120
+      show: false,
121
+    },
122
+    title: {
123
+      fontSize: 12,
124
+      color: '#fbffff',
125
+    },
126
+    detail: {
127
+      fontSize: 12,
128
+      color: '#fbffff',
129
+    },
130
+  });
131
+  const samplingPartySituationChart = ref(null);
132
+  const chartOption = ref({
133
+    series: [
134
+      {
135
+        type: 'gauge',
136
+        center: ['15%', '50%'],
137
+        data: [
138
+          {
139
+            value: 0,
140
+            name: '班组级异常率',
141
+            title: {
142
+              offsetCenter: ['0%', '-10%'],
143
+            },
144
+            detail: {
145
+              valueAnimation: true,
146
+              formatter: '{value}%',
147
+            },
148
+          },
149
+        ],
150
+        ...ops.value,
151
+        progress: {
152
+          show: true,
153
+          overlap: false,
154
+          roundCap: true,
155
+          clip: false,
156
+          itemStyle: {
157
+            color: color.colorA,
158
+          },
159
+        },
160
+      },
161
+      {
162
+        type: 'gauge',
163
+        center: ['50%', '50%'],
164
+        data: [
165
+          {
166
+            value: 0,
167
+            name: '班科级异常率',
168
+            title: {
169
+              offsetCenter: ['0%', '-10%'],
170
+            },
171
+            detail: {
172
+              valueAnimation: true,
173
+              formatter: '{value}%',
174
+            },
175
+          },
176
+        ],
177
+        ...ops.value,
178
+        progress: {
179
+          show: true,
180
+          overlap: false,
181
+          roundCap: true,
182
+          clip: false,
183
+          itemStyle: {
184
+            color: color.colorA,
185
+          },
186
+        },
187
+      },
188
+      {
189
+        type: 'gauge',
190
+        center: ['85%', '50%'],
191
+        data: [
192
+          {
193
+            value: 0,
194
+            name: '站级异常率',
195
+            title: {
196
+              offsetCenter: ['0%', '-10%'],
197
+            },
198
+            detail: {
199
+              valueAnimation: true,
200
+              formatter: '{value}%',
201
+            },
202
+          },
203
+        ],
204
+        ...ops.value,
205
+        progress: {
206
+          show: true,
207
+          overlap: false,
208
+          roundCap: true,
209
+          clip: false,
210
+          itemStyle: {
211
+            color: color.colorA,
212
+          },
213
+        },
214
+      },
215
+    ],
216
+  });
217
+  const { setOption: setSamplingPartySituationOption } = useEcharts(samplingPartySituationChart);
218
+
219
+
220
+  onMounted(() => {
221
+    executeSqlByKeyAndParam('spot_check_the_situation_of_the_inspection_party', {}).then(res => {
222
+      const data = res.data;
223
+      chartBarOption.value.series[0].data = data.map(el => el.total_count);
224
+      const levelMap = {
225
+        TEAM_LEVEL: 0,
226
+        DEPARTMENT_LEVEL: 1,
227
+        STATION_LEVEL: 2,
228
+      };
229
+
230
+      data.forEach(el => {
231
+        const index = levelMap[el.check_level];
232
+        if (index !== undefined) {
233
+          chartOption.value.series[index].progress.itemStyle.color = el.fail_percentage <= 30 ? color.colorA : el.fail_percentage <= 60 ? color.colorB : color.colorC;
234
+          chartOption.value.series[index].data[0].value = el.fail_percentage;
235
+        }
236
+      });
237
+      setSamplingPartySituationBarOption?.(chartBarOption.value);
238
+      setSamplingPartySituationOption?.(chartOption.value);
239
+    });
240
+  });
241
+</script>
242
+
243
+<style scoped>
244
+  .echarts {
245
+    height: 50%;
246
+    width: 100%;
247
+  }
248
+</style>

+ 170 - 0
src/views/dataBigScreen/dashboard-work/SpotCheckContent.vue

@@ -0,0 +1,170 @@
1
+<template>
2
+  <div class="abnormal-situation-ranking">
3
+    <div ref="abnormalSituationRankingChart" class="echarts" />
4
+
5
+    <div class="item">
6
+      <div v-for="(item, index) in list" class="item-line">
7
+        <span :style="{background:color[index]}" class="color"></span>
8
+        <span>{{ item.name }}</span>
9
+      </div>
10
+    </div>
11
+  </div>
12
+
13
+</template>
14
+
15
+<script setup>
16
+  import { computed, onMounted, ref } from 'vue';
17
+  import * as echarts from 'echarts';
18
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
19
+  import { useEcharts } from '@/hooks/chart.js';
20
+
21
+  const color = [
22
+    '#1DD8FE',
23
+    '#8639F8',
24
+  ];
25
+  const abnormalSituationRankingChart = ref(null);
26
+  const chartOption = ref({
27
+    color,
28
+    radar: [
29
+      {
30
+        indicator: [],
31
+        nameGap: 3,
32
+        center: ['50%', '50%'],
33
+        radius: 80,
34
+        splitNumber: 4,
35
+        axisName: {
36
+          color: '#fbffff',
37
+        },
38
+        axisLine: {
39
+          lineStyle: {
40
+            color: '#fbffff',
41
+            opacity: 0.1,
42
+          },
43
+        },
44
+        splitArea: {
45
+          areaStyle: {
46
+            color: ['none', 'none'],
47
+          },
48
+        },
49
+        splitLine: {
50
+          lineStyle: {  //所有圆环分隔线。
51
+            color: '#fbffff', //分隔线颜色
52
+            opacity: 0.5,
53
+          },
54
+        },
55
+      },
56
+    ],
57
+    series: [
58
+      {
59
+        type: 'radar',
60
+        data: [
61
+          {
62
+            value: [],
63
+            symbol: 'none',
64
+            areaStyle: {
65
+              color: new echarts.graphic.RadialGradient(1, 1, 1, [
66
+                {
67
+                  color: 'rgba(24,139,178, 0.5)',
68
+                  offset: 0,
69
+                },
70
+                {
71
+                  color: 'rgba(58,146,228, 0.9)',
72
+                  offset: 1,
73
+                },
74
+              ]),
75
+            },
76
+          },
77
+          {
78
+            value: [],
79
+            symbol: 'none',
80
+            areaStyle: {
81
+              color: new echarts.graphic.RadialGradient(1, 1, 1, [
82
+                {
83
+                  color: 'rgba(58,148,228, 0.5)',
84
+                  offset: 0,
85
+                },
86
+                {
87
+                  color: 'rgba(98,49,189, 0.9)',
88
+                  offset: 1,
89
+                },
90
+              ]),
91
+            },
92
+          },
93
+        ],
94
+      },
95
+    ],
96
+  });
97
+  const { setOption } = useEcharts(abnormalSituationRankingChart);
98
+
99
+  const list = computed(() => {
100
+    return [
101
+      { name: '抽查正常', color: color[0] },
102
+      { name: '抽查异常', color: color[1] },
103
+    ];
104
+  });
105
+
106
+  onMounted(() => {
107
+    executeSqlByKeyAndParam('spot_check_content_distribution', {}).then(res => {
108
+      chartOption.value.radar[0].indicator = res.data.map((el, idx) => {
109
+        return {
110
+          max: el.total_count,
111
+          text: el.category_name,
112
+        };
113
+      });
114
+      chartOption.value.series[0].data[0].value = res.data.map(el => el.pass_count);
115
+      chartOption.value.series[0].data[1].value = res.data.map(el => el.fail_count);
116
+
117
+      setOption?.(chartOption.value);
118
+    });
119
+  });
120
+</script>
121
+
122
+<style lang="less" scoped>
123
+  .abnormal-situation-ranking {
124
+    display: flex;
125
+    height: calc(100% - 40px);
126
+
127
+    .echarts {
128
+      flex: 1;
129
+      height: 100%;
130
+      width: 100%;
131
+    }
132
+
133
+    .item {
134
+      width: 100px;
135
+      padding: 20px 0;
136
+      display: flex;
137
+      justify-content: center;
138
+      flex-flow: column;
139
+    }
140
+
141
+    .item-line {
142
+      display: flex;
143
+      align-items: center;
144
+      color: #fbffff;
145
+      font-size: 12px;
146
+      width: 100%;
147
+      margin: 8px 0;
148
+
149
+      .color {
150
+        display: inline-flex;
151
+        width: 10px;
152
+        height: 10px;
153
+        border-radius: 5px;
154
+        margin-right: 4px;
155
+      }
156
+
157
+      .text {
158
+        flex: 1;
159
+        display: flex;
160
+        justify-content: space-between;
161
+
162
+        span {
163
+          display: inline-flex;
164
+          width: 33.33%;
165
+        }
166
+      }
167
+    }
168
+  }
169
+</style>
170
+

+ 242 - 0
src/views/dataBigScreen/dashboard-work/WorkQuality.vue

@@ -0,0 +1,242 @@
1
+<template>
2
+  <div class="work-quality">
3
+    <div class="dashboard-title">航站楼区域巡检热力图</div>
4
+
5
+    <div ref="workQualityLine" style="width: 100%; height: 40%" />
6
+    <div style="display: flex; justify-content: space-between;align-items: center; margin-top: 12px">
7
+      <div class="dashboard-title">巡检问题分析</div>
8
+      <div class="dashboard-right-menu">
9
+        <span
10
+          :class="{active: currentType === 'ranking_of_seizures_by_department'}"
11
+          @click="handleClick('ranking_of_seizures_by_department')"
12
+        >
13
+          今日
14
+        </span>
15
+        <span
16
+          :class="{active: currentType === 'ranking_of_seizures_by_team'}"
17
+          @click="handleClick('ranking_of_seizures_by_team')"
18
+        >
19
+          本周
20
+        </span>
21
+        <span
22
+          :class="{active: currentType === 'ranking_of_seizures_by_individual'}"
23
+          @click="handleClick('ranking_of_seizures_by_individual')"
24
+        >
25
+          本月
26
+        </span>
27
+      </div>
28
+    </div>
29
+    <div style="height: 50%; display: flex;">
30
+      <div ref="workQuality" style="width: 50%; height: 100%" />
31
+      <div ref="workQualityPie" style="width: 50%; height: 100%" />
32
+    </div>
33
+  </div>
34
+</template>
35
+<script setup>
36
+  import { onMounted, ref } from 'vue';
37
+  import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
38
+  import { useEcharts } from '@/hooks/chart.js';
39
+
40
+  const currentType = ref('ranking_of_seizures_by_department');
41
+  const handleClick = (type) => {
42
+    currentType.value = type;
43
+  };
44
+
45
+  // 航站楼区域巡检热力图
46
+  const workQualityLine = ref(null);
47
+  const chartLineOption = ref({
48
+    grid: {
49
+      left: '10',
50
+      top: '20',
51
+      right: '20',
52
+      bottom: '0',
53
+      containLabel: true,
54
+    },
55
+    visualMap: {
56
+      min: 0,
57
+      max: 100,
58
+      calculable: false,
59
+      orient: 'horizontal',
60
+      itemWidth: 0,
61
+      inRange: {
62
+        color: ['#F9CBAA', '#C55C10', '#843E0B', '#785E01'],
63
+      },
64
+    },
65
+    xAxis: {
66
+      type: 'category',
67
+      data: [],
68
+      axisLine: {
69
+        lineStyle: {
70
+          color: '#fbffff',
71
+        },
72
+      },
73
+      axisTick: {
74
+        show: false,
75
+      },
76
+    },
77
+    yAxis: {
78
+      type: 'category',
79
+      data: [],
80
+      axisLine: {
81
+        lineStyle: {
82
+          color: '#fbffff',
83
+        },
84
+      },
85
+      axisTick: {
86
+        show: false,
87
+      },
88
+    },
89
+    series: [
90
+      {
91
+        type: 'heatmap',
92
+        data: [],
93
+        itemStyle: {
94
+          borderWidth: 1, //设置border宽度
95
+          borderColor: '#fff',
96
+        },
97
+        label: {
98
+          show: true,
99
+        },
100
+      },
101
+    ],
102
+  });
103
+  const { setOption: setLineOption } = useEcharts(workQualityLine);
104
+
105
+  // 问题类型分布
106
+  const workQuality = ref(null);
107
+  const chartOption = ref({
108
+    title: {
109
+      text: '问题类型分布',
110
+      left: 'center',
111
+      textStyle: {
112
+        fontSize: 14,
113
+        color: '#fbffff',
114
+      },
115
+    },
116
+    legend: {
117
+      bottom: 10,
118
+      textStyle: {
119
+        fontSize: 10,
120
+        color: '#fbffff',
121
+      },
122
+    },
123
+    tooltip: {
124
+      show: false,
125
+    },
126
+    series: [
127
+      {
128
+        type: 'pie',
129
+        legendHoverLink: false,
130
+        radius: '40%',
131
+        center: ['50%', '40%'],
132
+        label: {
133
+          show: false,
134
+          position: 'center',
135
+        },
136
+        emphasis: {
137
+          disable: false,  //是否关闭扇区高亮效果
138
+          scale: false,    //扇区是否缩放
139
+          scaleSize: 0,
140
+        },
141
+        labelLine: {
142
+          show: false,
143
+        },
144
+        data: [],
145
+      },
146
+    ],
147
+  });
148
+  const { setOption } = useEcharts(workQuality);
149
+
150
+  // 问题严重程度
151
+  const workQualityPie = ref(null);
152
+  const chartPieOption = ref({
153
+    title: {
154
+      text: '问题严重程度',
155
+      left: 'center',
156
+      textStyle: {
157
+        fontSize: 14,
158
+        color: '#fbffff',
159
+      },
160
+    },
161
+    legend: {
162
+      bottom: 10,
163
+      textStyle: {
164
+        fontSize: 10,
165
+        color: '#fbffff',
166
+      },
167
+    },
168
+    tooltip: {
169
+      show: false,
170
+    },
171
+    series: [
172
+      {
173
+        type: 'pie',
174
+        legendHoverLink: false,
175
+        radius: ['40%', '30%'],
176
+        center: ['50%', '40%'],
177
+        label: {
178
+          show: false,
179
+          position: 'center',
180
+        },
181
+        emphasis: {
182
+          disable: false,  //是否关闭扇区高亮效果
183
+          scale: false,    //扇区是否缩放
184
+          scaleSize: 0,
185
+        },
186
+        labelLine: {
187
+          show: false,
188
+        },
189
+        data: [],
190
+      },
191
+    ],
192
+  });
193
+  const { setOption: setPieOption } = useEcharts(workQualityPie);
194
+
195
+  onMounted(() => {
196
+    // 航站楼区域巡检热力图
197
+    executeSqlByKeyAndParam('thermal_map_of_terminal_area_inspection', {}).then(res => {
198
+      const terminals = [...new Set(res.data.map(d => d.terminal_name))]; // Y 轴:航站楼
199
+      const regions = [...new Set(res.data.map(d => d.regional_name))];   // X 轴:区域
200
+
201
+      const heatmapData = res.data.map(item => {
202
+        const xIndex = regions.indexOf(item.regional_name);
203
+        const yIndex = terminals.indexOf(item.terminal_name);
204
+        return [xIndex, yIndex, item.total_count];
205
+      });
206
+
207
+      chartLineOption.value.xAxis.data = regions;
208
+      chartLineOption.value.yAxis.data = terminals;
209
+      chartLineOption.value.series[0].data = heatmapData;
210
+
211
+      setLineOption?.(chartLineOption.value);
212
+    });
213
+    // 巡检问题分析-问题类型分布
214
+    executeSqlByKeyAndParam('distribution_of_problem_types', {}).then(res => {
215
+      chartOption.value.series[0].data = res.data.map(el => {
216
+        return {
217
+          value: el.percentage,
218
+          name: el.category_name,
219
+        };
220
+      });
221
+      setOption?.(chartOption.value);
222
+    });
223
+    // 巡检问题分析-问题严重程度
224
+    executeSqlByKeyAndParam('severity_of_the_problem', {}).then(res => {
225
+      chartPieOption.value.series[0].data = res.data.map(el => {
226
+        return {
227
+          value: el.percentage,
228
+          name: el.dict_label,
229
+        };
230
+      });
231
+      setPieOption?.(chartPieOption.value);
232
+    });
233
+  });
234
+</script>
235
+
236
+<style lang="less" scoped>
237
+  .work-quality {
238
+    display: flex;
239
+    flex-flow: column;
240
+    height: 100%;
241
+  }
242
+</style>

+ 81 - 0
src/views/dataBigScreen/dashboard-work/index.vue

@@ -0,0 +1,81 @@
1
+<template>
2
+  <dashboard-container>
3
+    <top-title class="wow slideInDown" name="工作质量管理大屏"></top-title>
4
+
5
+    <template #left>
6
+      <div class="item item-center4">
7
+        <date-total />
8
+      </div>
9
+      <div class="item item-center1">
10
+        <div class="dashboard-title">分级质量控制</div>
11
+        <sampling-party-situation />
12
+      </div>
13
+      <div class="item item-center3">
14
+        <div class="dashboard-title">抽查内容分布</div>
15
+        <spot-check-content />
16
+      </div>
17
+    </template>
18
+    <template #center>
19
+      <div class="item item-center">
20
+        <work-quality />
21
+      </div>
22
+      <div class="item">
23
+        <div class="dashboard-title">发现问题次数走势</div>
24
+        <line-chart />
25
+      </div>
26
+    </template>
27
+    <template #right>
28
+      <div class="item">
29
+        <!-- 巡检人员表现排名 -->
30
+        <inspection-staff />
31
+      </div>
32
+      <div class="item">
33
+        <!-- 异常情况排名 -->
34
+        <abnormal-situation-ranking />
35
+      </div>
36
+      <div class="item">
37
+        <div class="dashboard-title">实时告警</div>
38
+        <abnormal-team />
39
+      </div>
40
+    </template>
41
+  </dashboard-container>
42
+</template>
43
+
44
+<script setup>
45
+  import LineChart from './LineChart.vue';
46
+  import SpotCheckContent from '@/views/dataBigScreen/dashboard-work/SpotCheckContent.vue';
47
+  import AbnormalSituationRanking from '@/views/dataBigScreen/dashboard-work/AbnormalSituationRanking.vue';
48
+  import SamplingPartySituation from '@/views/dataBigScreen/dashboard-work/SamplingPartySituation.vue';
49
+  import AbnormalTeam from '@/views/dataBigScreen/dashboard-work/AbnormalTeam.vue';
50
+  import WorkQuality from '@/views/dataBigScreen/dashboard-work/WorkQuality.vue';
51
+  import TopTitle from '@/views/dataBigScreen/dashboard/TopTitle.vue';
52
+  import InspectionStaff from '@/views/dataBigScreen/dashboard-work/InspectionStaff.vue';
53
+  import DateTotal from '@/views/dataBigScreen/dashboard-work/DateTotal.vue';
54
+  import DashboardContainer from '@/views/dataBigScreen/dashboard/components/DashboardContainer.vue';
55
+</script>
56
+
57
+<style lang="scss" scoped>
58
+  .item {
59
+    width: calc(100% + -0px);
60
+    height: calc(33.33% - 8px);
61
+    position: relative;
62
+    background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
63
+    overflow: hidden;
64
+
65
+    &.item-center {
66
+      height: calc(66.66% - 4px);
67
+    }
68
+
69
+    &.item-center1 {
70
+      height: calc(50% - 4px);
71
+    }
72
+
73
+    &.item-center3 {
74
+      height: calc(50% - 104px);
75
+    }
76
+
77
+    &.item-center4 {
78
+      height: 100px;
79
+    }
80
+  }
81
+</style>

+ 185 - 0
src/views/dataBigScreen/dashboard/ChannelDistribution.vue

@@ -0,0 +1,185 @@
1
+<template>
2
+  <scroll-seamless :list="list" title="查获通道分布">
3
+    <template #thead>
4
+      <span class="item"></span>
5
+      <span v-for="item in itemNameList" :key="item.channelCode" class="item">
6
+        {{ item.label }}<br />({{ item.total }})
7
+      </span>
8
+      <span class="item total">总数</span>
9
+    </template>
10
+    <template #default="{ row, index }">
11
+      <span class="item">{{ row.channelName }}</span>
12
+      <span v-for="(item, index) in row.item" :key="index" :class="{ num1: item.total < 15, num2: item.total > 15 }"
13
+        class="item num">
14
+        {{ item.total }}
15
+      </span>
16
+      <span class="item total">{{ row.totalQuantity }}</span>
17
+    </template>
18
+  </scroll-seamless>
19
+</template>
20
+
21
+<script setup>
22
+import { onMounted, ref } from 'vue';
23
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
24
+import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
25
+import { getChannel } from "@/api/largeScreen/largeScreen.js"
26
+import { treeSelectByType } from "@/api/system/common.js"
27
+
28
+
29
+
30
+let itemNameList = ref([]);
31
+let tabData = ref([])
32
+let list = ref([]);
33
+const pollingInterval = ref(null);
34
+onMounted(() => {
35
+  // fetchData()
36
+  pollingInterval.value = setInterval(fetchData(), 5000);
37
+});
38
+function fetchData() {
39
+  treeSelectByType("ITEM_CATEGORY", 1).then(res => {
40
+    tabData.value = res.data
41
+  })
42
+  getChannel().then(res => {
43
+    const resultMap = {};
44
+
45
+    let newArr = []
46
+    res.data.forEach(item => {
47
+      const { categoryCode, total } = item;
48
+      tabData.value.forEach(it => {
49
+        const { code, label, id } = it
50
+      
51
+
52
+        if (categoryCode == id) {
53
+          newArr.push(item)
54
+        }
55
+        if (!resultMap[id] && categoryCode == id) {
56
+
57
+
58
+          resultMap[id] = {
59
+            id,
60
+            label,
61
+            total
62
+          };
63
+        } else {
64
+
65
+          if (resultMap[id]) {
66
+            resultMap[id].total = id == categoryCode ? resultMap[id].total + total : resultMap[id].total;
67
+          }
68
+        }
69
+        // if (id == 10 && categoryCode == 10) {
70
+        //   console.log("锐器")
71
+        //   debugger
72
+
73
+        // }
74
+
75
+
76
+      })
77
+    });
78
+    itemNameList.value = Object.values(resultMap);
79
+
80
+    const groupedData = newArr.reduce((acc, item) => {
81
+      const key = item.channelName;
82
+      if (!acc[key]) {
83
+        acc[key] = {
84
+          channelName: key,
85
+          categoryCode: item.categoryCode,
86
+          item: [],
87
+        };
88
+      }
89
+      itemNameList.value.map((ite, index) => {
90
+        if (item.categoryCode == ite.id) {
91
+          acc[key].item[index] = item
92
+        } else if (acc[key].item[index]) {
93
+          acc[key].item[index] = acc[key].item[index]
94
+        } else {
95
+          acc[key].item[index] = { total: 0 }
96
+        }
97
+      })
98
+      // 只添加需要的字段
99
+      // const existingChannel = acc[key].item.find(
100
+      //   ch => {
101
+      //     ch.channelCode === item.channelCode
102
+      //   },
103
+      // );
104
+      // if (!existingChannel) {
105
+      //   acc[key].item.push(item);
106
+      // } else {y,可累加
107
+      //   existingChannel.total += item.total;
108
+      //   // 可选:如果需要合并同 channel 的 quantit
109
+      // }
110
+      acc[key].totalQuantity = acc[key].item.reduce((acc, item) => acc + item.total, 0);
111
+      return acc;
112
+    }, {});
113
+  
114
+
115
+    // 转换为数组格式
116
+    list.value = Object.values(groupedData).map(el => {
117
+      if (el.item.length !== itemNameList.value.length) {
118
+        el.item.push(...new Array(Math.max(0, itemNameList.value.length - el.item.length)).fill({ total: 0 }));
119
+      }
120
+      return el;
121
+    });
122
+
123
+  }).catch(error => {
124
+    console.error(error);
125
+    clearInterval(pollingInterval.value);
126
+  });
127
+  return fetchData
128
+}
129
+</script>
130
+
131
+<style lang="less" scoped>
132
+.item {
133
+  display: inline-flex;
134
+  align-items: center;
135
+  justify-content: center;
136
+  width: calc(18% - 4px);
137
+  white-space: nowrap;
138
+  height: 28px;
139
+  margin: 2px;
140
+  overflow: hidden;
141
+
142
+  &.total {
143
+    width: 10%;
144
+  }
145
+}
146
+
147
+.channel-distribution {
148
+  position: relative;
149
+  height: calc(100% - 50px);
150
+  overflow: hidden;
151
+}
152
+
153
+.channel-distribution-thead,
154
+.channel-distribution-tbody {
155
+  display: flex;
156
+  justify-content: center;
157
+  align-items: center;
158
+
159
+  span {
160
+    display: inline-flex;
161
+    align-items: center;
162
+    justify-content: center;
163
+    color: #fbffff;
164
+    font-size: 12px;
165
+    width: calc(16.66% - 4px);
166
+    white-space: nowrap;
167
+    height: 28px;
168
+    margin: 2px;
169
+  }
170
+}
171
+
172
+.num {
173
+  display: inline-flex;
174
+  background: #bbe1ff;
175
+  padding: 2px;
176
+
177
+  &.num1 {
178
+    background: #7daaff;
179
+  }
180
+
181
+  &.num2 {
182
+    background: #3d76dd;
183
+  }
184
+}
185
+</style>

+ 288 - 0
src/views/dataBigScreen/dashboard/ClassificationOfSeizedQuantity.vue

@@ -0,0 +1,288 @@
1
+<template>
2
+  <div class="classification-of-seized-quantity">
3
+    <div class="total">
4
+      <div class="count-inner">
5
+        <div class="item-title">今日查获总数</div>
6
+        <div class="count">
7
+          <vue-count-to :duration="2600" :end-val="totalNum.total" :start-val="0" class="card-panel-num" />
8
+        </div>
9
+        <div class="tips">
10
+          <span class="icon"><svg-icon icon-class="top" /> 较昨日{{ totalPercentage }}</span>
11
+
12
+        </div>
13
+      </div>
14
+      <div class="count-inner">
15
+        <div class="item-title">移交公安数量</div>
16
+        <div class="count">
17
+          <vue-count-to :duration="2600" :end-val="totalNum.policeTotal" :start-val="0" class="card-panel-num" />
18
+        </div>
19
+        <div class="tips">
20
+          <span class="icon up"><svg-icon icon-class="top" />较昨日{{ policePercentage }}</span>
21
+        </div>
22
+      </div>
23
+      <div class="count-inner">
24
+        <div class="item-title">隐匿夹带数量</div>
25
+        <div class="count">
26
+          <vue-count-to :duration="2600" :end-val="totalNum.concealTotal" :start-val="0" class="card-panel-num" />
27
+        </div>
28
+        <div class="tips">
29
+          <span class="icon down"><svg-icon icon-class="top" />较昨日{{ concealPercentage }}</span>
30
+        </div>
31
+      </div>
32
+    </div>
33
+    <div class="dashboard-title">查获位置分布</div>
34
+    <div ref="pieChart" class="echarts" />
35
+  </div>
36
+</template>
37
+<script setup>
38
+import { onMounted, ref } from 'vue';
39
+import VueCountTo from '@/components/VueCountTo/vue-countTo.vue';
40
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
41
+import { useEcharts } from '@/hooks/chart.js';
42
+import SvgIcon from '@/components/SvgIcon/index.vue';
43
+import { getTotalSome } from "@/api/largeScreen/largeScreen.js"
44
+import { getPosition } from "@/api/largeScreen/largeScreen.js"
45
+
46
+
47
+
48
+let totalNum = ref({
49
+  total: 0,
50
+  policeTotal: 0,
51
+  concealTotal: 0,
52
+  policeTotalBefore: 0,
53
+  totalBefore: 0,
54
+  concealTotalBefore: 0,
55
+});
56
+const policePercentage = computed(() => {
57
+  if (totalNum.value.policeTotalBefore == totalNum.value.policeTotal) return '无变化'
58
+  if (totalNum.value.policeTotalBefore == 0) return '+100%'
59
+  else {
60
+    let num = (totalNum.value.policeTotal - totalNum.value.policeTotalBefore) / totalNum.value.policeTotalBefore * 100
61
+    return num > 0 ? '+' + num.toFixed(2) + '%' : num.toFixed(2) + '%'
62
+  }
63
+});
64
+const totalPercentage = computed(() => {
65
+  if (totalNum.value.totalBefore == totalNum.value.total) return '无变化'
66
+  if (totalNum.value.totalBefore == 0) return '+100%'
67
+  else {
68
+    let num = (totalNum.value.total - totalNum.value.totalBefore) / totalNum.value.totalBefore * 100
69
+    return num > 0 ? '+' + num.toFixed(2) + '%' : num.toFixed(2) + '%'
70
+  }
71
+});
72
+const concealPercentage = computed(() => {
73
+  if (totalNum.value.concealTotalBefore == totalNum.value.concealTotal) return '无变化'
74
+  if (totalNum.value.concealTotalBefore == 0) return '+100%'
75
+  else {
76
+    let num = (totalNum.value.concealTotal - totalNum.value.concealTotalBefore) / totalNum.value.concealTotalBefore * 100
77
+    return num > 0 ? '+' + num.toFixed(2) + '%' : num.toFixed(2) + '%'
78
+  }
79
+});
80
+
81
+const pieChart = ref(null);
82
+const chartOption = ref({
83
+  color: ['#5470c6', '#91cc75', '#fac858', '#ee6666'],
84
+  tooltip: {
85
+    trigger: 'item',
86
+    formatter: '{a} <br/>{b}: {c} ({d}%)'
87
+  },
88
+  series: [
89
+    {
90
+      type: 'pie',
91
+      name: '部位类别',
92
+      height: '100%',
93
+      radius: '50%',
94
+      label: {
95
+        position: 'inner',
96
+      },
97
+      emphasis: {
98
+        label: {
99
+          show: true,
100
+          fontSize: '14',
101
+          formatter: '{b}\n{c} ({d}%)'
102
+        },
103
+        itemStyle: {
104
+          shadowBlur: 10,
105
+          shadowOffsetX: 0,
106
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
107
+        }
108
+      },
109
+      itemStyle: {
110
+        borderWidth: 1, //设置border宽度
111
+        borderColor: '#fff',
112
+      },
113
+      labelLine: {
114
+        show: false,
115
+      },
116
+      data: [
117
+        { name: '行李', value: 200 },
118
+        { name: '人体', value: 80 },
119
+        { name: '随身物品', value: 30 },
120
+      ],
121
+    },
122
+    {
123
+      type: 'pie',
124
+      name: '位置详情',
125
+      height: '100%',
126
+      radius: ['50%', '80%'],
127
+      labelLayout: {
128
+        hideOverlap: false
129
+      },
130
+      label: {
131
+        position: 'inner',
132
+        rotate: 70
133
+      },
134
+      itemStyle: {
135
+        borderWidth: 1,
136
+        borderColor: '#fff',
137
+      },
138
+      data: [
139
+        { name: '托运行李', value: 100 },
140
+        { name: '携带行李', value: 100 },
141
+        { name: '体外衣物', value: 50 },
142
+        { name: '体内藏匿', value: 30 },
143
+        { name: '随身包裹', value: 20 },
144
+        { name: '其他物品', value: 10 },
145
+      ],
146
+    },
147
+  ],
148
+});
149
+
150
+const { setOption } = useEcharts(pieChart);
151
+const pollingIntervalNum = ref(null);
152
+const pollingInterval = ref(null);
153
+onMounted(() => {
154
+  pollingIntervalNum.value = setInterval(fetchDataNum(), 5000);
155
+  pollingInterval.value = setInterval(fetchData(), 5000);
156
+
157
+});
158
+function fetchDataNum() {
159
+  getTotalSome().then(res => {
160
+    totalNum.value = res.data;
161
+  }).catch(error => {
162
+    console.error(error);
163
+    clearInterval(pollingIntervalNum.value);
164
+  });
165
+  return fetchDataNum
166
+}
167
+function fetchData() {
168
+  getPosition().then(res => {
169
+    chartOption.value.series[0].data = res.data.map((el, idx) => {
170
+      if (el.checkPositionNameTwo == '无') {
171
+        return {
172
+          value: el.total,
173
+          name: el.checkPositionNameOne,
174
+        };
175
+      }
176
+    });
177
+    chartOption.value.series[1].data = res.data.map((el, idx) => {
178
+      if (el.checkPositionNameTwo != '无') {
179
+        return {
180
+          value: el.total,
181
+          name: el.checkPositionNameTwo,
182
+        };
183
+      }
184
+    });
185
+    setOption?.(chartOption.value);
186
+  }).catch(error => {
187
+    console.error(error);
188
+    clearInterval(pollingInterval.value);
189
+  });
190
+  return fetchData
191
+}
192
+
193
+</script>
194
+
195
+<style lang="less" scoped>
196
+.classification-of-seized-quantity {
197
+  display: flex;
198
+  flex-flow: column;
199
+  height: 100%;
200
+}
201
+
202
+.total {
203
+  display: flex;
204
+  justify-content: space-between;
205
+
206
+  .count-inner {
207
+    width: calc(33.33% - 8px);
208
+    padding: 20px;
209
+    border: 1px solid #363F4C;
210
+    border-radius: 8px;
211
+
212
+    &:nth-child(1) {
213
+      color: #92C4FC;
214
+    }
215
+
216
+    &:nth-child(2) {
217
+      color: #C49929;
218
+    }
219
+
220
+    &:nth-child(3) {
221
+      color: #A83A3F;
222
+    }
223
+
224
+    .item-title {
225
+      margin-bottom: 8px;
226
+    }
227
+
228
+    .tips {
229
+      color: #9499A0;
230
+      font-size: 12px;
231
+      margin-top: 16px;
232
+
233
+      .icon {
234
+        margin-right: 8px;
235
+        color: #10B981;
236
+
237
+        .svg-icon {
238
+          fill: #10B981;
239
+          transform: rotate(-90deg);
240
+        }
241
+
242
+        &.up {
243
+          color: #EF4444;
244
+
245
+          .svg-icon {
246
+            fill: #EF4444;
247
+          }
248
+        }
249
+
250
+        &.down {
251
+          .svg-icon {
252
+            transform: rotate(90deg);
253
+          }
254
+        }
255
+      }
256
+    }
257
+  }
258
+}
259
+
260
+:deep(.card-panel-num) {
261
+  margin: 8px 0;
262
+  font-size: 20px;
263
+  // color: #fbffff;
264
+}
265
+
266
+.echarts {
267
+  flex: 1;
268
+  width: 100%;
269
+}
270
+
271
+.dashboard-title {
272
+  position: relative;
273
+  display: flex;
274
+  justify-content: space-between;
275
+  align-items: center;
276
+  flex-wrap: nowrap;
277
+  flex-direction: row;
278
+  z-index: 1;
279
+  align-content: flex-start;
280
+  background: url(/src/assets/images/titlebg-4340cf1c.png);
281
+  background-size: 100% 100%;
282
+  font-weight: 400;
283
+  font-size: 14px;
284
+  color: #fbffff;
285
+  padding: 0 20px;
286
+  height: 42px;
287
+}
288
+</style>

+ 156 - 0
src/views/dataBigScreen/dashboard/DeliberateConcealment.vue

@@ -0,0 +1,156 @@
1
+<!-- ConcealScroll.vue -->
2
+<template>
3
+  <scroll-seamless :list="list" title="隐匿夹带情况">
4
+    <template #thead>
5
+      <span class="item ranking">排名</span>
6
+      <span class="item start">隐匿类别</span>
7
+      <span class="item start">隐匿物品</span>
8
+      <span class="item">隐匿数量</span>
9
+      <span class="item">隐匿部位</span>
10
+    </template>
11
+    <template #default="{ row, index }">
12
+      <span class="item ranking">
13
+        <i :class="{ first: index === 0, second: index === 1, third: index === 2 }" class="index">
14
+          {{ index + 1 }}
15
+        </i>
16
+      </span>
17
+      <span class="item start">{{ row.categoryNameTwo }}</span>
18
+      <span class="item start">{{ row.itemName }}</span>
19
+      <span class="item num">{{ row.quantity }}</span>
20
+      <span class="item">{{ row.checkPositionNameOne }}</span>
21
+    </template>
22
+  </scroll-seamless>
23
+</template>
24
+
25
+<script setup>
26
+import { ref, onMounted } from 'vue';
27
+import { getConceal } from '@/api/largeScreen/largeScreen.js';
28
+import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
29
+const list = ref([]);
30
+const pollingInterval = ref(null);
31
+onMounted(() => {
32
+  pollingInterval.value = setInterval(fetchData(), 5000);
33
+});
34
+function fetchData() {
35
+  getConceal().then((res) => {
36
+    list.value = res.data;
37
+  })
38
+    .catch(error => {
39
+      console.error(error);
40
+      clearInterval(pollingInterval.value);
41
+    });
42
+  return fetchData
43
+}
44
+
45
+
46
+</script>
47
+
48
+<style scoped>
49
+/* 表头样式保持原样 */
50
+::v-deep(.thead) {
51
+  display: flex;
52
+  align-items: center;
53
+  width: 100%;
54
+  height: 40px;
55
+  padding: 0 16px;
56
+  color: #fbffff;
57
+  font-size: 12px;
58
+  font-weight: bold;
59
+}
60
+
61
+::v-deep(.thead .item) {
62
+  width: 20%;
63
+  display: inline-flex;
64
+  align-items: center;
65
+  justify-content: center;
66
+  white-space: nowrap;
67
+}
68
+
69
+::v-deep(.thead .item.ranking) {
70
+  width: 10%;
71
+
72
+}
73
+
74
+::v-deep(.thead .item.start) {
75
+  text-align: left;
76
+  justify-content: start;
77
+}
78
+
79
+/* 滚动容器 */
80
+.scroll-box {
81
+  height: calc(100% - 40px);
82
+  /* 去掉表头高度 */
83
+  overflow: hidden;
84
+  position: relative;
85
+}
86
+
87
+.scroll-list {
88
+  display: flex;
89
+  flex-direction: column;
90
+  animation-name: scroll;
91
+  animation-timing-function: linear;
92
+  animation-iteration-count: infinite;
93
+}
94
+
95
+@keyframes scroll {
96
+  0% {
97
+    transform: translateY(0);
98
+  }
99
+
100
+  100% {
101
+    transform: translateY(-50%);
102
+  }
103
+}
104
+
105
+/* 行样式 */
106
+.list-item {
107
+  display: flex;
108
+  align-items: center;
109
+  height: 24px;
110
+  flex-shrink: 0;
111
+  color: #fbffff;
112
+  font-size: 12px;
113
+  padding: 0 16px;
114
+}
115
+
116
+.list-item .item {
117
+  width: 20%;
118
+  text-align: center;
119
+  white-space: nowrap;
120
+}
121
+
122
+.list-item .item.start {
123
+  text-align: left;
124
+}
125
+
126
+.list-item .item.ranking {
127
+  width: 10%;
128
+}
129
+
130
+.index {
131
+  display: inline-flex;
132
+  justify-content: center;
133
+  align-items: center;
134
+  width: 16px;
135
+  height: 16px;
136
+  background: #5470c6;
137
+  border-radius: 4px;
138
+  font-style: normal;
139
+}
140
+
141
+.index.first {
142
+  background: #ff3232;
143
+}
144
+
145
+.index.second {
146
+  background: #ff9800;
147
+}
148
+
149
+.index.third {
150
+  background: #ffd200;
151
+}
152
+
153
+.num {
154
+  color: #91cc75;
155
+}
156
+</style>

+ 95 - 0
src/views/dataBigScreen/dashboard/LineChart.vue

@@ -0,0 +1,95 @@
1
+<template>
2
+  <div ref="lineChart" class="echarts" />
3
+</template>
4
+
5
+<script setup>
6
+import { onMounted, ref } from 'vue';
7
+import * as echarts from 'echarts';
8
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
9
+import { useEcharts } from '@/hooks/chart.js';
10
+import { getTimeSpan } from "@/api/largeScreen/largeScreen.js"
11
+
12
+
13
+const lineChart = ref(null);
14
+const chartOption = ref({
15
+  grid: {
16
+    left: '30',
17
+    top: '30',
18
+    right: '30',
19
+    bottom: '70',
20
+    containLabel: true,
21
+  },
22
+  xAxis: {
23
+    data: [],
24
+    axisLine: {
25
+      lineStyle: {
26
+        color: '#fbffff',
27
+      },
28
+    },
29
+    axisLabel: {
30
+      interval: 3,
31
+    },
32
+  },
33
+  yAxis: {
34
+    type: 'value',
35
+    axisLine: {
36
+      lineStyle: {
37
+        color: '#fbffff',
38
+      },
39
+    },
40
+  },
41
+  series: [
42
+    {
43
+      type: 'line',
44
+      symbol: 'none',
45
+      smooth: true,
46
+      itemStyle: {
47
+        color: '#fc8452',
48
+        borderDashOffset: 20,
49
+      },
50
+      endLabel: {
51
+        show: false,
52
+      },
53
+      areaStyle: {
54
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
55
+          {
56
+            offset: 0,
57
+            color: 'rgb(255, 70, 131)',
58
+          },
59
+          {
60
+            offset: 1,
61
+            color: 'rgb(255, 158, 68)',
62
+          },
63
+        ]),
64
+      },
65
+      data: [],
66
+    },
67
+  ],
68
+});
69
+const { setOption } = useEcharts(lineChart);
70
+const pollingInterval = ref(null);
71
+
72
+onMounted(() => {
73
+  const date = new Date('2023-01-01');
74
+  pollingInterval.value = setInterval(fetchData(), 5000);
75
+
76
+});
77
+function fetchData() {
78
+  getTimeSpan().then(res => {
79
+    chartOption.value.series[0].data = res.data.map(el => el.total);
80
+    chartOption.value.xAxis.data = res.data.map(el => el.hourOfDay + ':00');
81
+    setOption?.(chartOption.value);
82
+  }).catch(error => {
83
+    console.error(error);
84
+    clearInterval(pollingInterval.value);
85
+  });
86
+  return fetchData
87
+}
88
+</script>
89
+
90
+<style scoped>
91
+.echarts {
92
+  height: 100%;
93
+  width: 100%;
94
+}
95
+</style>

+ 196 - 0
src/views/dataBigScreen/dashboard/MajorContrabandCases.vue

@@ -0,0 +1,196 @@
1
+<template>
2
+  <scroll-seamless :list="list" title="移交公安情况">
3
+    <!-- 右侧 Tab -->
4
+    <template #right-menu>
5
+      <div class="dashboard-right-menu">
6
+        <span :class="{ active: currentType === 'ranking_of_seizures_by_department' }"
7
+          @click="handleClick('ranking_of_seizures_by_department')">今日</span>
8
+        <span :class="{ active: currentType === 'ranking_of_seizures_by_team' }"
9
+          @click="handleClick('ranking_of_seizures_by_team')">本周</span>
10
+        <span :class="{ active: currentType === 'ranking_of_seizures_by_individual' }"
11
+          @click="handleClick('ranking_of_seizures_by_individual')">本月</span>
12
+      </div>
13
+    </template>
14
+    <template #thead>
15
+      <span class="item">违禁品类型</span>
16
+      <span class="item">查获数量</span>
17
+      <span class="item">查获地点</span>
18
+      <span class="item">查获人</span>
19
+    </template>
20
+    <template #default="{ row, index }">
21
+      <span class="item icon">
22
+        <span class="icon">
23
+          <svg-icon :icon-class="row.svg" />
24
+        </span>
25
+        {{ row.categoryNameTwo }}
26
+      </span>
27
+      <span class="item">{{ row.quantity }}</span>
28
+      <span class="item">{{ row.regionalName }}</span>
29
+      <span class="item">{{ row.nackName }}</span>
30
+
31
+      <!-- <span class="item detail">详情</span> -->
32
+    </template>
33
+  </scroll-seamless>
34
+</template>
35
+
36
+<script setup>
37
+import { ref, onMounted } from 'vue';
38
+import { getPolice } from '@/api/largeScreen/largeScreen.js';
39
+import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
40
+
41
+function today() {
42
+  return new Date().toISOString().slice(0, 10);
43
+}
44
+function weekRange() {
45
+  const d = new Date();
46
+  const day = d.getDay() || 7; // 周日返回 7
47
+  const start = new Date(d);
48
+  start.setDate(d.getDate() - day + 1);
49
+  const end = new Date(start);
50
+  end.setDate(start.getDate() + 6);
51
+  return {
52
+    startDate: start.toISOString().slice(0, 10),
53
+    endDate: end.toISOString().slice(0, 10)
54
+  };
55
+}
56
+function monthRange() {
57
+  const d = new Date();
58
+  const start = new Date(d.getFullYear(), d.getMonth(), 1);
59
+  const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
60
+  return {
61
+    startDate: start.toISOString().slice(0, 10),
62
+    endDate: end.toISOString().slice(0, 10)
63
+  };
64
+}
65
+
66
+/* -------------- 组件逻辑 -------------- */
67
+const list = ref([]);
68
+const currentType = ref('ranking_of_seizures_by_department');
69
+const svg = ['du', 'huo', 'baozha'];
70
+const pollingInterval = ref(null);
71
+function loadData() {
72
+  let params = {};
73
+  switch (currentType.value) {
74
+    case 'ranking_of_seizures_by_department':
75
+      params = { specifiedDate: today() };
76
+      break;
77
+    case 'ranking_of_seizures_by_team':
78
+      params = weekRange();
79
+      break;
80
+    case 'ranking_of_seizures_by_individual':
81
+      params = monthRange();
82
+      break;
83
+  }
84
+  getPolice(params).then(res => {
85
+    const total = res.data.reduce((pre, cur) => pre + cur.quantity, 0);
86
+    list.value = res.data.map(item => ({
87
+      ...item,
88
+      svg: svg[Math.floor(Math.random() * svg.length)],
89
+      percentage: Math.floor((item.quantity / total) * 100),
90
+    }));
91
+  }).catch(error => {
92
+    console.error(error);
93
+    clearInterval(pollingInterval.value);
94
+  });
95
+  return loadData
96
+}
97
+
98
+const handleClick = (type) => {
99
+  currentType.value = type;
100
+  loadData();
101
+};
102
+
103
+onMounted(() => {
104
+  pollingInterval.value = setInterval(loadData(), 5000);
105
+}
106
+);
107
+</script>
108
+
109
+<style scoped>
110
+/* 字段标题 */
111
+.thead {
112
+  display: flex;
113
+  align-items: center;
114
+  width: 100%;
115
+  height: 40px;
116
+  padding: 0 16px;
117
+  color: #fbffff;
118
+  font-size: 12px;
119
+  font-weight: bold;
120
+}
121
+
122
+.thead .item {
123
+  flex: 1;
124
+  text-align: center;
125
+}
126
+
127
+/* 滚动容器 */
128
+.scroll-box {
129
+  width: 100%;
130
+  /* height: calc(100% - 40px); */
131
+  margin: 0 auto;
132
+  overflow: hidden;
133
+  color: #fbffff;
134
+  font-size: 12px;
135
+  padding: 0 16px;
136
+}
137
+
138
+.scroll-list {
139
+  height: 100%;
140
+  display: flex;
141
+  flex-direction: column;
142
+  animation-name: scroll;
143
+  animation-timing-function: linear;
144
+  animation-iteration-count: infinite;
145
+}
146
+
147
+@keyframes scroll {
148
+  0% {
149
+    transform: translateY(0);
150
+  }
151
+
152
+  100% {
153
+    transform: translateY(-50%);
154
+  }
155
+}
156
+
157
+/* 行样式 */
158
+.list-item {
159
+  display: flex;
160
+  align-items: center;
161
+  height: 36px;
162
+  flex-shrink: 0;
163
+}
164
+
165
+.list-item .item {
166
+  flex: 1;
167
+  text-align: center;
168
+  white-space: nowrap;
169
+}
170
+
171
+.icon {
172
+  display: inline-flex;
173
+  align-items: center;
174
+  justify-content: center;
175
+}
176
+
177
+.detail {
178
+  color: #8abaef;
179
+}
180
+
181
+.dashboard-right-menu span {
182
+  display: inline-flex;
183
+  align-items: center;
184
+  justify-content: center;
185
+  font-size: 12px;
186
+  background: #11134b;
187
+  padding: 4px 8px;
188
+  margin: -4px 0;
189
+  border-radius: 4px;
190
+  color: #fbffff;
191
+}
192
+
193
+.dashboard-right-menu span.active {
194
+  background: #171b75;
195
+}
196
+</style>

+ 86 - 0
src/views/dataBigScreen/dashboard/PieChart.vue

@@ -0,0 +1,86 @@
1
+<template>
2
+  <div ref="pieChart" class="echarts" />
3
+</template>
4
+
5
+<script setup>
6
+import { onMounted, ref } from 'vue';
7
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
8
+import { useEcharts } from '@/hooks/chart.js';
9
+import { getCategory } from "@/api/largeScreen/largeScreen.js"
10
+
11
+
12
+const pieChart = ref(null);
13
+const color = [
14
+  '#5470c6',
15
+  '#91cc75',
16
+  '#fac858',
17
+  '#ee6666',
18
+  '#fc8452',
19
+];
20
+const chartOption = ref({
21
+  color,
22
+  series: [
23
+    {
24
+      type: 'pie',
25
+      height: '80%',
26
+      label: {
27
+        normal: {
28
+          formatter: '{bg|}{title|{b}}\n{text|{@value}%}',
29
+          rich: {
30
+            bg: {
31
+              width: 10,
32
+              height: 10,
33
+              borderRadius: 50,
34
+            },
35
+            title: {
36
+              color: '#fbffff',
37
+              lineHeight: 16,
38
+              padding: [0, 0, 0, 4],
39
+            },
40
+            text: {
41
+              color: '#fbffff',
42
+              opacity: 0.5,
43
+              lineHeight: 16,
44
+              padding: [0, 0, 0, 14],
45
+            },
46
+          },
47
+          textStyle: {
48
+            color: '#fbffff',
49
+            fontSize: 12,
50
+          },
51
+        },
52
+      },
53
+      data: [],
54
+    },
55
+  ],
56
+});
57
+const { setOption } = useEcharts(pieChart);
58
+const pollingInterval = ref(null);
59
+onMounted(() => {
60
+  pollingInterval.value = setInterval(fetchData(), 5000);
61
+});
62
+function fetchData() {
63
+  getCategory().then(res => {
64
+    chartOption.value.series[0].data = res.data.map((el, idx) => {
65
+      return {
66
+        value: el.scale,
67
+        name: el.name,
68
+        label: { rich: { bg: { backgroundColor: color[idx % color.length] } } },
69
+      };
70
+    });
71
+    setOption?.(chartOption.value);
72
+  }).catch(error => {
73
+    console.error(error);
74
+    clearInterval(pollingInterval.value);
75
+  });
76
+  return fetchData
77
+}
78
+</script>
79
+
80
+<style scoped>
81
+.echarts {
82
+  height: 100%;
83
+  width: 100%;
84
+  margin-top: 12px;
85
+}
86
+</style>

+ 152 - 0
src/views/dataBigScreen/dashboard/PieRadiusChart.vue

@@ -0,0 +1,152 @@
1
+<template>
2
+  <div class="pie-radius-chart-content">
3
+    <div ref="pieRadiusChart" class="echarts item" />
4
+
5
+    <div class="item">
6
+      <div v-for="(item, index) in list" class="item-line">
7
+        <span :style="{ background: color[index] }" class="color"></span>
8
+        <div class="text">
9
+          <span>{{ item.name }}</span>
10
+          <span>{{ item.pie }}</span>
11
+          <span>{{ item.value }}</span>
12
+        </div>
13
+      </div>
14
+    </div>
15
+  </div>
16
+
17
+</template>
18
+
19
+<script setup>
20
+import { computed, onMounted, ref } from 'vue';
21
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
22
+import { useEcharts } from '@/hooks/chart.js';
23
+import { getPost } from "@/api/largeScreen/largeScreen.js"
24
+
25
+
26
+const color = [
27
+  '#5470c6',
28
+  '#91cc75',
29
+  '#fac858',
30
+  '#ee6666',
31
+  '#73c0de',
32
+  '#3ba272',
33
+  '#fc8452',
34
+];
35
+const pieRadiusChart = ref(null);
36
+const chartOption = ref({
37
+  tooltip: {
38
+    trigger: 'item',
39
+  },
40
+  color,
41
+  graphic: {
42
+    type: 'text',
43
+    left: 'center',
44
+    top: 'center',
45
+    style: {
46
+      text: '总数',
47
+      textAlign: 'center',
48
+      fill: '#fbffff',
49
+      width: 30,
50
+      height: 30,
51
+      fontSize: 14,
52
+    },
53
+  },
54
+  series: [
55
+    {
56
+      name: 'Access From',
57
+      type: 'pie',
58
+      radius: ['60%', '80%'],
59
+      avoidLabelOverlap: false,
60
+      label: {
61
+        show: false,
62
+        position: 'center',
63
+      },
64
+      emphasis: {
65
+        label: {
66
+          show: false,
67
+          fontSize: 40,
68
+          fontWeight: 'bold',
69
+        },
70
+      },
71
+      labelLine: {
72
+        show: false,
73
+      },
74
+      data: [],
75
+    },
76
+  ],
77
+});
78
+const { setOption } = useEcharts(pieRadiusChart);
79
+const pollingInterval = ref(null);
80
+const list = computed(() => {
81
+  return chartOption.value.series[0].data;
82
+});
83
+
84
+
85
+onMounted(() => {
86
+  pollingInterval.value = setInterval(fetchData(), 5000);
87
+});
88
+function fetchData() {
89
+  getPost().then(res => {
90
+    chartOption.value.series[0].data = res.data.map((el, idx) => {
91
+      return {
92
+        value: el.total,
93
+        name: el.name,
94
+      };
95
+    });
96
+    let dataNum = res.data.reduce((pre, cur) => pre + cur.total, 0);
97
+    chartOption.value.graphic.style.text = '总数量' + '\n\n' + dataNum;
98
+    setOption?.(chartOption.value);
99
+  }).catch(error => {
100
+    console.error(error);
101
+    clearInterval(pollingInterval.value);
102
+  });
103
+  return fetchData
104
+}
105
+</script>
106
+
107
+<style lang="less" scoped>
108
+.echarts {
109
+  height: 100%;
110
+  width: 100%;
111
+}
112
+
113
+.pie-radius-chart-content {
114
+  display: flex;
115
+  height: calc(100% - 40px);
116
+
117
+  .item {
118
+    width: 50%;
119
+    padding: 20px 0;
120
+    display: flex;
121
+    justify-content: space-between;
122
+    flex-flow: column;
123
+  }
124
+
125
+  .item-line {
126
+    display: flex;
127
+    align-items: center;
128
+    color: #fbffff;
129
+    font-size: 12px;
130
+    width: 100%;
131
+
132
+    .color {
133
+      display: inline-flex;
134
+      width: 10px;
135
+      height: 10px;
136
+      border-radius: 5px;
137
+      margin-right: 4px;
138
+    }
139
+
140
+    .text {
141
+      flex: 1;
142
+      display: flex;
143
+      justify-content: space-between;
144
+
145
+      span {
146
+        display: inline-flex;
147
+        width: 33.33%;
148
+      }
149
+    }
150
+  }
151
+}
152
+</style>

+ 227 - 0
src/views/dataBigScreen/dashboard/SeizedRanking.vue

@@ -0,0 +1,227 @@
1
+<template>
2
+  <scroll-seamless :list="list" title="查获排名">
3
+    <template #right-menu>
4
+      <div class="dashboard-right-menu">
5
+        <span :class="{ active: currentType === 1 }" @click="handleClick(1)">
6
+          按科室
7
+        </span>
8
+        <span :class="{ active: currentType === 2 }" @click="handleClick(2)">
9
+          按班组
10
+        </span>
11
+        <span :class="{ active: currentType === 3 }" @click="handleClick(3)">
12
+          按个人
13
+        </span>
14
+      </div>
15
+    </template>
16
+    <template #default="{ row, index }">
17
+      <span class="item ranking">
18
+        <i :class="{ first: index === 0, second: index === 1, third: index === 2 }" class="index">{{ index + 1
19
+        }}</i>
20
+      </span>
21
+      <span class="item title" :class="{ 'hide-left': currentType === 2 }" :title="row.name">{{ row.name }}</span>
22
+      <el-progress :percentage="row.percentagePer" :show-text="false" :stroke-width="10" :text-inside="true"
23
+        color="#409eff" />
24
+
25
+      <span class="num">{{ row.total }}</span>
26
+      <span class="pie">占比{{ row.scale }}%</span>
27
+    </template>
28
+  </scroll-seamless>
29
+</template>
30
+
31
+<script setup>
32
+import { ref, watchEffect } from 'vue';
33
+import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
34
+import { getRank } from "@/api/largeScreen/largeScreen.js"
35
+import ScrollSeamless from '@/views/dataBigScreen/dashboard/components/ScrollSeamless.vue';
36
+
37
+const currentType = ref(1);
38
+let list = ref([]);
39
+const pollingInterval = ref(null);
40
+watchEffect(() => {
41
+  getRank({ 'type': currentType.value }).then(res => {
42
+    const newList = res.data.map(el => {
43
+      return {
44
+        ...el,
45
+        percentagePer: el.scale,
46
+      };
47
+    });
48
+    list.value = newList
49
+
50
+  });
51
+});
52
+const handleClick = (type) => {
53
+  currentType.value = type;
54
+  console.log("newList", list.value)
55
+
56
+};
57
+onMounted(() => {
58
+  pollingInterval.value = setInterval(fetchData(), 5000);
59
+});
60
+function fetchData() {
61
+  getRank({ 'type': currentType.value }).then(res => {
62
+    const newList = res.data.map(el => {
63
+      return {
64
+        ...el,
65
+        percentagePer: el.scale,
66
+      };
67
+    });
68
+    list.value = newList
69
+  }).catch(error => {
70
+    clearInterval(pollingInterval.value);
71
+  });
72
+  return fetchData
73
+}
74
+</script>
75
+
76
+<style lang="less" scoped>
77
+.item {
78
+  display: inline-flex;
79
+  align-items: center;
80
+  justify-content: center;
81
+
82
+  &.ranking {
83
+    overflow: hidden;
84
+    text-overflow: ellipsis;
85
+    white-space: nowrap;
86
+    margin-right: 8px;
87
+    flex-shrink: 0;
88
+  }
89
+
90
+ &.title {
91
+    display: inline-flex;
92
+    justify-content: flex-start;
93
+    min-width: 80px;
94
+    max-width: 150px;
95
+    overflow: hidden;
96
+    margin-right: 12px;
97
+    white-space: nowrap;
98
+    text-align: left;
99
+    position: relative;
100
+  }
101
+
102
+  // 按班组时隐藏左边内容的样式
103
+  &.title.hide-left {
104
+    direction: rtl;
105
+    text-align: left;
106
+  }
107
+
108
+
109
+
110
+  &.title.hide-left span {
111
+    direction: ltr;
112
+    display: inline-block;
113
+    width: 100%;
114
+    overflow: hidden;
115
+    text-overflow: ellipsis;
116
+  }
117
+
118
+
119
+  .index {
120
+    display: inline-flex;
121
+    justify-content: center;
122
+    align-items: center;
123
+    width: 16px;
124
+    height: 16px;
125
+    background: #5470c6;
126
+    border-radius: 4px;
127
+    font-style: normal;
128
+
129
+    &.first {
130
+      background: #ff3232;
131
+    }
132
+
133
+    &.second {
134
+      background: #ff9800;
135
+    }
136
+
137
+    &.third {
138
+      background: #ffd200;
139
+    }
140
+  }
141
+}
142
+
143
+
144
+.num {
145
+  width: 40px;
146
+  margin-left: 12px;
147
+  flex-shrink: 0;
148
+}
149
+
150
+:deep(.el-progress) {
151
+  flex: 1;
152
+  min-width: 60px;
153
+  flex-shrink: 0;
154
+}
155
+
156
+:deep(.el-progress-bar__outer) {
157
+  background-color: #164B7F;
158
+}
159
+
160
+
161
+.pie {
162
+  width: 80px;
163
+  margin-left: 12px;
164
+  color: #73c0de;
165
+  white-space: nowrap;
166
+  flex-shrink: 0;
167
+}
168
+
169
+
170
+.list-item {
171
+  display: flex;
172
+  align-items: center;
173
+  color: #fbffff;
174
+  padding: 0px 16px;
175
+  font-size: 12px;
176
+  margin-top: 12px;
177
+  /* 可选 */
178
+}
179
+
180
+
181
+.item {
182
+  text-align: center;
183
+  white-space: nowrap;
184
+}
185
+
186
+.dashboard-right-menu span {
187
+  display: inline-flex;
188
+  align-items: center;
189
+  justify-content: center;
190
+  font-size: 12px;
191
+  background: #11134b;
192
+  padding: 4px 8px;
193
+  margin: -4px 0;
194
+  border-radius: 4px;
195
+  color: #fbffff;
196
+}
197
+
198
+.dashboard-right-menu span.active {
199
+  background: #171b75;
200
+}
201
+
202
+/* 滚动容器 */
203
+.scroll-box {
204
+  height: 100%;
205
+  overflow: hidden;
206
+  position: relative;
207
+  margin-top: 12px;
208
+}
209
+
210
+.scroll-list {
211
+  display: flex;
212
+  flex-direction: column;
213
+  animation-name: scroll;
214
+  animation-timing-function: linear;
215
+  animation-iteration-count: infinite;
216
+}
217
+
218
+@keyframes scroll {
219
+  0% {
220
+    transform: translateY(0);
221
+  }
222
+
223
+  100% {
224
+    transform: translateY(-50%);
225
+  }
226
+}
227
+</style>

+ 91 - 0
src/views/dataBigScreen/dashboard/TopTitle.vue

@@ -0,0 +1,91 @@
1
+<template>
2
+  <div class="pageTop wow fadeInDown">
3
+    <div class="pageTopbg"></div>
4
+    <div class="zhuangshi">
5
+      <div class="leftZhuangshi"></div>
6
+      <div class="rightZhuangshi"></div>
7
+    </div>
8
+    <div class="left"></div>
9
+    <div class="title">
10
+      <span>{{ name }}</span>
11
+    </div>
12
+    <div class="right">
13
+    </div>
14
+  </div>
15
+</template>
16
+
17
+<script name="TopTitle" setup>
18
+  const props = defineProps({
19
+    name: {
20
+      name: String,
21
+      default: '',
22
+    },
23
+  });
24
+</script>
25
+
26
+<style lang="less" scoped>
27
+  .pageTop {
28
+    position: absolute;
29
+    width: 100%;
30
+    height: 260px;
31
+    flex-shrink: 0;
32
+    display: flex;
33
+    justify-content: center;
34
+    align-items: flex-start;
35
+    flex-wrap: nowrap;
36
+    flex-direction: row;
37
+    align-content: flex-start;
38
+    pointer-events: none;
39
+    background: url('../../../assets/images/topbg-2521ad78.png') center top no-repeat;
40
+
41
+    .zhuangshi {
42
+      position: absolute;
43
+      top: 10px;
44
+      width: 96%;
45
+      left: 2%;
46
+      display: flex;
47
+      justify-content: space-between;
48
+      align-items: center;
49
+      flex-wrap: nowrap;
50
+      flex-direction: row;
51
+      align-content: flex-start;
52
+      z-index: 100;
53
+
54
+      .leftZhuangshi, .rightZhuangshi {
55
+        width: 192px;
56
+        background: url('../../../assets/images/下载.png') left center no-repeat;
57
+        height: 29px;
58
+        position: relative;
59
+      }
60
+
61
+      .rightZhuangshi {
62
+        position: relative;
63
+        transform: scaleX(-1);
64
+      }
65
+    }
66
+
67
+    .title {
68
+      position: relative;
69
+      width: 40%;
70
+      display: flex;
71
+      flex-shrink: 0;
72
+      justify-content: center;
73
+      align-items: flex-start;
74
+      flex-wrap: nowrap;
75
+      flex-direction: row;
76
+      align-content: flex-start;
77
+
78
+      span {
79
+        font-size: 36px;
80
+        font-weight: normal;
81
+        color: #FFFFFF;
82
+        margin-top: 15px;
83
+        letter-spacing: 2px;
84
+        background: linear-gradient(0deg, rgba(10, 177, 255, 1) 0%, rgba(255, 255, 255, 1) 50.2685546875%, rgba(255, 255, 255, 1) 83.7890625%, rgba(10, 177, 255, 1) 100%);
85
+        -webkit-background-clip: text;
86
+        -webkit-text-fill-color: transparent;
87
+      }
88
+    }
89
+
90
+  }
91
+</style>

+ 97 - 0
src/views/dataBigScreen/dashboard/components/DashboardContainer.vue

@@ -0,0 +1,97 @@
1
+<template>
2
+  <div :class="{ none: !$slots.default}" class="dashboard-container">
3
+    <slot></slot>
4
+    <div v-if="!props.hide" class="wow home-main">
5
+      <div class="home-main-left wow slideInLeft">
6
+        <slot name="left"></slot>
7
+      </div>
8
+      <div class="home-main-center wow bounceIn">
9
+        <slot name="center"></slot>
10
+      </div>
11
+      <div class="home-main-right wow slideInRight">
12
+        <slot name="right"></slot>
13
+      </div>
14
+    </div>
15
+  </div>
16
+</template>
17
+
18
+<script setup>
19
+  import { onMounted } from 'vue';
20
+  import WOW from 'wow.js';
21
+
22
+  const props = defineProps({
23
+    hide: {
24
+      type: Boolean,
25
+      default: false,
26
+    },
27
+  });
28
+
29
+  onMounted(() => {
30
+    const wow = new WOW();
31
+    wow.init();
32
+  });
33
+</script>
34
+
35
+<style lang="scss" scoped>
36
+  .dashboard-container {
37
+    position: relative;
38
+    width: 100%;
39
+    min-height: 100vh;
40
+    background: url('../../../../assets/images/bg-99b6904c.png');
41
+    background-size: 100% 100%;
42
+
43
+    &.none {
44
+      .home-main {
45
+        padding-top: 20px;
46
+      }
47
+    }
48
+
49
+    .home-main {
50
+      position: relative;
51
+      width: calc(100% - 20px);
52
+      padding: 100px 0 20px;
53
+      margin-left: 20px;
54
+      height: 100vh;
55
+      display: flex;
56
+      z-index: 1;
57
+      justify-content: space-between;
58
+      align-items: flex-start;
59
+      flex-wrap: nowrap;
60
+      flex-direction: row;
61
+      pointer-events: none;
62
+      overflow: hidden;
63
+
64
+      .home-main-left, .home-main-right {
65
+        width: calc(30% + -0px);
66
+        position: relative;
67
+        height: calc(100% + -0px);
68
+        display: flex;
69
+        justify-content: space-between;
70
+        align-items: center;
71
+        flex-wrap: nowrap;
72
+        flex-direction: column;
73
+        align-content: flex-start;
74
+        z-index: 2;
75
+        pointer-events: initial;
76
+      }
77
+
78
+      .home-main-left-inner, .home-main-center, .home-main-right-inner {
79
+        width: calc(100% + -0px);
80
+        height: 100%;
81
+        position: relative;
82
+        display: flex;
83
+        justify-content: space-between;
84
+        align-items: center;
85
+        flex-wrap: nowrap;
86
+        flex-direction: column;
87
+        align-content: flex-start;
88
+        z-index: 2;
89
+        pointer-events: initial;
90
+      }
91
+
92
+      .home-main-center {
93
+        width: calc(40% - 20px);
94
+      }
95
+    }
96
+  }
97
+</style>

+ 70 - 0
src/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue

@@ -0,0 +1,70 @@
1
+<template>
2
+  <div style="height: 100%; overflow: hidden">
3
+    <div class="scroll-seamless-header">
4
+      <span class="dashboard-title">{{ title }}</span>
5
+      <div class="right-menu">
6
+        <slot name="right-menu"></slot>
7
+      </div>
8
+    </div>
9
+
10
+    <div class="dashboard-chart-container dashboard-bg">
11
+      <slot></slot>
12
+    </div>
13
+  </div>
14
+</template>
15
+
16
+<script setup>
17
+const props = defineProps({
18
+  title: {
19
+    type: String,
20
+    default: '',
21
+  },
22
+  list: {
23
+    type: Array,
24
+    default: () => [],
25
+  },
26
+});
27
+</script>
28
+
29
+<style lang="less" scoped>
30
+.scroll-seamless-header {
31
+  width: 100%;
32
+  height: 42px;
33
+  display: flex;
34
+  justify-content: space-between;
35
+  align-items: center;
36
+
37
+  .right-menu {
38
+    margin-right: 20px;
39
+    color: #fff;
40
+
41
+  }
42
+
43
+}
44
+
45
+.dashboard-title {
46
+  position: relative;
47
+  display: flex;
48
+  justify-content: space-between;
49
+  align-items: center;
50
+  flex-wrap: nowrap;
51
+  flex-direction: row;
52
+  z-index: 1;
53
+  align-content: flex-start;
54
+  background: url(/src/assets/images/titlebg-4340cf1c.png);
55
+  background-size: 100% 100%;
56
+  font-weight: 400;
57
+  font-size: 14px;
58
+  color: #fbffff;
59
+  padding: 0 20px;
60
+  height: 42px;
61
+}
62
+
63
+.dashboard-bg {
64
+  background: linear-gradient(to bottom, #08617500, #13a2d6 200%);
65
+}
66
+
67
+.dashboard-chart-container {
68
+  height: calc(100% - 42px);
69
+}
70
+</style>

+ 73 - 0
src/views/dataBigScreen/dashboard/components/ScrollSeamless.vue

@@ -0,0 +1,73 @@
1
+<template>
2
+  <dashboard-item-container :title="title">
3
+    <template #right-menu>
4
+      <slot name="right-menu"></slot>
5
+    </template>
6
+    <div v-if="$slots.thead" class="thead">
7
+      <slot name="thead"></slot>
8
+    </div>
9
+    <vue3-scroll-seamless :class="{ none: !$slots.thead }" :classOptions="classOptions" :dataList="props.list"
10
+      class="scroll-wrap">
11
+      <div v-for="(row, index) of props.list" :key="index" class="list-item">
12
+        <slot v-bind="{ row, index }"></slot>
13
+      </div>
14
+    </vue3-scroll-seamless>
15
+  </dashboard-item-container>
16
+</template>
17
+
18
+<script setup>
19
+import { vue3ScrollSeamless } from "vue3-scroll-seamless";
20
+import DashboardItemContainer from '@/views/dataBigScreen/dashboard/components/DashboardItemContainer.vue';
21
+
22
+const props = defineProps({
23
+  title: {
24
+    type: String,
25
+    default: '',
26
+  },
27
+  list: {
28
+    type: Array,
29
+    default: () => [],
30
+  },
31
+});
32
+const classOptions = {
33
+  limitMoveNum: 5,
34
+  step: 1
35
+};
36
+</script>
37
+
38
+<style lang="less" scoped>
39
+.thead {
40
+  display: flex;
41
+  align-items: center;
42
+  width: 100%;
43
+  height: 40px;
44
+  padding: 0 16px;
45
+  color: #fbffff;
46
+  font-size: 12px;
47
+  font-weight: bold;
48
+}
49
+
50
+.scroll-wrap {
51
+  width: 100%;
52
+  // height: calc(100% - 82px);
53
+  margin: 0 auto;
54
+  overflow: hidden;
55
+  color: #fbffff;
56
+  font-size: 12px;
57
+  padding: 0 16px;
58
+
59
+  &.none {
60
+    // height: calc(100% - 52px);
61
+    margin-top: 12px;
62
+  }
63
+
64
+
65
+  .list-item {
66
+    display: flex;
67
+    align-items: center;
68
+    color: #fbffff;
69
+    padding: 4px 0;
70
+    font-size: 12px;
71
+  }
72
+}
73
+</style>

+ 86 - 0
src/views/dataBigScreen/dashboard/index.vue

@@ -0,0 +1,86 @@
1
+<template>
2
+  <dashboard-container>
3
+    <top-title class="wow slideInDown" name="智慧大屏监控中心"></top-title>
4
+    <template #left>
5
+      <div class="item">
6
+        <!-- 重大违禁品情况 -->
7
+        <major-contraband-cases />
8
+      </div>
9
+      <div class="item">
10
+        <!-- 故意隐匿情况 -->
11
+        <deliberate-concealment />
12
+      </div>
13
+      <div class="item">
14
+        <div class="dashboard-title">查获类别分布</div>
15
+        <pie-chart />
16
+      </div>
17
+    </template>
18
+    <template #center>
19
+      <div class="item item-center">
20
+        <classification-of-seized-quantity />
21
+      </div>
22
+      <div class="item">
23
+        <!-- 查获排名 -->
24
+        <seized-ranking />
25
+      </div>
26
+    </template>
27
+    <template #right>
28
+      <div class="item">
29
+        <!-- 查获通道分布 -->
30
+        <channel-distribution />
31
+      </div>
32
+      <div class="item">
33
+        <div class="dashboard-title">查获时段分布</div>
34
+        <line-chart />
35
+      </div>
36
+      <div class="item">
37
+        <div class="dashboard-title">查获岗位分布</div>
38
+        <pie-radius-chart />
39
+      </div>
40
+    </template>
41
+  </dashboard-container>
42
+</template>
43
+
44
+<script setup>
45
+import TopTitle from './TopTitle.vue';
46
+import PieChart from './PieChart.vue';
47
+import PieRadiusChart from './PieRadiusChart.vue';
48
+import LineChart from './LineChart.vue';
49
+import MajorContrabandCases from './MajorContrabandCases.vue';
50
+import SeizedRanking from './SeizedRanking.vue';
51
+import DeliberateConcealment from './DeliberateConcealment.vue';
52
+import ChannelDistribution from './ChannelDistribution.vue';
53
+import ClassificationOfSeizedQuantity from '@/views/dataBigScreen/dashboard/ClassificationOfSeizedQuantity.vue';
54
+import DashboardContainer from '@/views/dataBigScreen/dashboard/components/DashboardContainer.vue';
55
+</script>
56
+
57
+<style lang="scss" scoped>
58
+.item {
59
+  width: calc(100% + -0px);
60
+  height: calc(33.33% - 8px);
61
+  position: relative;
62
+  background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
63
+
64
+  &.item-center {
65
+    height: calc(66.66% - 4px);
66
+  }
67
+}
68
+
69
+.dashboard-title {
70
+  position: relative;
71
+  display: flex;
72
+  justify-content: space-between;
73
+  align-items: center;
74
+  flex-wrap: nowrap;
75
+  flex-direction: row;
76
+  z-index: 1;
77
+  align-content: flex-start;
78
+  background: url(/src/assets/images/titlebg-4340cf1c.png);
79
+  background-size: 100% 100%;
80
+  font-weight: 400;
81
+  font-size: 14px;
82
+  color: #fbffff;
83
+  padding: 0 20px;
84
+  height: 42px;
85
+}
86
+</style>

+ 149 - 0
src/views/dataBigScreen/examQuestionStatistics/components/dataDisplay.vue

@@ -0,0 +1,149 @@
1
+<template>
2
+	<view class="data_display">
3
+		<view class="top">
4
+			<view>{{ props.data.title }}</view>
5
+			<img src="" alt="" />
6
+		</view>
7
+		<view class="bottom">
8
+			<view class="left">
9
+				<view class="left_num">{{ props.data.current_month }}</view>
10
+				<view class="left_compare">
11
+					<view :class="props.data.status ? 'left_compare_green' : 'left_compare_red'">
12
+						<img :src="props.data.status ? '/src/assets/icons/shang.png' : '/src/assets/icons/xia.png'" alt="" />
13
+						<view>{{ props.data.percentage }}</view>
14
+					</view>
15
+					<view>&nbsp;vs上月</view>
16
+				</view>
17
+			</view>
18
+			<view ref="rightChart" class="right"></view>
19
+		</view>
20
+	</view>
21
+</template>
22
+
23
+<script setup lang="ts">
24
+	import { onMounted, onUnmounted, ref } from 'vue';
25
+	import * as echarts from 'echarts';
26
+	const props = defineProps({
27
+	  data: { type: Object, default: {} },
28
+	});
29
+	
30
+	const chart = ref(null);
31
+	const rightChart = ref(null);
32
+	const chartOption = ref({
33
+		series: [
34
+			{
35
+				name: 'pie',
36
+				type: 'pie',
37
+				radius: ['40%', '70%'],
38
+				avoidLabelOverlap: false,
39
+				label: {
40
+					show: false,
41
+					position: 'center'
42
+				},
43
+				data: [
44
+					{ value: 800, name: 'Search Engine' },
45
+					{ value: 200, name: 'Direct' }
46
+				],
47
+				color: ['#165DFF', '#ccc'] // 直接在这里指定颜色
48
+			}
49
+		]
50
+	});
51
+	
52
+	const initChart = () => {
53
+		if (props.data.title == '参考人数') {
54
+			chart.value = echarts.init(rightChart.value, 'macarons');
55
+			chart.value.setOption(chartOption.value);
56
+		} else if (props.data.title == '平均成绩') {
57
+			chartOption.value.series[0].color = ['#00CED1', '#ccc'];
58
+			chart.value = echarts.init(rightChart.value, 'macarons');
59
+			chart.value.setOption(chartOption.value);
60
+		} else if (props.data.title == '合格率') {
61
+			chartOption.value.series[0].color = ['#00B42A', '#ccc'];
62
+			chart.value = echarts.init(rightChart.value, 'macarons');
63
+			chart.value.setOption(chartOption.value);
64
+		} else {
65
+			chartOption.value.series[0].color = ['#FF7D00', '#ccc'];
66
+			chart.value = echarts.init(rightChart.value, 'macarons');
67
+			chart.value.setOption(chartOption.value);
68
+		}
69
+	};
70
+	
71
+	onMounted(() => {
72
+	  initChart();
73
+	  window.addEventListener('resize', () => {
74
+	    chart.value.resize();
75
+	  });
76
+	});
77
+	
78
+	onUnmounted(() => {
79
+	  if (!chart.value) {
80
+	    return;
81
+	  }
82
+	  chart.value.dispose();
83
+	  chart.value = null;
84
+	});
85
+</script>
86
+
87
+<style lang="scss" scoped>
88
+	.data_display {
89
+		background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
90
+		margin-right: 10px;
91
+		color: #fff;
92
+		flex: 1;
93
+		&:nth-child(1) {
94
+			margin-left: 10px;
95
+		}
96
+		.top {
97
+			position: relative;
98
+			display: flex;
99
+			justify-content: space-between;
100
+			align-items: center;
101
+			flex-wrap: nowrap;
102
+			flex-direction: row;
103
+			z-index: 1;
104
+			align-content: flex-start;
105
+			background: url(/src/assets/images/titlebg-4340cf1c.png);
106
+			background-size: 100% 100%;
107
+			font-weight: 400;
108
+			font-size: 14px;
109
+			color: #fbffff;
110
+			padding: 8px 20px;
111
+			margin-bottom: 8px;
112
+		}
113
+		.bottom {
114
+			width: 100%;
115
+			display: flex;
116
+			justify-content: space-between;
117
+			align-items: center;
118
+			padding: 0 10px;
119
+			.left {
120
+				display: flex;
121
+				flex-direction: column;
122
+				margin-left: 40px;
123
+				.left_num {
124
+					font-size: 18px;
125
+					font-weight: 800;
126
+				}
127
+				.left_compare {
128
+					margin-top: 10px;
129
+					.left_compare_green {
130
+						color: greenyellow;
131
+					}
132
+					.left_compare_red {
133
+						color: orangered;
134
+					}
135
+					img {
136
+						width: 15px;
137
+						height: 15px;
138
+						padding-top: 2px;
139
+					}
140
+				}
141
+			}
142
+			.right {
143
+				width: 100px;
144
+				height: 100px;
145
+				margin-right: 20px;
146
+			}
147
+		}
148
+	}
149
+</style>

+ 857 - 0
src/views/dataBigScreen/examQuestionStatistics/index.vue

@@ -0,0 +1,857 @@
1
+<template>
2
+	<view class="examQuestionStatistics">
3
+		<view class="top">
4
+			<DataDisplay v-for="(item, index) in data.list" :data="item" :key="index" />
5
+		</view>
6
+		<view class="center">
7
+			<view class="_left">
8
+				<view class="center_title">
9
+					<view class="title">题目类型占比</view>
10
+				</view>
11
+				<view class="_cont">
12
+					<view ref="center_EchartTwo" class="_Echart"></view>
13
+				</view>
14
+			</view>
15
+			<view class="_center">
16
+				<view class="center_title">
17
+					<view class="title">各科室成绩走势</view>
18
+					<view>
19
+						<el-select
20
+							v-model="selectVal.selectValue"
21
+							class="m-2"
22
+							placeholder="选择题目"
23
+							style="width: 240px"
24
+							@change="changeSelect"
25
+						>
26
+							<el-option
27
+								v-for="item in selectData.selectOptions"
28
+								:key="item.qc_id"
29
+								:label="item.qc_name"
30
+								:value="item.qc_id"
31
+							/>
32
+						</el-select>
33
+					</view>
34
+				</view>
35
+				<view class="_cont">
36
+					<view ref="center_EchartThree" class="_Echart"></view>
37
+				</view>
38
+			</view>
39
+			<view class="_right">
40
+				<view class="center_title">
41
+					<view class="title">各科室综合排名</view>
42
+				</view>
43
+				<view class="ranking">
44
+					<view class="ranking_item" v-for="(item, index) in data.rankingList" :key="index">
45
+						<view>
46
+							<view class="serial" :style="index == 0 ? 'background: #ff3232' : (index == 1 ? 'background: #ff9800' : (index == 2 ? 'background: #ffd200' : 'background: #5470c6'))">{{ index + 1 }}</view>
47
+							<view class="name">{{ item.dept_name }}</view>
48
+						</view>
49
+						<view>
50
+							<view class="count">{{ item.er_score }}</view>
51
+							<img :src="item.status ? '/src/assets/icons/shang.png' : '/src/assets/icons/xia.png'" alt="" />
52
+							<view class="percentage" :style="!item.status ? 'color: #ff3232' : 'color: #00B42A'">{{ item.percentage }}</view>
53
+						</view>
54
+					</view>
55
+				</view>
56
+			</view>
57
+		</view>
58
+		<view class="bottom">
59
+			<view class="center_box">
60
+				<view class="center_left">
61
+					<view class="center_title">
62
+						<view class="title">各类型题目得分分布</view>
63
+						<view class="filter_tag">
64
+							<span v-for="(item, index) in activeList" :key="index" @click="changeActive(index)" :class="activeName == index ? 'label active' : 'label'">{{ item }}</span>
65
+						</view>
66
+					</view>
67
+					<view class="center_cont">
68
+						<view ref="center_Echart" class="center_Echart"></view>
69
+					</view>
70
+				</view>
71
+				<view class="center_right">
72
+					<view class="center_title">
73
+						<view class="title">薄弱知识点分析</view>
74
+						<view style="color: red;margin-right: 20px;">Top5</view>
75
+					</view>
76
+					<view class="cont_box">
77
+						<view class="right_cont" v-for="(item, index) in data.illegalList" :key="index">
78
+							<view class="cont_title">
79
+								<view>{{ item.category_name }}</view>
80
+								<view>{{ item.correct_rate_percent }}%</view>
81
+							</view>
82
+							<view class="progress">
83
+								<el-progress :show-text="false" :percentage="item.correct_rate_percent" :color="item.correct_rate_percent <= '50' ? 'red' : (item.correct_rate_percent > '50' && item.correct_rate_percent <= '70' ? '#FF7D00' : '#817e8a')" />
84
+							</view>
85
+						</view>
86
+					</view>
87
+				</view>
88
+			</view>
89
+			<view class="_center">
90
+				<view class="center_title">
91
+					<view class="title">各项题目类型成绩走势</view>
92
+				</view>
93
+				<view class="_cont">
94
+					<view ref="echarts_name" class="_Echart"></view>
95
+				</view>
96
+			</view>
97
+			<view class="failed_right">
98
+				<view class="failed_user">
99
+					<view class="center_title">
100
+						<view class="title">未达标人员</view>
101
+						<view style="margin-bottom: 8px;">
102
+							<el-date-picker style="width: 100%" v-model="selectVal.timeVal" value-format="YYYY-MM" @change="changeFilterTime" type="month" placeholder="选择时间" />
103
+						</view>
104
+					</view>
105
+					<view class="user_ranking">
106
+						<vue3-scroll-seamless
107
+							:classOptions="classOptions"
108
+							:dataList="data.userList"
109
+							class="scroll-wrap"
110
+						>
111
+					  	<view class="ranking_item" v-for="(item, index) in data.userList" :key="index">
112
+					  		<view class="user_info">
113
+					  			<img :src="item.avatar_url" alt="" />
114
+					  			<view class="info">
115
+					  				<view class="name">{{ item.user_name }}</view>
116
+					  				<view class="division">{{ item.dept_name }}</view>
117
+					  			</view>
118
+					  		</view>
119
+					  		<view class="fraction">{{ item.total_exams }}/{{ item.unqualified_count }}</view>
120
+					  	</view>
121
+						</vue3-scroll-seamless>
122
+					</view>
123
+				</view>
124
+				<view class="desc">
125
+					<view class="center_title">
126
+						<view class="title">训练建议</view>
127
+					</view>
128
+					<view class="desc_cont"> 
129
+						<view class="cont_text">
130
+							<view class="title">加强X光机识别训练</view>
131
+							<view class="text">&nbsp;&nbsp;&nbsp;针对X光机违禁品识别薄弱情况,建议每周组织两次专项训练,邀请专家进行案例分析。</view>
132
+						</view>
133
+						<view class="divider"></view>
134
+						<view  class="cont_text">
135
+							<view class="title">开展应急处置演练</view>
136
+							<view class="text">&nbsp;&nbsp;&nbsp;应急处置流程得分较低,建议每季度组织一次全站范围的应急演练,提高实际操作能力。</view>
137
+						</view>
138
+						<view class="divider"></view>
139
+						<view class="cont_text">
140
+							<view class="title">更新危险品知识培训</view>
141
+							<view class="text">&nbsp;&nbsp;&nbsp;危险品分类标准知识掌握不足,建议更新培训资料,增加最新案例和识别技巧。</view>
142
+						</view>
143
+					</view>
144
+				</view>
145
+			</view>
146
+		</view>
147
+	</view>
148
+</template>
149
+
150
+<script lang="ts" setup>
151
+	import { reactive, ref, onMounted, onUnmounted } from 'vue';
152
+	import { executeSqlByKeyAndParam } from '@/api/system/sql.js';
153
+	import DataDisplay from './components/dataDisplay.vue';
154
+	import * as echarts from 'echarts';
155
+
156
+	const activeName = ref('0');
157
+	const activeList = reactive(['本月', '季度', '年度']);
158
+	const selectData = reactive({
159
+		selectOptions: []
160
+	});
161
+	const classOptions = {
162
+	  limitMoveNum: 6,
163
+	};
164
+	const selectVal = reactive({
165
+		selectValue: '',
166
+		timeVal: ''
167
+	});
168
+	const data = reactive({
169
+		list: [],
170
+		illegalList: [],
171
+		rankingList: [],
172
+		userList: []
173
+	});
174
+
175
+	const chart = ref(null);
176
+	const center_Echart = ref(null);
177
+	const chartOption = ref({
178
+		legend: {
179
+			data: ['平均得分'],
180
+			left: '30px',
181
+			textStyle: {
182
+				color: '#fff',
183
+			},
184
+			selectedMode: false
185
+		},
186
+		radar: {
187
+			indicator: [],
188
+			radius: '40%',
189
+			center: ['42%', '50%'],
190
+			name: {
191
+				// 修改指标名称的文本样式
192
+				textStyle: {
193
+					color: '#fff', // 设置文字颜色为红色
194
+					fontSize: 12, // 设置文字大小
195
+				}
196
+			}
197
+		},
198
+		series: [
199
+			{
200
+				name: 'Budget vs spending',
201
+				type: 'radar',
202
+				data: [
203
+					{
204
+						value: [80, 70, 75, 80, 80, 80],
205
+						name: '平均得分',
206
+						label: {
207
+							show: true,
208
+							color: '#fff',
209
+							formatter: function (params) {
210
+								return params.value;
211
+							},
212
+						},
213
+						areaStyle: {
214
+							color: '#a9bdff',
215
+						},
216
+						// 可以设置其他样式,如颜色等
217
+						itemStyle: {
218
+							color: 'blue',
219
+						},
220
+					},
221
+				],
222
+			},
223
+		],
224
+	});
225
+	
226
+	const chartTwo = ref(null);
227
+	const center_EchartTwo = ref(null);
228
+	const chartTwoOption = ref({
229
+		tooltip: {
230
+			trigger: 'item'
231
+		},
232
+		legend: {
233
+			left: 'center',
234
+			top: 15,
235
+			textStyle: {
236
+				color: '#fff',
237
+			},
238
+			selectedMode: false
239
+		},
240
+		series: [
241
+			{
242
+				name: '占比',
243
+				type: 'pie',
244
+				radius: ['40%', '70%'],
245
+				center:['50%','60%'],
246
+				avoidLabelOverlap: false,
247
+				label: {
248
+					show: false,
249
+					position: 'center'
250
+				},
251
+				labelLine: {
252
+					show: false
253
+				},
254
+				data: []
255
+			}
256
+		]
257
+	});
258
+	
259
+	const chartThree = ref(null);
260
+	const center_EchartThree = ref(null);
261
+	const chartThreeOption = ref({
262
+		tooltip: {
263
+			trigger: 'axis',
264
+			axisPointer: {
265
+				type: 'cross',
266
+				label: {
267
+					backgroundColor: '#6a7985'
268
+				}
269
+			}
270
+		},
271
+		legend: {
272
+			data: [],
273
+			textStyle: {
274
+				color: '#fff',
275
+			},
276
+			selectedMode: false
277
+		},
278
+		xAxis: {
279
+			type: 'category',
280
+			boundaryGap: false,
281
+			data: ['1月', '2月', '3月', '4月', '5月', '6月'],
282
+			axisLabel: {
283
+				color: '#fff'
284
+			}
285
+		},
286
+		yAxis: {
287
+			type: 'value',
288
+			axisLabel: {
289
+				color: '#fff'
290
+			}
291
+		},
292
+		series: []
293
+	});
294
+	
295
+	const chartT = ref(null);
296
+	const echarts_name = ref(null);
297
+	const chartTOption = ref({
298
+		legend: {
299
+			data: [],
300
+			textStyle: {
301
+				color: '#fff',
302
+			},
303
+			selectedMode: false
304
+		},
305
+		grid: {
306
+			left: '3%',
307
+			right: '4%',
308
+			bottom: '3%',
309
+			containLabel: true
310
+		},
311
+		xAxis: [
312
+			{
313
+				type: 'category',
314
+				boundaryGap: false,
315
+				data: ['1月', '2月', '3月', '4月', '5月', '6月'],
316
+				axisLabel: {
317
+					color: '#fff'
318
+				}
319
+			}
320
+		],
321
+		yAxis: [
322
+			{
323
+				type: 'value',
324
+				axisLabel: {
325
+					color: '#fff'
326
+				}
327
+			}
328
+		],
329
+		series: []
330
+	});
331
+
332
+	// 初始化图标
333
+	const initChart = () => {
334
+		chart.value = echarts.init(center_Echart.value, 'macarons');
335
+		chart.value.setOption(chartOption.value);
336
+		chartTwo.value = echarts.init(center_EchartTwo.value, 'macarons');
337
+		chartTwo.value.setOption(chartTwoOption.value);
338
+		chartThree.value = echarts.init(center_EchartThree.value, 'macarons');
339
+		chartThree.value.setOption(chartThreeOption.value);
340
+		chartT.value = echarts.init(echarts_name.value, 'macarons');
341
+		chartT.value.setOption(chartTOption.value);
342
+	};
343
+	
344
+	// 点击切换各类型题目得分分布
345
+	const changeActive = async (ind: any) => {
346
+		if (activeName.value == ind) return;
347
+		if (ind == 0) {
348
+			// 月度
349
+			await getCount('month_avg_score', '得分分布', '', null);
350
+		} else if (ind == 1) {
351
+			// 季度
352
+			await getCount('quarter_avg_score', '得分分布', '', null);
353
+		} else {
354
+			// 年度
355
+			await getCount('year_avg_score', '得分分布', '', null);
356
+		}
357
+		chart.value = echarts.init(center_Echart.value, 'macarons');
358
+		chart.value.setOption(chartOption.value);
359
+		activeName.value = ind;
360
+	};
361
+	
362
+	// 处理接口返回数据
363
+	const getCount = async (url: String, title: String, key: any, params: any) => {
364
+		await executeSqlByKeyAndParam(url, params).then(res => {
365
+			if (title == '参考人数') {
366
+				data.list.push({
367
+					title,
368
+					current_month: res.data[0].current_month_count,
369
+					previous_month: res.data[0].previous_month_count,
370
+					percentage: res.data[0].year_over_year_growth,
371
+					status: res.data[0].current_month_count >= res.data[0].previous_month_count
372
+				});
373
+			} else if (title == '平均成绩') {
374
+				data.list.push({
375
+					title,
376
+					current_month: res.data[0].current_month_avg,
377
+					previous_month: res.data[0].previous_month_avg,
378
+					percentage: res.data[0].year_over_year_growth,
379
+					status: res.data[0].current_month_avg >= res.data[0].previous_month_avg
380
+				});
381
+			} else if (title == '合格率') {
382
+				data.list.push({
383
+					title,
384
+					current_month: res.data[0].current_month_pass_rate,
385
+					previous_month: res.data[0].previous_month_pass_rate,
386
+					percentage: res.data[0].year_over_year_growth,
387
+					status: res.data[0].current_month_pass_rate >= res.data[0].previous_month_pass_rate
388
+				});
389
+			} else if (title == '优秀率') {
390
+				data.list.push({
391
+					title,
392
+					current_month: res.data[0].current_month_good_rate,
393
+					previous_month: res.data[0].previous_month_good_rate,
394
+					percentage: res.data[0].year_over_year_growth,
395
+					status: res.data[0].current_month_good_rate >= res.data[0].previous_month_good_rate
396
+				});
397
+			} else if (title == '综合排名') {
398
+				data[key] = res.data;
399
+			} else if (title == '得分分布') {
400
+				const nameList = res.data.map((item) => {
401
+					return {
402
+						name: item.category,
403
+					}
404
+				});
405
+				const valList = res.data.map((item) => item.avg_score);
406
+				chartOption.value.radar.indicator = nameList;
407
+				chartOption.value.series[0].data[0].value = valList;
408
+			} else if (title == '问答占比') {
409
+				chartTwoOption.value.series[0].data = res.data.map((item) => {
410
+					return {
411
+						name: item.category_name,
412
+						value: item.total_count
413
+					}
414
+				});
415
+			} else if (title == '题目类型下拉') {
416
+				selectData.selectOptions = res.data;
417
+			} else if (title == '未达标') {
418
+				data.userList = res.data;
419
+			} else if (title == '薄弱知识') {
420
+				data.illegalList = res.data;
421
+			}
422
+		});
423
+	};
424
+	
425
+	// 处理走势折线图数据
426
+	const handlerDatas = (arr, name, type) => {
427
+		if (type == 'topic') {
428
+			const data = arr.filter((item) => item.question_type == name);
429
+			const list = [0, 0, 0, 0, 0, 0];
430
+			data.map((item, ind) => {
431
+				list[item.month - 1] = item.total_score;
432
+			});
433
+			return {
434
+				name: name,
435
+				type: 'line',
436
+				stack: 'Total',
437
+				emphasis: {
438
+					focus: 'series'
439
+				},
440
+				data: list
441
+			};
442
+		} else {
443
+			const data = arr.filter((item) => item.dept_name == name);
444
+			const list = [0, 0, 0, 0, 0, 0];
445
+			data.map((item, ind) => {
446
+				list[item.month - 1] = item.score;
447
+			});
448
+			return {
449
+				name: name,
450
+				type: 'line',
451
+				stack: 'Total',
452
+				areaStyle: {},
453
+				emphasis: {
454
+					focus: 'series'
455
+				},
456
+				data: list
457
+			};
458
+		}
459
+	}
460
+	
461
+	// 科室成绩走势
462
+	const getScoreData = async (url, params, type) => {
463
+		await executeSqlByKeyAndParam(url, params).then(res => {
464
+			if (type == 'topic') {
465
+				const newArr = res.data.map((item) => item.question_type).filter((item, index, arr) => arr.indexOf(item) === index);
466
+				const filteredArr = [...newArr].filter(el => el !== undefined);
467
+				const newVal = filteredArr.map((item) => handlerDatas(res.data, item, type));
468
+				chartTOption.value.legend.data = filteredArr;
469
+				chartTOption.value.series = newVal;
470
+			} else {
471
+				const newArr = res.data.map((item) => item.dept_name).filter((item, index, arr) => arr.indexOf(item) === index);
472
+				const newVal = newArr.map((item) => handlerDatas(res.data, item, ''));
473
+				chartThreeOption.value.legend.data = newArr;
474
+				chartThreeOption.value.series = newVal;
475
+			}
476
+		});
477
+	}
478
+	
479
+	// 修改科室成绩走势筛选条件
480
+	const changeSelect = (e) => {
481
+		getScoreData('department_score_trend', { qc_id: e }, '');
482
+		chartThree.value = echarts.init(center_EchartThree.value, 'macarons');
483
+		chartThree.value.setOption(chartThreeOption.value);
484
+	};
485
+	
486
+	// 获取当前月
487
+	const getCurrentMonth = () => {
488
+		const currentYear = new Date().getFullYear();
489
+		const month = new Date().getMonth() + 1;
490
+		return `${currentYear}-${month < 10 ? '0' + month : month}`;
491
+	}
492
+	
493
+	// 未达标人员根据时间筛选
494
+	const changeFilterTime = (e) => {
495
+		getCount('exam_of_no_pass_rate', '未达标', '', { month: e || getCurrentMonth() });
496
+	};
497
+	
498
+	onMounted(async () => {
499
+		await getCount('exam_of_total_count', '参考人数', '', null);
500
+		await getCount('exam_of_avg', '平均成绩', '', null);
501
+		await getCount('exam_of_pass_rate', '合格率', '', null);
502
+		await getCount('exam_of_good_rate', '优秀率', '', null);
503
+		await getCount('exam_of_dept2_sort', '综合排名', 'rankingList', null);
504
+		await getCount('month_avg_score', '得分分布', '', null);
505
+		await getCount('proportion_of_answer_categories', '问答占比', '', null);
506
+		await getCount('question_type', '题目类型下拉', '', null);
507
+		await getCount('top5_weak_knowledge_points', '薄弱知识', '', null);
508
+		await getCount('exam_of_no_pass_rate', '未达标', '', { month: getCurrentMonth() });
509
+		await getScoreData('department_score_trend', { qc_id: 'QC250624000003' }, '');
510
+		await getScoreData('six_months_question_score', null, 'topic');
511
+		initChart();
512
+		window.addEventListener('resize', () => {
513
+			chart.value.resize();
514
+			chartTwo.value.resize();
515
+			chartThree.value.resize();
516
+			chartT.value.resize();
517
+		});
518
+	});
519
+
520
+	onUnmounted(() => {
521
+		if (!chart.value) {
522
+			return;
523
+		}
524
+		chart.value.dispose();
525
+		chart.value = null;
526
+		if (!chartTwo.value) {
527
+			return;
528
+		}
529
+		chartTwo.value.dispose();
530
+		chartTwo.value = null;
531
+		if (!chartThree.value) {
532
+			return;
533
+		}
534
+		chartThree.value.dispose();
535
+		chartThree.value = null;
536
+		if (!chartT.value) {
537
+			return;
538
+		}
539
+		chartT.value.dispose();
540
+		chartT.value = null;
541
+	});
542
+</script>
543
+
544
+<style lang="scss" scoped>
545
+	::v-deep .el-select__wrapper {
546
+		background-color: rgba(0, 0, 0, .3);
547
+	}
548
+	::v-deep .el-select__placeholder {
549
+		color: #fff;
550
+	}
551
+	::v-deep .is-transparent {
552
+		color: #a8abb2;
553
+	}
554
+	::v-deep .el-input__wrapper {
555
+		background-color: rgba(0, 0, 0, .3);
556
+	}
557
+	::v-deep .el-input__inner {
558
+		color: #fff;
559
+	}
560
+	.examQuestionStatistics {
561
+		width: 100%;
562
+		position: fixed;
563
+		height: 100%;
564
+		background: url('../../../assets/images/bg-99b6904c.png');
565
+		background-size: 100% 100%;
566
+		overflow-y: auto;
567
+		padding-bottom: 150px;
568
+		.center_title {
569
+			width: 100%;
570
+			display: flex;
571
+			justify-content: space-between;
572
+			align-items: center;
573
+			.title {
574
+				position: relative;
575
+				display: flex;
576
+				justify-content: space-between;
577
+				align-items: center;
578
+				flex-wrap: nowrap;
579
+				flex-direction: row;
580
+				z-index: 1;
581
+				align-content: flex-start;
582
+				background: url(/src/assets/images/titlebg-4340cf1c.png);
583
+				background-size: 100% 100%;
584
+				font-weight: 400;
585
+				font-size: 14px;
586
+				color: #fbffff;
587
+				padding: 8px 20px;
588
+				margin-bottom: 8px;
589
+				min-width: 110px;
590
+			}
591
+			.filter_tag {
592
+				padding-right: 15px;
593
+				.label {
594
+					display: inline-flex;
595
+					align-items: center;
596
+					justify-content: center;
597
+					font-size: 12px;
598
+					background: #11134b;
599
+					padding: 4px 4px;
600
+					margin: -4px 0;
601
+					border-radius: 4px;
602
+					color: #fbffff;
603
+					cursor: pointer;
604
+				}
605
+				.active {
606
+					background: #171b75;
607
+				}
608
+			}
609
+		}
610
+		.top {
611
+			width: calc(100% - 200px);
612
+			display: flex;
613
+			justify-content: space-between;
614
+			margin-top: 10px;
615
+		}
616
+		.center {
617
+			width: calc(100% - 220px);
618
+			display: flex;
619
+			justify-content: space-between;
620
+			margin-top: 10px;
621
+			margin-left: 10px;
622
+			._left {
623
+				width: 24.5%;
624
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
625
+				._cont {
626
+					width: 100%;
627
+					height: 400px;
628
+					display: flex;
629
+					justify-content: center;
630
+					._Echart {
631
+						width: 100%;
632
+						height: 100%;
633
+					}
634
+				}
635
+			}
636
+			._center {
637
+				width: 50%;
638
+				margin-left: 10px;
639
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
640
+				._cont {
641
+					width: 100%;
642
+					height: 400px;
643
+					display: flex;
644
+					justify-content: center;
645
+					padding: 10px;
646
+					._Echart {
647
+						width: 100%;
648
+						height: 100%;
649
+					}
650
+				}
651
+			}
652
+			._right {
653
+				width: 24.5%;
654
+				margin-left: 10px;
655
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
656
+				.ranking {
657
+					width: 100%;
658
+					padding: 0 20px;
659
+					display: inline-block;
660
+					color: #fff;
661
+					.ranking_item {
662
+						height: 40px;
663
+						display: flex;
664
+						justify-content: space-between;
665
+						align-items: center;
666
+						.serial {
667
+							display: inline-block;
668
+							width: 16px;
669
+							height: 16px;
670
+							color: #fff;
671
+							border-radius: 4px;
672
+							text-align: center;
673
+							line-height: 16px;
674
+							font-size: 15px;
675
+							margin-right: 8px;
676
+						}
677
+						.name {
678
+							font-size: 15px;
679
+						}
680
+						.count {
681
+							font-size: 15px;
682
+							font-weight: 800;
683
+							margin-right: 8px;
684
+						}
685
+						img {
686
+							width: 15px;
687
+							height: 15px;
688
+							padding-top: 5px;
689
+						}
690
+						.percentage {
691
+							font-size: 12px;
692
+						}
693
+					}
694
+				}
695
+			}
696
+		}
697
+		.bottom {
698
+			width: calc(100% - 220px);
699
+			display: flex;
700
+			justify-content: space-between;
701
+			margin-top: 10px;
702
+			margin-left: 10px;
703
+			.center_box {
704
+				width: 24.5%;
705
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
706
+				.center_left {
707
+					width: 100%;
708
+					background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
709
+					display: inline-block;
710
+					.center_cont {
711
+						width: 100%;
712
+						display: flex;
713
+						justify-content: center;
714
+						.center_Echart {
715
+							width: 100%;
716
+							height: 280px;
717
+						}
718
+					}
719
+				}
720
+				.center_right {
721
+					display: block;
722
+					background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
723
+					.cont_box {
724
+						width: calc(100% - 40px);
725
+						margin-left: 20px;
726
+						display: inline-block;
727
+						.right_cont {
728
+							width: 100%;
729
+							height: 60px;
730
+							display: inline-block;
731
+							.cont_title {
732
+								height: 40px;
733
+								display: flex;
734
+								justify-content: space-between;
735
+								align-items: center;
736
+								color: #fff;
737
+							}
738
+							.progress {
739
+								margin-top: 10px;
740
+							}
741
+						}
742
+					}
743
+				}
744
+			}
745
+			._center {
746
+				width: 50%;
747
+				margin-left: 10px;
748
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
749
+				._cont {
750
+					width: 100%;
751
+					height: 600px;
752
+					display: flex;
753
+					justify-content: center;
754
+					padding: 10px;
755
+					._Echart {
756
+						width: 100%;
757
+						height: 100%;
758
+					}
759
+				}
760
+			}
761
+			.failed_right {
762
+				width: 24.5%;
763
+				margin-left: 10px;
764
+				background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
765
+				.failed_user {
766
+					display: block;
767
+					background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
768
+					.user_ranking {
769
+						width: 100%;
770
+						height: 286px;
771
+						padding: 0 20px;
772
+						overflow: hidden;
773
+						display: inline-block;
774
+						color: #fff;
775
+						.ranking_item {
776
+							width: 91%;
777
+							height: 70px;
778
+							display: flex;
779
+							justify-content: space-between;
780
+							align-items: center;
781
+							border-bottom: 1px solid #ffffff40;
782
+							.user_info {
783
+								display: flex;
784
+								img {
785
+									width: 50px;
786
+									height: 50px;
787
+									border-radius: 50%;
788
+									border: 1px solid #ffffff40;
789
+								}
790
+								.info {
791
+									display: flex;
792
+									flex-direction: column;
793
+									margin-left: 8px;
794
+									font-size: 15px;
795
+									.division {
796
+										color: #ccc;
797
+										font-size: 14px;
798
+										margin-top: 8px;
799
+									}
800
+								}
801
+							}
802
+							.fraction {
803
+								color: #ff3232;
804
+							}
805
+						}
806
+					}
807
+				}
808
+				.desc {
809
+					display: block;
810
+					.desc_cont {
811
+						padding: 10px;
812
+						padding-bottom: 0;
813
+						color: #fff;
814
+						display: flex;
815
+						flex-wrap: wrap;
816
+						justify-content: space-around;
817
+						background: linear-gradient(to bottom, rgba(8, 97, 117, 0) 0%, #13a2d6 200%);
818
+						.cont_text {
819
+							display: flex;
820
+							flex-direction: column;
821
+							margin: 10px 0;
822
+							&:nth-child(1) {
823
+								border-left: 5px solid blue;
824
+							}
825
+							&:nth-child(3) {
826
+								border-left: 5px solid red;
827
+							}
828
+							&:nth-child(5) {
829
+								border-left: 5px solid yellow;
830
+							}
831
+							& {
832
+								padding-left: 10px;
833
+							}
834
+							.title {
835
+								font-size: 15px;
836
+								font-weight: 600;
837
+							}
838
+							.text {
839
+								margin-top: 8px;
840
+								font-size: 13px;
841
+								line-height: 23px;
842
+							}
843
+							&:last-child {
844
+								border-bottom: none;
845
+							}
846
+						}
847
+						.divider {
848
+							width: 100%;
849
+							height: 1px;
850
+							border-bottom: 1px solid #ffffff40;
851
+						}
852
+					}
853
+				}
854
+			}
855
+		}
856
+	}
857
+</style>

+ 82 - 0
src/views/error/401.vue

@@ -0,0 +1,82 @@
1
+<template>
2
+  <div class="errPage-container">
3
+    <el-button icon="arrow-left" class="pan-back-btn" @click="back">
4
+      返回
5
+    </el-button>
6
+    <el-row>
7
+      <el-col :span="12">
8
+        <h1 class="text-jumbo text-ginormous">
9
+          401错误!
10
+        </h1>
11
+        <h2>您没有访问权限!</h2>
12
+        <h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
13
+        <ul class="list-unstyled">
14
+          <li class="link-type">
15
+            <router-link to="/">
16
+              回首页
17
+            </router-link>
18
+          </li>
19
+        </ul>
20
+      </el-col>
21
+      <el-col :span="12">
22
+        <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
23
+      </el-col>
24
+    </el-row>
25
+  </div>
26
+</template>
27
+
28
+<script setup>
29
+import errImage from "@/assets/401_images/401.gif"
30
+
31
+let { proxy } = getCurrentInstance()
32
+
33
+const errGif = ref(errImage + "?" + +new Date())
34
+
35
+function back() {
36
+  if (proxy.$route.query.noGoBack) {
37
+    proxy.$router.push({ path: "/" })
38
+  } else {
39
+    proxy.$router.go(-1)
40
+  }
41
+}
42
+</script>
43
+
44
+<style lang="scss" scoped>
45
+.errPage-container {
46
+  width: 800px;
47
+  max-width: 100%;
48
+  margin: 100px auto;
49
+  .pan-back-btn {
50
+    background: #008489;
51
+    color: #fff;
52
+    border: none !important;
53
+  }
54
+  .pan-gif {
55
+    margin: 0 auto;
56
+    display: block;
57
+  }
58
+  .pan-img {
59
+    display: block;
60
+    margin: 0 auto;
61
+    width: 100%;
62
+  }
63
+  .text-jumbo {
64
+    font-size: 60px;
65
+    font-weight: 700;
66
+    color: #484848;
67
+  }
68
+  .list-unstyled {
69
+    font-size: 14px;
70
+    li {
71
+      padding-bottom: 5px;
72
+    }
73
+    a {
74
+      color: #008489;
75
+      text-decoration: none;
76
+      &:hover {
77
+        text-decoration: underline;
78
+      }
79
+    }
80
+  }
81
+}
82
+</style>

+ 227 - 0
src/views/error/404.vue

@@ -0,0 +1,227 @@
1
+<template>
2
+  <div class="wscn-http404-container">
3
+    <div class="wscn-http404">
4
+      <div class="pic-404">
5
+        <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
6
+        <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
7
+        <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
8
+        <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
9
+      </div>
10
+      <div class="bullshit">
11
+        <div class="bullshit__oops">
12
+          404错误!
13
+        </div>
14
+        <div class="bullshit__headline">
15
+          {{ message }}
16
+        </div>
17
+        <div class="bullshit__info">
18
+          对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
19
+        </div>
20
+        <router-link to="/index" class="bullshit__return-home">
21
+          返回首页
22
+        </router-link>
23
+      </div>
24
+    </div>
25
+  </div>
26
+</template>
27
+
28
+<script setup>
29
+let message = computed(() => {
30
+  return '找不到网页!'
31
+})
32
+</script>
33
+
34
+<style lang="scss" scoped>
35
+.wscn-http404-container{
36
+  transform: translate(-50%,-50%);
37
+  position: absolute;
38
+  top: 40%;
39
+  left: 50%;
40
+}
41
+.wscn-http404 {
42
+  position: relative;
43
+  width: 1200px;
44
+  padding: 0 50px;
45
+  overflow: hidden;
46
+  .pic-404 {
47
+    position: relative;
48
+    float: left;
49
+    width: 600px;
50
+    overflow: hidden;
51
+    &__parent {
52
+      width: 100%;
53
+    }
54
+    &__child {
55
+      position: absolute;
56
+      &.left {
57
+        width: 80px;
58
+        top: 17px;
59
+        left: 220px;
60
+        opacity: 0;
61
+        animation-name: cloudLeft;
62
+        animation-duration: 2s;
63
+        animation-timing-function: linear;
64
+        animation-fill-mode: forwards;
65
+        animation-delay: 1s;
66
+      }
67
+      &.mid {
68
+        width: 46px;
69
+        top: 10px;
70
+        left: 420px;
71
+        opacity: 0;
72
+        animation-name: cloudMid;
73
+        animation-duration: 2s;
74
+        animation-timing-function: linear;
75
+        animation-fill-mode: forwards;
76
+        animation-delay: 1.2s;
77
+      }
78
+      &.right {
79
+        width: 62px;
80
+        top: 100px;
81
+        left: 500px;
82
+        opacity: 0;
83
+        animation-name: cloudRight;
84
+        animation-duration: 2s;
85
+        animation-timing-function: linear;
86
+        animation-fill-mode: forwards;
87
+        animation-delay: 1s;
88
+      }
89
+      @keyframes cloudLeft {
90
+        0% {
91
+          top: 17px;
92
+          left: 220px;
93
+          opacity: 0;
94
+        }
95
+        20% {
96
+          top: 33px;
97
+          left: 188px;
98
+          opacity: 1;
99
+        }
100
+        80% {
101
+          top: 81px;
102
+          left: 92px;
103
+          opacity: 1;
104
+        }
105
+        100% {
106
+          top: 97px;
107
+          left: 60px;
108
+          opacity: 0;
109
+        }
110
+      }
111
+      @keyframes cloudMid {
112
+        0% {
113
+          top: 10px;
114
+          left: 420px;
115
+          opacity: 0;
116
+        }
117
+        20% {
118
+          top: 40px;
119
+          left: 360px;
120
+          opacity: 1;
121
+        }
122
+        70% {
123
+          top: 130px;
124
+          left: 180px;
125
+          opacity: 1;
126
+        }
127
+        100% {
128
+          top: 160px;
129
+          left: 120px;
130
+          opacity: 0;
131
+        }
132
+      }
133
+      @keyframes cloudRight {
134
+        0% {
135
+          top: 100px;
136
+          left: 500px;
137
+          opacity: 0;
138
+        }
139
+        20% {
140
+          top: 120px;
141
+          left: 460px;
142
+          opacity: 1;
143
+        }
144
+        80% {
145
+          top: 180px;
146
+          left: 340px;
147
+          opacity: 1;
148
+        }
149
+        100% {
150
+          top: 200px;
151
+          left: 300px;
152
+          opacity: 0;
153
+        }
154
+      }
155
+    }
156
+  }
157
+  .bullshit {
158
+    position: relative;
159
+    float: left;
160
+    width: 300px;
161
+    padding: 30px 0;
162
+    overflow: hidden;
163
+    &__oops {
164
+      font-size: 32px;
165
+      font-weight: bold;
166
+      line-height: 40px;
167
+      color: #1482f0;
168
+      opacity: 0;
169
+      margin-bottom: 20px;
170
+      animation-name: slideUp;
171
+      animation-duration: 0.5s;
172
+      animation-fill-mode: forwards;
173
+    }
174
+    &__headline {
175
+      font-size: 20px;
176
+      line-height: 24px;
177
+      color: #222;
178
+      font-weight: bold;
179
+      opacity: 0;
180
+      margin-bottom: 10px;
181
+      animation-name: slideUp;
182
+      animation-duration: 0.5s;
183
+      animation-delay: 0.1s;
184
+      animation-fill-mode: forwards;
185
+    }
186
+    &__info {
187
+      font-size: 13px;
188
+      line-height: 21px;
189
+      color: grey;
190
+      opacity: 0;
191
+      margin-bottom: 30px;
192
+      animation-name: slideUp;
193
+      animation-duration: 0.5s;
194
+      animation-delay: 0.2s;
195
+      animation-fill-mode: forwards;
196
+    }
197
+    &__return-home {
198
+      display: block;
199
+      float: left;
200
+      width: 110px;
201
+      height: 36px;
202
+      background: #1482f0;
203
+      border-radius: 100px;
204
+      text-align: center;
205
+      color: #ffffff;
206
+      opacity: 0;
207
+      font-size: 14px;
208
+      line-height: 36px;
209
+      cursor: pointer;
210
+      animation-name: slideUp;
211
+      animation-duration: 0.5s;
212
+      animation-delay: 0.3s;
213
+      animation-fill-mode: forwards;
214
+    }
215
+    @keyframes slideUp {
216
+      0% {
217
+        transform: translateY(60px);
218
+        opacity: 0;
219
+      }
220
+      100% {
221
+        transform: translateY(0);
222
+        opacity: 1;
223
+      }
224
+    }
225
+  }
226
+}
227
+</style>

文件差异内容过多而无法显示
+ 1131 - 0
src/views/index.vue


+ 584 - 0
src/views/item/items/index.vue

@@ -0,0 +1,584 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="租户号" prop="tenantId">
5
+        <el-input
6
+          v-model="queryParams.tenantId"
7
+          placeholder="请输入租户号"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="乐观锁" prop="revision">
13
+        <el-input
14
+          v-model="queryParams.revision"
15
+          placeholder="请输入乐观锁"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item label="查获记录ID" prop="recordId">
21
+        <el-input
22
+          v-model="queryParams.recordId"
23
+          placeholder="请输入查获记录ID"
24
+          clearable
25
+          @keyup.enter="handleQuery"
26
+        />
27
+      </el-form-item>
28
+      <el-form-item label="物品编码" prop="itemCode">
29
+        <el-input
30
+          v-model="queryParams.itemCode"
31
+          placeholder="请输入物品编码"
32
+          clearable
33
+          @keyup.enter="handleQuery"
34
+        />
35
+      </el-form-item>
36
+      <el-form-item label="数量" prop="quantity">
37
+        <el-input
38
+          v-model="queryParams.quantity"
39
+          placeholder="请输入数量"
40
+          clearable
41
+          @keyup.enter="handleQuery"
42
+        />
43
+      </el-form-item>
44
+      <el-form-item label="图片URL" prop="imageUrl">
45
+        <el-input
46
+          v-model="queryParams.imageUrl"
47
+          placeholder="请输入图片URL"
48
+          clearable
49
+          @keyup.enter="handleQuery"
50
+        />
51
+      </el-form-item>
52
+      <el-form-item label="检查部位编码1级" prop="checkPositionCodeOne">
53
+        <el-input
54
+          v-model="queryParams.checkPositionCodeOne"
55
+          placeholder="请输入检查部位编码1级"
56
+          clearable
57
+          @keyup.enter="handleQuery"
58
+        />
59
+      </el-form-item>
60
+      <el-form-item label="是否故意隐匿(1-是,0-否)" prop="isActiveConcealment">
61
+        <el-input
62
+          v-model="queryParams.isActiveConcealment"
63
+          placeholder="请输入是否故意隐匿(1-是,0-否)"
64
+          clearable
65
+          @keyup.enter="handleQuery"
66
+        />
67
+      </el-form-item>
68
+      <el-form-item label="检查部位编码2级" prop="checkPositionCodeTwo">
69
+        <el-input
70
+          v-model="queryParams.checkPositionCodeTwo"
71
+          placeholder="请输入检查部位编码2级"
72
+          clearable
73
+          @keyup.enter="handleQuery"
74
+        />
75
+      </el-form-item>
76
+      <el-form-item label="具体位置" prop="checkPositionSpecific">
77
+        <el-input
78
+          v-model="queryParams.checkPositionSpecific"
79
+          placeholder="请输入具体位置"
80
+          clearable
81
+          @keyup.enter="handleQuery"
82
+        />
83
+      </el-form-item>
84
+      <el-form-item label="物品名称" prop="itemName">
85
+        <el-input
86
+          v-model="queryParams.itemName"
87
+          placeholder="请输入物品名称"
88
+          clearable
89
+          @keyup.enter="handleQuery"
90
+        />
91
+      </el-form-item>
92
+      <el-form-item label="物品分类编码1级" prop="categoryCodeOne">
93
+        <el-input
94
+          v-model="queryParams.categoryCodeOne"
95
+          placeholder="请输入物品分类编码1级"
96
+          clearable
97
+          @keyup.enter="handleQuery"
98
+        />
99
+      </el-form-item>
100
+      <el-form-item label="物品分类名称1级" prop="categoryNameOne">
101
+        <el-input
102
+          v-model="queryParams.categoryNameOne"
103
+          placeholder="请输入物品分类名称1级"
104
+          clearable
105
+          @keyup.enter="handleQuery"
106
+        />
107
+      </el-form-item>
108
+      <el-form-item label="物品分类编码2级" prop="categoryCodeTwo">
109
+        <el-input
110
+          v-model="queryParams.categoryCodeTwo"
111
+          placeholder="请输入物品分类编码2级"
112
+          clearable
113
+          @keyup.enter="handleQuery"
114
+        />
115
+      </el-form-item>
116
+      <el-form-item label="物品分类名称2级" prop="categoryNameTwo">
117
+        <el-input
118
+          v-model="queryParams.categoryNameTwo"
119
+          placeholder="请输入物品分类名称2级"
120
+          clearable
121
+          @keyup.enter="handleQuery"
122
+        />
123
+      </el-form-item>
124
+      <el-form-item label="单位" prop="unit">
125
+        <el-input
126
+          v-model="queryParams.unit"
127
+          placeholder="请输入单位"
128
+          clearable
129
+          @keyup.enter="handleQuery"
130
+        />
131
+      </el-form-item>
132
+      <el-form-item label="检查部位名称1级" prop="checkPositionNameOne">
133
+        <el-input
134
+          v-model="queryParams.checkPositionNameOne"
135
+          placeholder="请输入检查部位名称1级"
136
+          clearable
137
+          @keyup.enter="handleQuery"
138
+        />
139
+      </el-form-item>
140
+      <el-form-item label="检查部位名称2级" prop="checkPositionNameTwo">
141
+        <el-input
142
+          v-model="queryParams.checkPositionNameTwo"
143
+          placeholder="请输入检查部位名称2级"
144
+          clearable
145
+          @keyup.enter="handleQuery"
146
+        />
147
+      </el-form-item>
148
+      <el-form-item label="单位名称" prop="unitDesc">
149
+        <el-input
150
+          v-model="queryParams.unitDesc"
151
+          placeholder="请输入单位名称"
152
+          clearable
153
+          @keyup.enter="handleQuery"
154
+        />
155
+      </el-form-item>
156
+      <el-form-item label="处理方式" prop="handlingMethod">
157
+        <el-input
158
+          v-model="queryParams.handlingMethod"
159
+          placeholder="请输入处理方式"
160
+          clearable
161
+          @keyup.enter="handleQuery"
162
+        />
163
+      </el-form-item>
164
+      <el-form-item label="处理方式名称" prop="handlingMethodDesc">
165
+        <el-input
166
+          v-model="queryParams.handlingMethodDesc"
167
+          placeholder="请输入处理方式名称"
168
+          clearable
169
+          @keyup.enter="handleQuery"
170
+        />
171
+      </el-form-item>
172
+      <el-form-item label="图片ID" prop="imageId">
173
+        <el-input
174
+          v-model="queryParams.imageId"
175
+          placeholder="请输入图片ID"
176
+          clearable
177
+          @keyup.enter="handleQuery"
178
+        />
179
+      </el-form-item>
180
+      <el-form-item>
181
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
182
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
183
+      </el-form-item>
184
+    </el-form>
185
+
186
+    <el-row :gutter="10" class="mb8">
187
+      <el-col :span="1.5">
188
+        <el-button
189
+          type="primary"
190
+          plain
191
+          icon="Plus"
192
+          @click="handleAdd"
193
+          v-hasPermi="['item:items:add']"
194
+        >新增</el-button>
195
+      </el-col>
196
+      <el-col :span="1.5">
197
+        <el-button
198
+          type="success"
199
+          plain
200
+          icon="Edit"
201
+          :disabled="single"
202
+          @click="handleUpdate"
203
+          v-hasPermi="['item:items:edit']"
204
+        >修改</el-button>
205
+      </el-col>
206
+      <el-col :span="1.5">
207
+        <el-button
208
+          type="danger"
209
+          plain
210
+          icon="Delete"
211
+          :disabled="multiple"
212
+          @click="handleDelete"
213
+          v-hasPermi="['item:items:remove']"
214
+        >删除</el-button>
215
+      </el-col>
216
+      <el-col :span="1.5">
217
+        <el-button
218
+          type="warning"
219
+          plain
220
+          icon="Download"
221
+          @click="handleExport"
222
+          v-hasPermi="['item:items:export']"
223
+        >导出</el-button>
224
+      </el-col>
225
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
226
+    </el-row>
227
+
228
+    <el-table v-loading="loading" :data="itemsList" @selection-change="handleSelectionChange">
229
+      <el-table-column type="selection" width="55" align="center" />
230
+      <el-table-column label="租户号" align="center" prop="tenantId" />
231
+      <el-table-column label="乐观锁" align="center" prop="revision" />
232
+      <el-table-column label="主键" align="center" prop="id" />
233
+      <el-table-column label="查获记录ID" align="center" prop="recordId" />
234
+      <el-table-column label="物品编码" align="center" prop="itemCode" />
235
+      <el-table-column label="数量" align="center" prop="quantity" />
236
+      <el-table-column label="图片URL" align="center" prop="imageUrl" />
237
+      <el-table-column label="备注" align="center" prop="remark" />
238
+      <el-table-column label="检查部位编码1级" align="center" prop="checkPositionCodeOne" />
239
+      <el-table-column label="是否故意隐匿(1-是,0-否)" align="center" prop="isActiveConcealment" />
240
+      <el-table-column label="检查部位编码2级" align="center" prop="checkPositionCodeTwo" />
241
+      <el-table-column label="具体位置" align="center" prop="checkPositionSpecific" />
242
+      <el-table-column label="物品名称" align="center" prop="itemName" />
243
+      <el-table-column label="物品分类编码1级" align="center" prop="categoryCodeOne" />
244
+      <el-table-column label="物品分类名称1级" align="center" prop="categoryNameOne" />
245
+      <el-table-column label="物品分类编码2级" align="center" prop="categoryCodeTwo" />
246
+      <el-table-column label="物品分类名称2级" align="center" prop="categoryNameTwo" />
247
+      <el-table-column label="单位" align="center" prop="unit" />
248
+      <el-table-column label="检查部位名称1级" align="center" prop="checkPositionNameOne" />
249
+      <el-table-column label="检查部位名称2级" align="center" prop="checkPositionNameTwo" />
250
+      <el-table-column label="单位名称" align="center" prop="unitDesc" />
251
+      <el-table-column label="处理方式" align="center" prop="handlingMethod" />
252
+      <el-table-column label="处理方式名称" align="center" prop="handlingMethodDesc" />
253
+      <el-table-column label="图片ID" align="center" prop="imageId" />
254
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
255
+        <template #default="scope">
256
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['item:items:edit']">修改</el-button>
257
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['item:items:remove']">删除</el-button>
258
+        </template>
259
+      </el-table-column>
260
+    </el-table>
261
+    
262
+    <pagination
263
+      v-show="total>0"
264
+      :total="total"
265
+      v-model:page="queryParams.pageNum"
266
+      v-model:limit="queryParams.pageSize"
267
+      @pagination="getList"
268
+    />
269
+
270
+    <!-- 添加或修改查获物品明细对话框 -->
271
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
272
+      <el-form ref="itemsRef" :model="form" :rules="rules" label-width="80px">
273
+        <el-form-item label="租户号" prop="tenantId">
274
+          <el-input v-model="form.tenantId" placeholder="请输入租户号" />
275
+        </el-form-item>
276
+        <el-form-item label="乐观锁" prop="revision">
277
+          <el-input v-model="form.revision" placeholder="请输入乐观锁" />
278
+        </el-form-item>
279
+        <el-form-item label="查获记录ID" prop="recordId">
280
+          <el-input v-model="form.recordId" placeholder="请输入查获记录ID" />
281
+        </el-form-item>
282
+        <el-form-item label="物品编码" prop="itemCode">
283
+          <el-input v-model="form.itemCode" placeholder="请输入物品编码" />
284
+        </el-form-item>
285
+        <el-form-item label="数量" prop="quantity">
286
+          <el-input v-model="form.quantity" placeholder="请输入数量" />
287
+        </el-form-item>
288
+        <el-form-item label="图片URL" prop="imageUrl">
289
+          <el-input v-model="form.imageUrl" placeholder="请输入图片URL" />
290
+        </el-form-item>
291
+        <el-form-item label="备注" prop="remark">
292
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
293
+        </el-form-item>
294
+        <el-form-item label="检查部位编码1级" prop="checkPositionCodeOne">
295
+          <el-input v-model="form.checkPositionCodeOne" placeholder="请输入检查部位编码1级" />
296
+        </el-form-item>
297
+        <el-form-item label="是否故意隐匿(1-是,0-否)" prop="isActiveConcealment">
298
+          <el-input v-model="form.isActiveConcealment" placeholder="请输入是否故意隐匿(1-是,0-否)" />
299
+        </el-form-item>
300
+        <el-form-item label="检查部位编码2级" prop="checkPositionCodeTwo">
301
+          <el-input v-model="form.checkPositionCodeTwo" placeholder="请输入检查部位编码2级" />
302
+        </el-form-item>
303
+        <el-form-item label="具体位置" prop="checkPositionSpecific">
304
+          <el-input v-model="form.checkPositionSpecific" placeholder="请输入具体位置" />
305
+        </el-form-item>
306
+        <el-form-item label="物品名称" prop="itemName">
307
+          <el-input v-model="form.itemName" placeholder="请输入物品名称" />
308
+        </el-form-item>
309
+        <el-form-item label="物品分类编码1级" prop="categoryCodeOne">
310
+          <el-input v-model="form.categoryCodeOne" placeholder="请输入物品分类编码1级" />
311
+        </el-form-item>
312
+        <el-form-item label="物品分类名称1级" prop="categoryNameOne">
313
+          <el-input v-model="form.categoryNameOne" placeholder="请输入物品分类名称1级" />
314
+        </el-form-item>
315
+        <el-form-item label="物品分类编码2级" prop="categoryCodeTwo">
316
+          <el-input v-model="form.categoryCodeTwo" placeholder="请输入物品分类编码2级" />
317
+        </el-form-item>
318
+        <el-form-item label="物品分类名称2级" prop="categoryNameTwo">
319
+          <el-input v-model="form.categoryNameTwo" placeholder="请输入物品分类名称2级" />
320
+        </el-form-item>
321
+        <el-form-item label="单位" prop="unit">
322
+          <el-input v-model="form.unit" placeholder="请输入单位" />
323
+        </el-form-item>
324
+        <el-form-item label="检查部位名称1级" prop="checkPositionNameOne">
325
+          <el-input v-model="form.checkPositionNameOne" placeholder="请输入检查部位名称1级" />
326
+        </el-form-item>
327
+        <el-form-item label="检查部位名称2级" prop="checkPositionNameTwo">
328
+          <el-input v-model="form.checkPositionNameTwo" placeholder="请输入检查部位名称2级" />
329
+        </el-form-item>
330
+        <el-form-item label="单位名称" prop="unitDesc">
331
+          <el-input v-model="form.unitDesc" placeholder="请输入单位名称" />
332
+        </el-form-item>
333
+        <el-form-item label="处理方式" prop="handlingMethod">
334
+          <el-input v-model="form.handlingMethod" placeholder="请输入处理方式" />
335
+        </el-form-item>
336
+        <el-form-item label="处理方式名称" prop="handlingMethodDesc">
337
+          <el-input v-model="form.handlingMethodDesc" placeholder="请输入处理方式名称" />
338
+        </el-form-item>
339
+        <el-form-item label="图片ID" prop="imageId">
340
+          <el-input v-model="form.imageId" placeholder="请输入图片ID" />
341
+        </el-form-item>
342
+      </el-form>
343
+      <template #footer>
344
+        <div class="dialog-footer">
345
+          <el-button type="primary" @click="submitForm">确 定</el-button>
346
+          <el-button @click="cancel">取 消</el-button>
347
+        </div>
348
+      </template>
349
+    </el-dialog>
350
+  </div>
351
+</template>
352
+
353
+<script setup name="Items">
354
+import { listItems, getItems, delItems, addItems, updateItems } from "@/api/item/items"
355
+
356
+const { proxy } = getCurrentInstance()
357
+
358
+const itemsList = ref([])
359
+const open = ref(false)
360
+const loading = ref(true)
361
+const showSearch = ref(true)
362
+const ids = ref([])
363
+const single = ref(true)
364
+const multiple = ref(true)
365
+const total = ref(0)
366
+const title = ref("")
367
+
368
+const data = reactive({
369
+  form: {},
370
+  queryParams: {
371
+    pageNum: 1,
372
+    pageSize: 10,
373
+    tenantId: null,
374
+    revision: null,
375
+    recordId: null,
376
+    itemCode: null,
377
+    quantity: null,
378
+    imageUrl: null,
379
+    checkPositionCodeOne: null,
380
+    isActiveConcealment: null,
381
+    checkPositionCodeTwo: null,
382
+    checkPositionSpecific: null,
383
+    itemName: null,
384
+    categoryCodeOne: null,
385
+    categoryNameOne: null,
386
+    categoryCodeTwo: null,
387
+    categoryNameTwo: null,
388
+    unit: null,
389
+    checkPositionNameOne: null,
390
+    checkPositionNameTwo: null,
391
+    unitDesc: null,
392
+    handlingMethod: null,
393
+    handlingMethodDesc: null,
394
+    imageId: null
395
+  },
396
+  rules: {
397
+    recordId: [
398
+      { required: true, message: "查获记录ID不能为空", trigger: "blur" }
399
+    ],
400
+    itemCode: [
401
+      { required: true, message: "物品编码不能为空", trigger: "blur" }
402
+    ],
403
+    quantity: [
404
+      { required: true, message: "数量不能为空", trigger: "blur" }
405
+    ],
406
+    checkPositionCodeOne: [
407
+      { required: true, message: "检查部位编码1级不能为空", trigger: "blur" }
408
+    ],
409
+    isActiveConcealment: [
410
+      { required: true, message: "是否故意隐匿(1-是,0-否)不能为空", trigger: "blur" }
411
+    ],
412
+    checkPositionCodeTwo: [
413
+      { required: true, message: "检查部位编码2级不能为空", trigger: "blur" }
414
+    ],
415
+    checkPositionSpecific: [
416
+      { required: true, message: "具体位置不能为空", trigger: "blur" }
417
+    ],
418
+    itemName: [
419
+      { required: true, message: "物品名称不能为空", trigger: "blur" }
420
+    ],
421
+    categoryCodeOne: [
422
+      { required: true, message: "物品分类编码1级不能为空", trigger: "blur" }
423
+    ],
424
+    categoryNameOne: [
425
+      { required: true, message: "物品分类名称1级不能为空", trigger: "blur" }
426
+    ],
427
+    categoryCodeTwo: [
428
+      { required: true, message: "物品分类编码2级不能为空", trigger: "blur" }
429
+    ],
430
+    categoryNameTwo: [
431
+      { required: true, message: "物品分类名称2级不能为空", trigger: "blur" }
432
+    ],
433
+    unit: [
434
+      { required: true, message: "单位不能为空", trigger: "blur" }
435
+    ],
436
+    checkPositionNameOne: [
437
+      { required: true, message: "检查部位名称1级不能为空", trigger: "blur" }
438
+    ],
439
+    checkPositionNameTwo: [
440
+      { required: true, message: "检查部位名称2级不能为空", trigger: "blur" }
441
+    ],
442
+    unitDesc: [
443
+      { required: true, message: "单位名称不能为空", trigger: "blur" }
444
+    ],
445
+    handlingMethod: [
446
+      { required: true, message: "处理方式不能为空", trigger: "blur" }
447
+    ],
448
+    handlingMethodDesc: [
449
+      { required: true, message: "处理方式名称不能为空", trigger: "blur" }
450
+    ],
451
+  }
452
+})
453
+
454
+const { queryParams, form, rules } = toRefs(data)
455
+
456
+/** 查询查获物品明细列表 */
457
+function getList() {
458
+  loading.value = true
459
+  listItems(queryParams.value).then(response => {
460
+    itemsList.value = response.rows
461
+    total.value = response.total
462
+    loading.value = false
463
+  })
464
+}
465
+
466
+// 取消按钮
467
+function cancel() {
468
+  open.value = false
469
+  reset()
470
+}
471
+
472
+// 表单重置
473
+function reset() {
474
+  form.value = {
475
+    tenantId: null,
476
+    revision: null,
477
+    createBy: null,
478
+    createTime: null,
479
+    updateBy: null,
480
+    updateTime: null,
481
+    id: null,
482
+    recordId: null,
483
+    itemCode: null,
484
+    quantity: null,
485
+    imageUrl: null,
486
+    remark: null,
487
+    checkPositionCodeOne: null,
488
+    isActiveConcealment: null,
489
+    checkPositionCodeTwo: null,
490
+    checkPositionSpecific: null,
491
+    itemName: null,
492
+    categoryCodeOne: null,
493
+    categoryNameOne: null,
494
+    categoryCodeTwo: null,
495
+    categoryNameTwo: null,
496
+    unit: null,
497
+    checkPositionNameOne: null,
498
+    checkPositionNameTwo: null,
499
+    unitDesc: null,
500
+    handlingMethod: null,
501
+    handlingMethodDesc: null,
502
+    imageId: null
503
+  }
504
+  proxy.resetForm("itemsRef")
505
+}
506
+
507
+/** 搜索按钮操作 */
508
+function handleQuery() {
509
+  queryParams.value.pageNum = 1
510
+  getList()
511
+}
512
+
513
+/** 重置按钮操作 */
514
+function resetQuery() {
515
+  proxy.resetForm("queryRef")
516
+  handleQuery()
517
+}
518
+
519
+// 多选框选中数据
520
+function handleSelectionChange(selection) {
521
+  ids.value = selection.map(item => item.id)
522
+  single.value = selection.length != 1
523
+  multiple.value = !selection.length
524
+}
525
+
526
+/** 新增按钮操作 */
527
+function handleAdd() {
528
+  reset()
529
+  open.value = true
530
+  title.value = "添加查获物品明细"
531
+}
532
+
533
+/** 修改按钮操作 */
534
+function handleUpdate(row) {
535
+  reset()
536
+  const _id = row.id || ids.value
537
+  getItems(_id).then(response => {
538
+    form.value = response.data
539
+    open.value = true
540
+    title.value = "修改查获物品明细"
541
+  })
542
+}
543
+
544
+/** 提交按钮 */
545
+function submitForm() {
546
+  proxy.$refs["itemsRef"].validate(valid => {
547
+    if (valid) {
548
+      if (form.value.id != null) {
549
+        updateItems(form.value).then(response => {
550
+          proxy.$modal.msgSuccess("修改成功")
551
+          open.value = false
552
+          getList()
553
+        })
554
+      } else {
555
+        addItems(form.value).then(response => {
556
+          proxy.$modal.msgSuccess("新增成功")
557
+          open.value = false
558
+          getList()
559
+        })
560
+      }
561
+    }
562
+  })
563
+}
564
+
565
+/** 删除按钮操作 */
566
+function handleDelete(row) {
567
+  const _ids = row.id || ids.value
568
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
569
+    return delItems(_ids)
570
+  }).then(() => {
571
+    getList()
572
+    proxy.$modal.msgSuccess("删除成功")
573
+  }).catch(() => {})
574
+}
575
+
576
+/** 导出按钮操作 */
577
+function handleExport() {
578
+  proxy.download('item/items/export', {
579
+    ...queryParams.value
580
+  }, `items_${new Date().getTime()}.xlsx`)
581
+}
582
+
583
+getList()
584
+</script>

+ 430 - 0
src/views/item/record/index.vue

@@ -0,0 +1,430 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+
5
+      <el-form-item label="检查人员" prop="inspectUserName">
6
+        <el-input v-model="queryParams.inspectUserName" placeholder="请输入检查人员" clearable @keyup.enter="handleQuery" />
7
+      </el-form-item>
8
+
9
+      <el-form-item label="查获时间" prop="seizureTime">
10
+        <el-date-picker clearable v-model="queryParams.seizureTime" type="date" value-format="YYYY-MM-DD"
11
+          placeholder="请选择查获时间">
12
+        </el-date-picker>
13
+      </el-form-item>
14
+
15
+      <el-form-item label="安检位置" prop="regionalName">
16
+        <el-input v-model="queryParams.regionalName" placeholder="请输入安检位置" clearable @keyup.enter="handleQuery" />
17
+      </el-form-item>
18
+
19
+      <el-form-item label="查获班组" prop="inspectTeamName">
20
+        <el-input v-model="queryParams.inspectTeamName" placeholder="请输入查获班组" clearable @keyup.enter="handleQuery" />
21
+      </el-form-item>
22
+
23
+      <!-- <el-form-item label="违禁类型" prop="forbiddenTypeText">
24
+        <el-input v-model="queryParams.forbiddenTypeText" placeholder="请输入违禁品类型" clearable @keyup.enter="handleQuery" />
25
+      </el-form-item>
26
+
27
+      <el-form-item label="查获部位" prop="checkPositionSpecific">
28
+        <el-input v-model="queryParams.checkPositionSpecific" placeholder="请输入查获部位" clearable
29
+          @keyup.enter="handleQuery" />
30
+      </el-form-item>
31
+
32
+      <el-form-item label="处理方式" prop="handlingMethodDesc">
33
+        <el-input v-model="queryParams.handlingMethodDesc" placeholder="请输入处理方式" clearable @keyup.enter="handleQuery" />
34
+      </el-form-item>
35
+
36
+      <el-form-item label="是否有意隐匿" prop="regionalName">
37
+        <el-input v-model="queryParams.inspectTeamName" placeholder="请输入通道名称" clearable @keyup.enter="handleQuery" />
38
+      </el-form-item> -->
39
+
40
+
41
+
42
+      <el-form-item>
43
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
44
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
45
+      </el-form-item>
46
+    </el-form>
47
+
48
+    <el-row :gutter="10" class="mb8">
49
+      <!-- <el-col :span="1.5">
50
+        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['item:record:add']">新增</el-button>
51
+      </el-col>
52
+      <el-col :span="1.5">
53
+        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
54
+          v-hasPermi="['item:record:edit']">修改</el-button>
55
+      </el-col>
56
+      <el-col :span="1.5">
57
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
58
+          v-hasPermi="['item:record:remove']">删除</el-button>
59
+      </el-col> -->
60
+      <el-col :span="1.5">
61
+        <el-button type="warning" plain icon="Download" @click="handleExport"
62
+          v-hasPermi="['item:record:export']">导出</el-button>
63
+      </el-col>
64
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
65
+    </el-row>
66
+
67
+    <el-table v-loading="loading" :data="recordList" @selection-change="handleSelectionChange">
68
+      <el-table-column type="selection" width="55" align="center" />
69
+
70
+      <el-table-column label="安检员" align="center" prop="inspectUserName" />
71
+      <el-table-column label="查获时间" align="center" prop="seizureTime" width="180">
72
+        <template #default="scope">
73
+          <span>{{ parseTime(scope.row.seizureTime, '{y}-{m}-{d}') }}</span>
74
+        </template>
75
+      </el-table-column>
76
+      <el-table-column label="安检位置" align="center" prop="regionalName" />
77
+      <el-table-column label="安检岗位" align="center" prop="checkMethodDesc" />
78
+      <el-table-column label="查获班组" align="center" prop="inspectTeamName" />
79
+      <el-table-column label="上报班组" align="center" prop="attendanceTeamName" />
80
+      <!-- <el-table-column label="违禁品类型" align="center" prop="categoryNameTwo" />
81
+      <el-table-column label="数量" align="center" prop="quantity" />
82
+      <el-table-column label="查获部位" align="center" prop="customLocation" />
83
+      <el-table-column label="具体位置" align="center" prop="checkPositionSpecific" />
84
+      <el-table-column label="处理方式" align="center" prop="handlingMethodDesc" />
85
+      <el-table-column label="是否有意隐匿" align="center" prop="isActiveConcealment" />
86
+      <el-table-column label="旅客姓名" align="center" prop="passengerName" />
87
+      <el-table-column label="旅客身份证号" align="center" prop="passengerCard" />
88
+      <el-table-column label="航班号" align="center" prop="passengerFlight" /> -->
89
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
90
+        <template #default="scope">
91
+          <!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['item:record:edit']">修改</el-button>
92
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['item:record:remove']">删除</el-button> -->
93
+          <el-button link type="info" icon="View" @click="handleDetail(scope.row)"
94
+            v-hasPermi="['check:checkRecord:query']">详情</el-button>
95
+        </template>
96
+      </el-table-column>
97
+    </el-table>
98
+
99
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
100
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
101
+
102
+    <!-- 添加或修改查获记录对话框 -->
103
+    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
104
+      <el-form ref="recordRef" :model="form" :rules="rules" label-width="9em" disabled>
105
+        <el-form-item label="违禁品类别/类型" prop="channelCode">
106
+          <el-input :value="`${form.itemSeizureItemsList[0].categoryNameOne} / ${form.itemSeizureItemsList[0].categoryNameTwo}`" placeholder="- -" />
107
+        </el-form-item>
108
+        <!-- <el-form-item label="违禁品名称" prop="channelCode">
109
+          <el-input v-model="form.itemSeizureItemsList[0].itemName" placeholder="" />
110
+        </el-form-item> -->
111
+
112
+        <el-form-item label="数量" prop="channelCode">
113
+          <el-input v-model="form.itemSeizureItemsList[0].quantity" placeholder="-" />
114
+        </el-form-item>
115
+         <!-- <el-form-item label="查获部位" prop="channelCode">
116
+          <el-input v-model="form.itemSeizureItemsList[0].checkPositionNameOne" placeholder="" />
117
+        </el-form-item> -->
118
+           <el-form-item label="部位类别/类型" prop="channelCode">
119
+          <el-input :value="`${form.itemSeizureItemsList[0].checkPositionNameOne} / ${form.itemSeizureItemsList[0].checkPositionNameTwo}`" placeholder="- -" />
120
+        </el-form-item>
121
+        <!-- <el-form-item label="具体位置" prop="channelCode">
122
+          <el-input v-model="form.itemSeizureItemsList[0].checkPositionSpecific" placeholder="" />
123
+        </el-form-item> -->
124
+        <el-form-item label="处理方式" prop="channelCode">
125
+          <el-input v-model="form.itemSeizureItemsList[0].handlingMethodDesc" placeholder="-" />
126
+        </el-form-item>
127
+        <el-form-item label="是否隐匿夹带" prop="channelCode">
128
+          <el-select v-model="form.itemSeizureItemsList[0].isActiveConcealment"  placeholder="-">
129
+            <el-option label="否" :value="0" />
130
+            <el-option label="是" :value="1" />
131
+          </el-select>
132
+        </el-form-item>
133
+        <!-- <el-form-item label="旅客姓名" prop="passengerName">
134
+          <el-input v-model="form.passengerName" placeholder="请输入旅客姓名" />
135
+        </el-form-item>
136
+        <el-form-item label="旅客身份证号" prop="passengerCard">
137
+          <el-input v-model="form.passengerCard" placeholder="请输入旅客身份证号" />
138
+        </el-form-item>
139
+        <el-form-item label="航班号" prop="passengerFlight">
140
+          <el-input v-model="form.passengerFlight" placeholder="-" />
141
+        </el-form-item> -->
142
+        
143
+      </el-form>
144
+      <template #footer>
145
+        <div class="dialog-footer">
146
+          <!-- <el-button type="primary" @click="submitForm">确 定</el-button> -->
147
+          <el-button @click="cancel">取 消</el-button>
148
+        </div>
149
+      </template>
150
+    </el-dialog>
151
+  </div>
152
+</template>
153
+
154
+<script setup name="Record">
155
+import { listRecord, getRecord, delRecord, addRecord, updateRecord } from "@/api/item/record"
156
+import { onMounted } from 'vue'
157
+
158
+const { proxy } = getCurrentInstance()
159
+
160
+const recordList = ref([])
161
+const open = ref(false)
162
+const loading = ref(true)
163
+const showSearch = ref(true)
164
+const ids = ref([])
165
+const single = ref(true)
166
+const multiple = ref(true)
167
+const total = ref(0)
168
+const title = ref("")
169
+
170
+const data = reactive({
171
+  form: {},
172
+  queryParams: {
173
+    pageNum: 1,
174
+    pageSize: 10,
175
+    channelCode: null,
176
+    seizureTime: null,
177
+    checkMethod: null,
178
+    attendanceId: null,
179
+    attendanceTeamName: null,
180
+    attendanceDepartmentName: null,
181
+    attendanceStationName: null,
182
+    channelName: null,
183
+    regionalName: null,
184
+    terminlName: null,
185
+    passengerName: null,
186
+    passengerGender: null,
187
+    passengerCard: null,
188
+    passengerFlight: null,
189
+    inspectTeamId: null,
190
+    inspectTeamName: null,
191
+    inspectDepartmentId: null,
192
+    inspectDepartmentName: null,
193
+    inspectStationId: null,
194
+    inspectStationName: null,
195
+    checkMethodDesc: null,
196
+    passengerGenderDesc: null,
197
+    inspectUserName: null
198
+  },
199
+  rules: {
200
+    channelCode: [
201
+      { required: true, message: "通道编码不能为空", trigger: "blur" }
202
+    ],
203
+    seizureTime: [
204
+      { required: true, message: "查获时间不能为空", trigger: "blur" }
205
+    ],
206
+    checkMethod: [
207
+      { required: true, message: "检查岗位不能为空", trigger: "blur" }
208
+    ],
209
+    attendanceId: [
210
+      { required: true, message: "考勤记录ID不能为空", trigger: "blur" }
211
+    ],
212
+    attendanceTeamId: [
213
+      { required: true, message: "考勤班组ID不能为空", trigger: "blur" }
214
+    ],
215
+    attendanceTeamName: [
216
+      { required: true, message: "考勤班组名称不能为空", trigger: "blur" }
217
+    ],
218
+    attendanceDepartmentId: [
219
+      { required: true, message: "考勤科室ID不能为空", trigger: "blur" }
220
+    ],
221
+    attendanceDepartmentName: [
222
+      { required: true, message: "考勤科室名称不能为空", trigger: "blur" }
223
+    ],
224
+    attendanceStationId: [
225
+      { required: true, message: "考勤机构站ID不能为空", trigger: "blur" }
226
+    ],
227
+    attendanceStationName: [
228
+      { required: true, message: "考勤机构站名称不能为空", trigger: "blur" }
229
+    ],
230
+    channelName: [
231
+      { required: true, message: "通道名称不能为空", trigger: "blur" }
232
+    ],
233
+    regionalCode: [
234
+      { required: true, message: "区域编码不能为空", trigger: "blur" }
235
+    ],
236
+    regionalName: [
237
+      { required: true, message: "区域名称不能为空", trigger: "blur" }
238
+    ],
239
+    terminlCode: [
240
+      { required: true, message: "航站楼编码不能为空", trigger: "blur" }
241
+    ],
242
+    terminlName: [
243
+      { required: true, message: "航站楼名称不能为空", trigger: "blur" }
244
+    ],
245
+    inspectTeamId: [
246
+      { required: true, message: "查获人班组ID不能为空", trigger: "blur" }
247
+    ],
248
+    inspectTeamName: [
249
+      { required: true, message: "查获人班组名称不能为空", trigger: "blur" }
250
+    ],
251
+    inspectDepartmentId: [
252
+      { required: true, message: "查获人科室ID不能为空", trigger: "blur" }
253
+    ],
254
+    inspectDepartmentName: [
255
+      { required: true, message: "查获人科室名称不能为空", trigger: "blur" }
256
+    ],
257
+    inspectStationId: [
258
+      { required: true, message: "查获人机构站ID不能为空", trigger: "blur" }
259
+    ],
260
+    inspectStationName: [
261
+      { required: true, message: "查获人机构站名称不能为空", trigger: "blur" }
262
+    ],
263
+    checkMethodDesc: [
264
+      { required: true, message: "检查岗位名称不能为空", trigger: "blur" }
265
+    ],
266
+    passengerGenderDesc: [
267
+      { required: true, message: "旅客性别名称不能为空", trigger: "blur" }
268
+    ],
269
+    inspectUserName: [
270
+      { required: true, message: "检查人员名称不能为空", trigger: "blur" }
271
+    ]
272
+  }
273
+})
274
+
275
+const { queryParams, form, rules } = toRefs(data)
276
+
277
+/** 查询查获记录列表 */
278
+function getList() {
279
+  loading.value = true
280
+  listRecord(queryParams.value).then(response => {
281
+    recordList.value = response.rows
282
+    total.value = response.total
283
+    loading.value = false
284
+  })
285
+}
286
+
287
+// 取消按钮
288
+function cancel() {
289
+  open.value = false
290
+  reset()
291
+}
292
+
293
+// 表单重置
294
+function reset() {
295
+  form.value = {
296
+    tenantId: null,
297
+    revision: null,
298
+    createBy: null,
299
+    createTime: null,
300
+    updateBy: null,
301
+    updateTime: null,
302
+    inspectUserId: null,
303
+    channelCode: null,
304
+    seizureTime: null,
305
+    checkMethod: null,
306
+    remark: null,
307
+    attendanceId: null,
308
+    id: null,
309
+    attendanceTeamId: null,
310
+    attendanceTeamName: null,
311
+    attendanceDepartmentId: null,
312
+    attendanceDepartmentName: null,
313
+    attendanceStationId: null,
314
+    attendanceStationName: null,
315
+    channelName: null,
316
+    regionalCode: null,
317
+    regionalName: null,
318
+    terminlCode: null,
319
+    terminlName: null,
320
+    passengerName: null,
321
+    passengerGender: null,
322
+     itemSeizureItemsList: [{}],
323
+    passengerCard: null,
324
+    passengerFlight: null,
325
+    inspectTeamId: null,
326
+    inspectTeamName: null,
327
+    inspectDepartmentId: null,
328
+    inspectDepartmentName: null,
329
+    inspectStationId: null,
330
+    inspectStationName: null,
331
+    checkMethodDesc: null,
332
+    passengerGenderDesc: null,
333
+    inspectUserName: null
334
+  }
335
+  proxy.resetForm("recordRef")
336
+}
337
+
338
+/** 搜索按钮操作 */
339
+function handleQuery() {
340
+  queryParams.value.pageNum = 1
341
+  getList()
342
+}
343
+
344
+/** 重置按钮操作 */
345
+function resetQuery() {
346
+  proxy.resetForm("queryRef")
347
+  handleQuery()
348
+}
349
+
350
+// 多选框选中数据
351
+function handleSelectionChange(selection) {
352
+  ids.value = selection.map(item => item.id)
353
+  single.value = selection.length != 1
354
+  multiple.value = !selection.length
355
+}
356
+
357
+/** 新增按钮操作 */
358
+function handleAdd() {
359
+  reset()
360
+  open.value = true
361
+  title.value = "添加查获记录"
362
+}
363
+
364
+/** 修改按钮操作 */
365
+function handleUpdate(row) {
366
+  reset()
367
+  const _id = row.id || ids.value
368
+  getRecord(_id).then(response => {
369
+    form.value = response.data
370
+    open.value = true
371
+    title.value = "修改查获记录"
372
+  })
373
+}
374
+
375
+/** 查看详情 */
376
+function handleDetail(row) {
377
+  reset()
378
+  const _id = row.id || ids.value
379
+  getRecord(_id).then(response => {
380
+    form.value = response.data
381
+    open.value = true
382
+    title.value = "详情"
383
+  })
384
+}
385
+
386
+
387
+
388
+/** 提交按钮 */
389
+function submitForm() {
390
+  proxy.$refs["recordRef"].validate(valid => {
391
+    if (valid) {
392
+      if (form.value.id != null) {
393
+        updateRecord(form.value).then(response => {
394
+          proxy.$modal.msgSuccess("修改成功")
395
+          open.value = false
396
+          getList()
397
+        })
398
+      } else {
399
+        addRecord(form.value).then(response => {
400
+          proxy.$modal.msgSuccess("新增成功")
401
+          open.value = false
402
+          getList()
403
+        })
404
+      }
405
+    }
406
+  })
407
+}
408
+
409
+/** 删除按钮操作 */
410
+function handleDelete(row) {
411
+  const _ids = row.id || ids.value
412
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
413
+    return delRecord(_ids)
414
+  }).then(() => {
415
+    getList()
416
+    proxy.$modal.msgSuccess("删除成功")
417
+  }).catch(() => { })
418
+}
419
+
420
+/** 导出按钮操作 */
421
+function handleExport() {
422
+  proxy.download('item/record/export', {
423
+    ...queryParams.value
424
+  }, `record_${new Date().getTime()}.xlsx`)
425
+}
426
+
427
+onMounted(() => {
428
+  getList()
429
+})
430
+</script>

+ 229 - 0
src/views/login.vue

@@ -0,0 +1,229 @@
1
+<template>
2
+  <div class="login">
3
+    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
4
+      <h3 class="title">{{ title }}</h3>
5
+      <el-form-item prop="username">
6
+        <el-input
7
+          v-model="loginForm.username"
8
+          type="text"
9
+          size="large"
10
+          auto-complete="off"
11
+          placeholder="账号"
12
+        >
13
+          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
14
+        </el-input>
15
+      </el-form-item>
16
+      <el-form-item prop="password">
17
+        <el-input
18
+          v-model="loginForm.password"
19
+          type="password"
20
+          size="large"
21
+          auto-complete="off"
22
+          placeholder="密码"
23
+          @keyup.enter="handleLogin"
24
+        >
25
+          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
26
+        </el-input>
27
+      </el-form-item>
28
+      <el-form-item prop="code" v-if="captchaEnabled">
29
+        <el-input
30
+          v-model="loginForm.code"
31
+          size="large"
32
+          auto-complete="off"
33
+          placeholder="验证码"
34
+          style="width: 63%"
35
+          @keyup.enter="handleLogin"
36
+        >
37
+          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
38
+        </el-input>
39
+        <div class="login-code">
40
+          <img :src="codeUrl" @click="getCode" class="login-code-img"/>
41
+        </div>
42
+      </el-form-item>
43
+      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
44
+      <el-form-item style="width:100%;">
45
+        <el-button
46
+          :loading="loading"
47
+          size="large"
48
+          type="primary"
49
+          style="width:100%;"
50
+          @click.prevent="handleLogin"
51
+        >
52
+          <span v-if="!loading">登 录</span>
53
+          <span v-else>登 录 中...</span>
54
+        </el-button>
55
+        <div style="float: right;" v-if="register">
56
+          <router-link class="link-type" :to="'/register'">立即注册</router-link>
57
+        </div>
58
+      </el-form-item>
59
+    </el-form>
60
+    <!--  底部  -->
61
+    <div class="el-login-footer">
62
+      <span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
63
+    </div>
64
+  </div>
65
+</template>
66
+
67
+<script setup>
68
+import { getCodeImg } from "@/api/login"
69
+import Cookies from "js-cookie"
70
+import { encrypt, decrypt } from "@/utils/jsencrypt"
71
+import useUserStore from '@/store/modules/user'
72
+
73
+const title = import.meta.env.VITE_APP_TITLE
74
+const userStore = useUserStore()
75
+const route = useRoute()
76
+const router = useRouter()
77
+const { proxy } = getCurrentInstance()
78
+
79
+const loginForm = ref({
80
+  username: "admin",
81
+  password: "admin123",
82
+  rememberMe: false,
83
+  code: "",
84
+  uuid: ""
85
+})
86
+
87
+const loginRules = {
88
+  username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
89
+  password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
90
+  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
91
+}
92
+
93
+const codeUrl = ref("")
94
+const loading = ref(false)
95
+// 验证码开关
96
+const captchaEnabled = ref(true)
97
+// 注册开关
98
+const register = ref(false)
99
+const redirect = ref(undefined)
100
+
101
+watch(route, (newRoute) => {
102
+    redirect.value = newRoute.query && newRoute.query.redirect
103
+}, { immediate: true })
104
+
105
+function handleLogin() {
106
+  proxy.$refs.loginRef.validate(valid => {
107
+    if (valid) {
108
+      loading.value = true
109
+      // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
110
+      if (loginForm.value.rememberMe) {
111
+        Cookies.set("username", loginForm.value.username, { expires: 30 })
112
+        Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
113
+        Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })
114
+      } else {
115
+        // 否则移除
116
+        Cookies.remove("username")
117
+        Cookies.remove("password")
118
+        Cookies.remove("rememberMe")
119
+      }
120
+      // 调用action的登录方法
121
+      userStore.login(loginForm.value).then(() => {
122
+        const query = route.query
123
+        const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
124
+          if (cur !== "redirect") {
125
+            acc[cur] = query[cur]
126
+          }
127
+          return acc
128
+        }, {})
129
+        router.push({ path: redirect.value || "/", query: otherQueryParams })
130
+      }).catch(() => {
131
+        loading.value = false
132
+        // 重新获取验证码
133
+        if (captchaEnabled.value) {
134
+          getCode()
135
+        }
136
+      })
137
+    }
138
+  })
139
+}
140
+
141
+function getCode() {
142
+  getCodeImg().then(res => {
143
+    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
144
+    if (captchaEnabled.value) {
145
+      codeUrl.value = "data:image/gif;base64," + res.img
146
+      loginForm.value.uuid = res.uuid
147
+    }
148
+  })
149
+}
150
+
151
+function getCookie() {
152
+  const username = Cookies.get("username")
153
+  const password = Cookies.get("password")
154
+  const rememberMe = Cookies.get("rememberMe")
155
+  loginForm.value = {
156
+    username: username === undefined ? loginForm.value.username : username,
157
+    password: password === undefined ? loginForm.value.password : decrypt(password),
158
+    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
159
+  }
160
+}
161
+
162
+getCode()
163
+getCookie()
164
+</script>
165
+
166
+<style lang='scss' scoped>
167
+.login {
168
+  display: flex;
169
+  justify-content: center;
170
+  align-items: center;
171
+  height: 100%;
172
+  background-image: url("../assets/images/login-background.jpg");
173
+  background-size: cover;
174
+}
175
+.title {
176
+  margin: 0px auto 30px auto;
177
+  text-align: center;
178
+  color: #707070;
179
+}
180
+
181
+.login-form {
182
+  border-radius: 6px;
183
+  background: #ffffff;
184
+  width: 400px;
185
+  padding: 25px 25px 5px 25px;
186
+  z-index: 1;
187
+  .el-input {
188
+    height: 40px;
189
+    input {
190
+      height: 40px;
191
+    }
192
+  }
193
+  .input-icon {
194
+    height: 39px;
195
+    width: 14px;
196
+    margin-left: 0px;
197
+  }
198
+}
199
+.login-tip {
200
+  font-size: 13px;
201
+  text-align: center;
202
+  color: #bfbfbf;
203
+}
204
+.login-code {
205
+  width: 33%;
206
+  height: 40px;
207
+  float: right;
208
+  img {
209
+    cursor: pointer;
210
+    vertical-align: middle;
211
+  }
212
+}
213
+.el-login-footer {
214
+  height: 40px;
215
+  line-height: 40px;
216
+  position: fixed;
217
+  bottom: 0;
218
+  width: 100%;
219
+  text-align: center;
220
+  color: #fff;
221
+  font-family: Arial;
222
+  font-size: 12px;
223
+  letter-spacing: 1px;
224
+}
225
+.login-code-img {
226
+  height: 40px;
227
+  padding-left: 12px;
228
+}
229
+</style>

+ 132 - 0
src/views/monitor/cache/index.vue

@@ -0,0 +1,132 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-row :gutter="10">
4
+      <el-col :span="24" class="card-box">
5
+        <el-card>
6
+          <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">基本信息</span></template>
7
+          <div class="el-table el-table--enable-row-hover el-table--medium">
8
+            <table cellspacing="0" style="width: 100%">
9
+              <tbody>
10
+                <tr>
11
+                  <td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
12
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
13
+                  <td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
14
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
15
+                  <td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
16
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
17
+                  <td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
18
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
19
+                </tr>
20
+                <tr>
21
+                  <td class="el-table__cell is-leaf"><div class="cell">运行时间(天)</div></td>
22
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
23
+                  <td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
24
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
25
+                  <td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
26
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
27
+                  <td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
28
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
29
+                </tr>
30
+                <tr>
31
+                  <td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
32
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
33
+                  <td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
34
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
35
+                  <td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
36
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
37
+                  <td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
38
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
39
+                </tr>
40
+              </tbody>
41
+            </table>
42
+          </div>
43
+        </el-card>
44
+      </el-col>
45
+
46
+      <el-col :span="12" class="card-box">
47
+        <el-card>
48
+          <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">命令统计</span></template>
49
+          <div class="el-table el-table--enable-row-hover el-table--medium">
50
+            <div ref="commandstats" style="height: 420px" />
51
+          </div>
52
+        </el-card>
53
+      </el-col>
54
+
55
+      <el-col :span="12" class="card-box">
56
+        <el-card>
57
+          <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span></template>
58
+          <div class="el-table el-table--enable-row-hover el-table--medium">
59
+            <div ref="usedmemory" style="height: 420px" />
60
+          </div>
61
+        </el-card>
62
+      </el-col>
63
+    </el-row>
64
+  </div>
65
+</template>
66
+
67
+<script setup name="Cache">
68
+import { getCache } from '@/api/monitor/cache'
69
+import * as echarts from 'echarts'
70
+
71
+const cache = ref([])
72
+const commandstats = ref(null)
73
+const usedmemory = ref(null)
74
+const { proxy } = getCurrentInstance()
75
+
76
+function getList() {
77
+  proxy.$modal.loading("正在加载缓存监控数据,请稍候!")
78
+  getCache().then(response => {
79
+    proxy.$modal.closeLoading()
80
+    cache.value = response.data
81
+
82
+    const commandstatsIntance = echarts.init(commandstats.value, "macarons")
83
+    commandstatsIntance.setOption({
84
+      tooltip: {
85
+        trigger: "item",
86
+        formatter: "{a} <br/>{b} : {c} ({d}%)"
87
+      },
88
+      series: [
89
+        {
90
+          name: "命令",
91
+          type: "pie",
92
+          roseType: "radius",
93
+          radius: [15, 95],
94
+          center: ["50%", "38%"],
95
+          data: response.data.commandStats,
96
+          animationEasing: "cubicInOut",
97
+          animationDuration: 1000
98
+        }
99
+      ]
100
+    })
101
+    const usedmemoryInstance = echarts.init(usedmemory.value, "macarons")
102
+    usedmemoryInstance.setOption({
103
+      tooltip: {
104
+        formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
105
+      },
106
+      series: [
107
+        {
108
+          name: "峰值",
109
+          type: "gauge",
110
+          min: 0,
111
+          max: 1000,
112
+          detail: {
113
+            formatter: cache.value.info.used_memory_human
114
+          },
115
+          data: [
116
+            {
117
+              value: parseFloat(cache.value.info.used_memory_human),
118
+              name: "内存消耗"
119
+            }
120
+          ]
121
+        }
122
+      ]
123
+    })
124
+    window.addEventListener("resize", () => {
125
+      commandstatsIntance.resize()
126
+      usedmemoryInstance.resize()
127
+    })
128
+  })
129
+}
130
+
131
+getList()
132
+</script>

+ 246 - 0
src/views/monitor/cache/list.vue

@@ -0,0 +1,246 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-row :gutter="10">
4
+      <el-col :span="8">
5
+        <el-card style="height: calc(100vh - 125px)">
6
+          <template #header>
7
+            <Collection style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">缓存列表</span>
8
+            <el-button
9
+              style="float: right; padding: 3px 0"
10
+              link
11
+              type="primary"
12
+              icon="Refresh"
13
+              @click="refreshCacheNames()"
14
+            ></el-button>
15
+          </template>
16
+          <el-table
17
+            v-loading="loading"
18
+            :data="cacheNames"
19
+            :height="tableHeight"
20
+            highlight-current-row
21
+            @row-click="getCacheKeys"
22
+            style="width: 100%"
23
+          >
24
+            <el-table-column
25
+              label="序号"
26
+              width="60"
27
+              type="index"
28
+            ></el-table-column>
29
+
30
+            <el-table-column
31
+              label="缓存名称"
32
+              align="center"
33
+              prop="cacheName"
34
+              :show-overflow-tooltip="true"
35
+              :formatter="nameFormatter"
36
+            ></el-table-column>
37
+
38
+            <el-table-column
39
+              label="备注"
40
+              align="center"
41
+              prop="remark"
42
+              :show-overflow-tooltip="true"
43
+            />
44
+            <el-table-column
45
+              label="操作"
46
+              width="60"
47
+              align="center"
48
+              class-name="small-padding fixed-width"
49
+            >
50
+              <template #default="scope">
51
+                <el-button
52
+                  link
53
+                  type="primary"
54
+                  icon="Delete"
55
+                  @click="handleClearCacheName(scope.row)"
56
+                ></el-button>
57
+              </template>
58
+            </el-table-column>
59
+          </el-table>
60
+        </el-card>
61
+      </el-col>
62
+
63
+      <el-col :span="8">
64
+        <el-card style="height: calc(100vh - 125px)">
65
+          <template #header>
66
+            <Key style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">键名列表</span>
67
+            <el-button
68
+              style="float: right; padding: 3px 0"
69
+              link
70
+              type="primary"
71
+              icon="Refresh"
72
+              @click="refreshCacheKeys()"
73
+            ></el-button>
74
+          </template>
75
+          <el-table
76
+            v-loading="subLoading"
77
+            :data="cacheKeys"
78
+            :height="tableHeight"
79
+            highlight-current-row
80
+            @row-click="handleCacheValue"
81
+            style="width: 100%"
82
+          >
83
+            <el-table-column
84
+              label="序号"
85
+              width="60"
86
+              type="index"
87
+            ></el-table-column>
88
+            <el-table-column
89
+              label="缓存键名"
90
+              align="center"
91
+              :show-overflow-tooltip="true"
92
+              :formatter="keyFormatter"
93
+            >
94
+            </el-table-column>
95
+            <el-table-column
96
+              label="操作"
97
+              width="60"
98
+              align="center"
99
+              class-name="small-padding fixed-width"
100
+            >
101
+              <template #default="scope">
102
+                <el-button
103
+                  link
104
+                  type="primary"
105
+                  icon="Delete"
106
+                  @click="handleClearCacheKey(scope.row)"
107
+                ></el-button>
108
+              </template>
109
+            </el-table-column>
110
+          </el-table>
111
+        </el-card>
112
+      </el-col>
113
+
114
+      <el-col :span="8">
115
+        <el-card :bordered="false" style="height: calc(100vh - 125px)">
116
+          <template #header>
117
+            <Document style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">缓存内容</span>
118
+            <el-button
119
+              style="float: right; padding: 3px 0"
120
+              link
121
+              type="primary"
122
+              icon="Refresh"
123
+              @click="handleClearCacheAll()"
124
+              >清理全部</el-button
125
+            >
126
+          </template>
127
+          <el-form :model="cacheForm">
128
+            <el-row :gutter="32">
129
+              <el-col :offset="1" :span="22">
130
+                <el-form-item label="缓存名称:" prop="cacheName">
131
+                  <el-input v-model="cacheForm.cacheName" :readOnly="true" />
132
+                </el-form-item>
133
+              </el-col>
134
+              <el-col :offset="1" :span="22">
135
+                <el-form-item label="缓存键名:" prop="cacheKey">
136
+                  <el-input v-model="cacheForm.cacheKey" :readOnly="true" />
137
+                </el-form-item>
138
+              </el-col>
139
+              <el-col :offset="1" :span="22">
140
+                <el-form-item label="缓存内容:" prop="cacheValue">
141
+                  <el-input
142
+                    v-model="cacheForm.cacheValue"
143
+                    type="textarea"
144
+                    :rows="8"
145
+                    :readOnly="true"
146
+                  />
147
+                </el-form-item>
148
+              </el-col>
149
+            </el-row>
150
+          </el-form>
151
+        </el-card>
152
+      </el-col>
153
+    </el-row>
154
+  </div>
155
+</template>
156
+
157
+<script setup name="CacheList">
158
+import { listCacheName, listCacheKey, getCacheValue, clearCacheName, clearCacheKey, clearCacheAll } from "@/api/monitor/cache"
159
+
160
+const { proxy } = getCurrentInstance()
161
+
162
+const cacheNames = ref([])
163
+const cacheKeys = ref([])
164
+const cacheForm = ref({})
165
+const loading = ref(true)
166
+const subLoading = ref(false)
167
+const nowCacheName = ref("")
168
+const tableHeight = ref(window.innerHeight - 200)
169
+
170
+/** 查询缓存名称列表 */
171
+function getCacheNames() {
172
+  loading.value = true
173
+  listCacheName().then(response => {
174
+    cacheNames.value = response.data
175
+    loading.value = false
176
+  })
177
+}
178
+
179
+/** 刷新缓存名称列表 */
180
+function refreshCacheNames() {
181
+  getCacheNames()
182
+  proxy.$modal.msgSuccess("刷新缓存列表成功")
183
+}
184
+
185
+/** 清理指定名称缓存 */
186
+function handleClearCacheName(row) {
187
+  clearCacheName(row.cacheName).then(response => {
188
+    proxy.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
189
+    getCacheKeys()
190
+  })
191
+}
192
+
193
+/** 查询缓存键名列表 */
194
+function getCacheKeys(row) {
195
+  const cacheName = row !== undefined ? row.cacheName : nowCacheName.value
196
+  if (cacheName === "") {
197
+    return
198
+  }
199
+  subLoading.value = true
200
+  listCacheKey(cacheName).then(response => {
201
+    cacheKeys.value = response.data
202
+    subLoading.value = false
203
+    nowCacheName.value = cacheName
204
+  })
205
+}
206
+
207
+/** 刷新缓存键名列表 */
208
+function refreshCacheKeys() {
209
+  getCacheKeys()
210
+  proxy.$modal.msgSuccess("刷新键名列表成功")
211
+}
212
+
213
+/** 清理指定键名缓存 */
214
+function handleClearCacheKey(cacheKey) {
215
+  clearCacheKey(cacheKey).then(response => {
216
+    proxy.$modal.msgSuccess("清理缓存键名[" + cacheKey + "]成功")
217
+    getCacheKeys()
218
+  })
219
+}
220
+
221
+/** 列表前缀去除 */
222
+function nameFormatter(row) {
223
+  return row.cacheName.replace(":", "")
224
+}
225
+
226
+/** 键名前缀去除 */
227
+function keyFormatter(cacheKey) {
228
+  return cacheKey.replace(nowCacheName.value, "")
229
+}
230
+
231
+/** 查询缓存内容详细 */
232
+function handleCacheValue(cacheKey) {
233
+  getCacheValue(nowCacheName.value, cacheKey).then(response => {
234
+    cacheForm.value = response.data
235
+  })
236
+}
237
+
238
+/** 清理全部缓存 */
239
+function handleClearCacheAll() {
240
+  clearCacheAll().then(response => {
241
+    proxy.$modal.msgSuccess("清理全部缓存成功")
242
+  })
243
+}
244
+
245
+getCacheNames()
246
+</script>

+ 13 - 0
src/views/monitor/druid/index.vue

@@ -0,0 +1,13 @@
1
+<template>
2
+   <div>
3
+      <i-frame v-model:src="url"></i-frame>
4
+   </div>
5
+</template>
6
+
7
+<script setup>
8
+import iFrame from '@/components/iFrame'
9
+
10
+import { ref } from 'vue'
11
+
12
+const url = ref(import.meta.env.VITE_APP_BASE_API + '/druid/login.html')
13
+</script>

+ 502 - 0
src/views/monitor/job/index.vue

@@ -0,0 +1,502 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+         <el-form-item label="任务名称" prop="jobName">
5
+            <el-input
6
+               v-model="queryParams.jobName"
7
+               placeholder="请输入任务名称"
8
+               clearable
9
+               style="width: 200px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="任务组名" prop="jobGroup">
14
+            <el-select v-model="queryParams.jobGroup" placeholder="请选择任务组名" clearable style="width: 200px">
15
+               <el-option
16
+                  v-for="dict in sys_job_group"
17
+                  :key="dict.value"
18
+                  :label="dict.label"
19
+                  :value="dict.value"
20
+               />
21
+            </el-select>
22
+         </el-form-item>
23
+         <el-form-item label="任务状态" prop="status">
24
+            <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable style="width: 200px">
25
+               <el-option
26
+                  v-for="dict in sys_job_status"
27
+                  :key="dict.value"
28
+                  :label="dict.label"
29
+                  :value="dict.value"
30
+               />
31
+            </el-select>
32
+         </el-form-item>
33
+         <el-form-item>
34
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
35
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
36
+         </el-form-item>
37
+      </el-form>
38
+
39
+      <el-row :gutter="10" class="mb8">
40
+         <el-col :span="1.5">
41
+            <el-button
42
+               type="primary"
43
+               plain
44
+               icon="Plus"
45
+               @click="handleAdd"
46
+               v-hasPermi="['monitor:job:add']"
47
+            >新增</el-button>
48
+         </el-col>
49
+         <el-col :span="1.5">
50
+            <el-button
51
+               type="success"
52
+               plain
53
+               icon="Edit"
54
+               :disabled="single"
55
+               @click="handleUpdate"
56
+               v-hasPermi="['monitor:job:edit']"
57
+            >修改</el-button>
58
+         </el-col>
59
+         <el-col :span="1.5">
60
+            <el-button
61
+               type="danger"
62
+               plain
63
+               icon="Delete"
64
+               :disabled="multiple"
65
+               @click="handleDelete"
66
+               v-hasPermi="['monitor:job:remove']"
67
+            >删除</el-button>
68
+         </el-col>
69
+         <el-col :span="1.5">
70
+            <el-button
71
+               type="warning"
72
+               plain
73
+               icon="Download"
74
+               @click="handleExport"
75
+               v-hasPermi="['monitor:job:export']"
76
+            >导出</el-button>
77
+         </el-col>
78
+         <el-col :span="1.5">
79
+            <el-button
80
+               type="info"
81
+               plain
82
+               icon="Operation"
83
+               @click="handleJobLog"
84
+               v-hasPermi="['monitor:job:query']"
85
+            >日志</el-button>
86
+         </el-col>
87
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
88
+      </el-row>
89
+
90
+      <el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
91
+         <el-table-column type="selection" width="55" align="center" />
92
+         <el-table-column label="任务编号" width="100" align="center" prop="jobId" />
93
+         <el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
94
+         <el-table-column label="任务组名" align="center" prop="jobGroup">
95
+            <template #default="scope">
96
+               <dict-tag :options="sys_job_group" :value="scope.row.jobGroup" />
97
+            </template>
98
+         </el-table-column>
99
+         <el-table-column label="调用目标字符串" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
100
+         <el-table-column label="cron执行表达式" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
101
+         <el-table-column label="状态" align="center">
102
+            <template #default="scope">
103
+               <el-switch
104
+                  v-model="scope.row.status"
105
+                  active-value="0"
106
+                  inactive-value="1"
107
+                  @change="handleStatusChange(scope.row)"
108
+               ></el-switch>
109
+            </template>
110
+         </el-table-column>
111
+         <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
112
+            <template #default="scope">
113
+               <el-tooltip content="修改" placement="top">
114
+                  <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['monitor:job:edit']"></el-button>
115
+               </el-tooltip>
116
+               <el-tooltip content="删除" placement="top">
117
+                  <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['monitor:job:remove']"></el-button>
118
+               </el-tooltip>
119
+               <el-tooltip content="执行一次" placement="top">
120
+                  <el-button link type="primary" icon="CaretRight" @click="handleRun(scope.row)" v-hasPermi="['monitor:job:changeStatus']"></el-button>
121
+               </el-tooltip>
122
+               <el-tooltip content="任务详细" placement="top">
123
+                  <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:job:query']"></el-button>
124
+               </el-tooltip>
125
+               <el-tooltip content="调度日志" placement="top">
126
+                  <el-button link type="primary" icon="Operation" @click="handleJobLog(scope.row)" v-hasPermi="['monitor:job:query']"></el-button>
127
+               </el-tooltip>
128
+            </template>
129
+         </el-table-column>
130
+      </el-table>
131
+
132
+      <pagination
133
+         v-show="total > 0"
134
+         :total="total"
135
+         v-model:page="queryParams.pageNum"
136
+         v-model:limit="queryParams.pageSize"
137
+         @pagination="getList"
138
+      />
139
+
140
+      <!-- 添加或修改定时任务对话框 -->
141
+      <el-dialog :title="title" v-model="open" width="820px" append-to-body>
142
+         <el-form ref="jobRef" :model="form" :rules="rules" label-width="120px">
143
+            <el-row>
144
+               <el-col :span="12">
145
+                  <el-form-item label="任务名称" prop="jobName">
146
+                     <el-input v-model="form.jobName" placeholder="请输入任务名称" />
147
+                  </el-form-item>
148
+               </el-col>
149
+               <el-col :span="12">
150
+                  <el-form-item label="任务分组" prop="jobGroup">
151
+                     <el-select v-model="form.jobGroup" placeholder="请选择">
152
+                        <el-option
153
+                           v-for="dict in sys_job_group"
154
+                           :key="dict.value"
155
+                           :label="dict.label"
156
+                           :value="dict.value"
157
+                        ></el-option>
158
+                     </el-select>
159
+                  </el-form-item>
160
+               </el-col>
161
+               <el-col :span="24">
162
+                  <el-form-item prop="invokeTarget">
163
+                     <template #label>
164
+                        <span>
165
+                           调用方法
166
+                           <el-tooltip placement="top">
167
+                              <template #content>
168
+                                 <div>
169
+                                    Bean调用示例:ryTask.ryParams('ry')
170
+                                    <br />Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
171
+                                    <br />参数说明:支持字符串,布尔类型,长整型,浮点型,整型
172
+                                 </div>
173
+                              </template>
174
+                              <el-icon><question-filled /></el-icon>
175
+                           </el-tooltip>
176
+                        </span>
177
+                     </template>
178
+                     <el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串" />
179
+                  </el-form-item>
180
+               </el-col>
181
+               <el-col :span="24">
182
+                  <el-form-item label="cron表达式" prop="cronExpression">
183
+                     <el-input v-model="form.cronExpression" placeholder="请输入cron执行表达式">
184
+                        <template #append>
185
+                           <el-button type="primary" @click="handleShowCron">
186
+                              生成表达式
187
+                              <i class="el-icon-time el-icon--right"></i>
188
+                           </el-button>
189
+                        </template>
190
+                     </el-input>
191
+                  </el-form-item>
192
+               </el-col>
193
+               <el-col :span="24" v-if="form.jobId !== undefined">
194
+                  <el-form-item label="状态">
195
+                     <el-radio-group v-model="form.status">
196
+                        <el-radio
197
+                           v-for="dict in sys_job_status"
198
+                           :key="dict.value"
199
+                           :value="dict.value"
200
+                        >{{ dict.label }}</el-radio>
201
+                     </el-radio-group>
202
+                  </el-form-item>
203
+               </el-col>
204
+               <el-col :span="12">
205
+                  <el-form-item label="执行策略" prop="misfirePolicy">
206
+                     <el-radio-group v-model="form.misfirePolicy">
207
+                        <el-radio-button value="1">立即执行</el-radio-button>
208
+                        <el-radio-button value="2">执行一次</el-radio-button>
209
+                        <el-radio-button value="3">放弃执行</el-radio-button>
210
+                     </el-radio-group>
211
+                  </el-form-item>
212
+               </el-col>
213
+               <el-col :span="12">
214
+                  <el-form-item label="是否并发" prop="concurrent">
215
+                     <el-radio-group v-model="form.concurrent">
216
+                        <el-radio-button value="0">允许</el-radio-button>
217
+                        <el-radio-button value="1">禁止</el-radio-button>
218
+                     </el-radio-group>
219
+                  </el-form-item>
220
+               </el-col>
221
+            </el-row>
222
+         </el-form>
223
+         <template #footer>
224
+            <div class="dialog-footer">
225
+               <el-button type="primary" @click="submitForm">确 定</el-button>
226
+               <el-button @click="cancel">取 消</el-button>
227
+            </div>
228
+         </template>
229
+      </el-dialog>
230
+
231
+     <el-dialog title="Cron表达式生成器" v-model="openCron" append-to-body destroy-on-close>
232
+       <crontab ref="crontabRef" @hide="openCron=false" @fill="crontabFill" :expression="expression"></crontab>
233
+     </el-dialog>
234
+
235
+      <!-- 任务日志详细 -->
236
+      <el-dialog title="任务详细" v-model="openView" width="700px" append-to-body>
237
+         <el-form :model="form" label-width="120px">
238
+            <el-row>
239
+               <el-col :span="12">
240
+                  <el-form-item label="任务编号:">{{ form.jobId }}</el-form-item>
241
+                  <el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
242
+               </el-col>
243
+               <el-col :span="12">
244
+                  <el-form-item label="任务分组:">{{ jobGroupFormat(form) }}</el-form-item>
245
+                  <el-form-item label="创建时间:">{{ form.createTime }}</el-form-item>
246
+               </el-col>
247
+               <el-col :span="12">
248
+                  <el-form-item label="cron表达式:">{{ form.cronExpression }}</el-form-item>
249
+               </el-col>
250
+               <el-col :span="12">
251
+                  <el-form-item label="下次执行时间:">{{ parseTime(form.nextValidTime) }}</el-form-item>
252
+               </el-col>
253
+               <el-col :span="24">
254
+                  <el-form-item label="调用目标方法:">{{ form.invokeTarget }}</el-form-item>
255
+               </el-col>
256
+               <el-col :span="12">
257
+                  <el-form-item label="任务状态:">
258
+                     <div v-if="form.status == 0">正常</div>
259
+                     <div v-else-if="form.status == 1">暂停</div>
260
+                  </el-form-item>
261
+               </el-col>
262
+               <el-col :span="12">
263
+                  <el-form-item label="是否并发:">
264
+                     <div v-if="form.concurrent == 0">允许</div>
265
+                     <div v-else-if="form.concurrent == 1">禁止</div>
266
+                  </el-form-item>
267
+               </el-col>
268
+               <el-col :span="12">
269
+                  <el-form-item label="执行策略:">
270
+                     <div v-if="form.misfirePolicy == 0">默认策略</div>
271
+                     <div v-else-if="form.misfirePolicy == 1">立即执行</div>
272
+                     <div v-else-if="form.misfirePolicy == 2">执行一次</div>
273
+                     <div v-else-if="form.misfirePolicy == 3">放弃执行</div>
274
+                  </el-form-item>
275
+               </el-col>
276
+            </el-row>
277
+         </el-form>
278
+         <template #footer>
279
+            <div class="dialog-footer">
280
+               <el-button @click="openView = false">关 闭</el-button>
281
+            </div>
282
+         </template>
283
+      </el-dialog>
284
+   </div>
285
+</template>
286
+
287
+<script setup name="Job">
288
+import Crontab from '@/components/Crontab'
289
+import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
290
+
291
+const router = useRouter()
292
+const { proxy } = getCurrentInstance()
293
+const { sys_job_group, sys_job_status } = proxy.useDict("sys_job_group", "sys_job_status")
294
+
295
+const jobList = ref([])
296
+const open = ref(false)
297
+const loading = ref(true)
298
+const showSearch = ref(true)
299
+const ids = ref([])
300
+const single = ref(true)
301
+const multiple = ref(true)
302
+const total = ref(0)
303
+const title = ref("")
304
+const openView = ref(false)
305
+const openCron = ref(false)
306
+const expression = ref("")
307
+
308
+const data = reactive({
309
+  form: {},
310
+  queryParams: {
311
+    pageNum: 1,
312
+    pageSize: 10,
313
+    jobName: undefined,
314
+    jobGroup: undefined,
315
+    status: undefined
316
+  },
317
+  rules: {
318
+    jobName: [{ required: true, message: "任务名称不能为空", trigger: "blur" }],
319
+    invokeTarget: [{ required: true, message: "调用目标字符串不能为空", trigger: "blur" }],
320
+    cronExpression: [{ required: true, message: "cron执行表达式不能为空", trigger: "change" }]
321
+  }
322
+})
323
+
324
+const { queryParams, form, rules } = toRefs(data)
325
+
326
+/** 查询定时任务列表 */
327
+function getList() {
328
+  loading.value = true
329
+  listJob(queryParams.value).then(response => {
330
+    jobList.value = response.rows
331
+    total.value = response.total
332
+    loading.value = false
333
+  })
334
+}
335
+
336
+/** 任务组名字典翻译 */
337
+function jobGroupFormat(row, column) {
338
+  return proxy.selectDictLabel(sys_job_group.value, row.jobGroup)
339
+}
340
+
341
+/** 取消按钮 */
342
+function cancel() {
343
+  open.value = false
344
+  reset()
345
+}
346
+
347
+/** 表单重置 */
348
+function reset() {
349
+  form.value = {
350
+    jobId: undefined,
351
+    jobName: undefined,
352
+    jobGroup: undefined,
353
+    invokeTarget: undefined,
354
+    cronExpression: undefined,
355
+    misfirePolicy: 1,
356
+    concurrent: 1,
357
+    status: "0"
358
+  }
359
+  proxy.resetForm("jobRef")
360
+}
361
+
362
+/** 搜索按钮操作 */
363
+function handleQuery() {
364
+  queryParams.value.pageNum = 1
365
+  getList()
366
+}
367
+
368
+/** 重置按钮操作 */
369
+function resetQuery() {
370
+  proxy.resetForm("queryRef")
371
+  handleQuery()
372
+}
373
+
374
+// 多选框选中数据
375
+function handleSelectionChange(selection) {
376
+  ids.value = selection.map(item => item.jobId)
377
+  single.value = selection.length != 1
378
+  multiple.value = !selection.length
379
+}
380
+
381
+// 更多操作触发
382
+function handleCommand(command, row) {
383
+  switch (command) {
384
+    case "handleRun":
385
+      handleRun(row)
386
+      break
387
+    case "handleView":
388
+      handleView(row)
389
+      break
390
+    case "handleJobLog":
391
+      handleJobLog(row)
392
+      break
393
+    default:
394
+      break
395
+  }
396
+}
397
+
398
+// 任务状态修改
399
+function handleStatusChange(row) {
400
+  let text = row.status === "0" ? "启用" : "停用"
401
+  proxy.$modal.confirm('确认要"' + text + '""' + row.jobName + '"任务吗?').then(function () {
402
+    return changeJobStatus(row.jobId, row.status)
403
+  }).then(() => {
404
+    proxy.$modal.msgSuccess(text + "成功")
405
+  }).catch(function () {
406
+    row.status = row.status === "0" ? "1" : "0"
407
+  })
408
+}
409
+
410
+/* 立即执行一次 */
411
+function handleRun(row) {
412
+  proxy.$modal.confirm('确认要立即执行一次"' + row.jobName + '"任务吗?').then(function () {
413
+    return runJob(row.jobId, row.jobGroup)
414
+  }).then(() => {
415
+    proxy.$modal.msgSuccess("执行成功")})
416
+  .catch(() => {})
417
+}
418
+
419
+/** 任务详细信息 */
420
+function handleView(row) {
421
+  getJob(row.jobId).then(response => {
422
+    form.value = response.data
423
+    openView.value = true
424
+  })
425
+}
426
+
427
+/** cron表达式按钮操作 */
428
+function handleShowCron() {
429
+  expression.value = form.value.cronExpression
430
+  openCron.value = true
431
+}
432
+
433
+/** 确定后回传值 */
434
+function crontabFill(value) {
435
+  form.value.cronExpression = value
436
+}
437
+
438
+/** 任务日志列表查询 */
439
+function handleJobLog(row) {
440
+  const jobId = row.jobId || 0
441
+  router.push('/monitor/job-log/index/' + jobId)
442
+}
443
+
444
+/** 新增按钮操作 */
445
+function handleAdd() {
446
+  reset()
447
+  open.value = true
448
+  title.value = "添加任务"
449
+}
450
+
451
+/** 修改按钮操作 */
452
+function handleUpdate(row) {
453
+  reset()
454
+  const jobId = row.jobId || ids.value
455
+  getJob(jobId).then(response => {
456
+    form.value = response.data
457
+    open.value = true
458
+    title.value = "修改任务"
459
+  })
460
+}
461
+
462
+/** 提交按钮 */
463
+function submitForm() {
464
+  proxy.$refs["jobRef"].validate(valid => {
465
+    if (valid) {
466
+      if (form.value.jobId != undefined) {
467
+        updateJob(form.value).then(response => {
468
+          proxy.$modal.msgSuccess("修改成功")
469
+          open.value = false
470
+          getList()
471
+        })
472
+      } else {
473
+        addJob(form.value).then(response => {
474
+          proxy.$modal.msgSuccess("新增成功")
475
+          open.value = false
476
+          getList()
477
+        })
478
+      }
479
+    }
480
+  })
481
+}
482
+
483
+/** 删除按钮操作 */
484
+function handleDelete(row) {
485
+  const jobIds = row.jobId || ids.value
486
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
487
+    return delJob(jobIds)
488
+  }).then(() => {
489
+    getList()
490
+    proxy.$modal.msgSuccess("删除成功")
491
+  }).catch(() => {})
492
+}
493
+
494
+/** 导出按钮操作 */
495
+function handleExport() {
496
+  proxy.download("monitor/job/export", {
497
+    ...queryParams.value,
498
+  }, `job_${new Date().getTime()}.xlsx`)
499
+}
500
+
501
+getList()
502
+</script>

+ 283 - 0
src/views/monitor/job/log.vue

@@ -0,0 +1,283 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+         <el-form-item label="任务名称" prop="jobName">
5
+            <el-input
6
+               v-model="queryParams.jobName"
7
+               placeholder="请输入任务名称"
8
+               clearable
9
+               style="width: 240px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="任务组名" prop="jobGroup">
14
+            <el-select
15
+               v-model="queryParams.jobGroup"
16
+               placeholder="请选择任务组名"
17
+               clearable
18
+               style="width: 240px"
19
+            >
20
+               <el-option
21
+                  v-for="dict in sys_job_group"
22
+                  :key="dict.value"
23
+                  :label="dict.label"
24
+                  :value="dict.value"
25
+               />
26
+            </el-select>
27
+         </el-form-item>
28
+         <el-form-item label="执行状态" prop="status">
29
+            <el-select
30
+               v-model="queryParams.status"
31
+               placeholder="请选择执行状态"
32
+               clearable
33
+               style="width: 240px"
34
+            >
35
+               <el-option
36
+                  v-for="dict in sys_common_status"
37
+                  :key="dict.value"
38
+                  :label="dict.label"
39
+                  :value="dict.value"
40
+               />
41
+            </el-select>
42
+         </el-form-item>
43
+         <el-form-item label="执行时间" style="width: 308px">
44
+            <el-date-picker
45
+               v-model="dateRange"
46
+               value-format="YYYY-MM-DD"
47
+               type="daterange"
48
+               range-separator="-"
49
+               start-placeholder="开始日期"
50
+               end-placeholder="结束日期"
51
+            ></el-date-picker>
52
+         </el-form-item>
53
+         <el-form-item>
54
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
55
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
56
+         </el-form-item>
57
+      </el-form>
58
+
59
+      <el-row :gutter="10" class="mb8">
60
+         <el-col :span="1.5">
61
+            <el-button
62
+               type="danger"
63
+               plain
64
+               icon="Delete"
65
+               :disabled="multiple"
66
+               @click="handleDelete"
67
+               v-hasPermi="['monitor:job:remove']"
68
+            >删除</el-button>
69
+         </el-col>
70
+         <el-col :span="1.5">
71
+            <el-button
72
+               type="danger"
73
+               plain
74
+               icon="Delete"
75
+               @click="handleClean"
76
+               v-hasPermi="['monitor:job:remove']"
77
+            >清空</el-button>
78
+         </el-col>
79
+         <el-col :span="1.5">
80
+            <el-button
81
+               type="warning"
82
+               plain
83
+               icon="Download"
84
+               @click="handleExport"
85
+               v-hasPermi="['monitor:job:export']"
86
+            >导出</el-button>
87
+         </el-col>
88
+         <el-col :span="1.5">
89
+            <el-button 
90
+               type="warning" 
91
+               plain 
92
+               icon="Close"
93
+               @click="handleClose"
94
+            >关闭</el-button>
95
+         </el-col>
96
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
97
+      </el-row>
98
+
99
+      <el-table v-loading="loading" :data="jobLogList" @selection-change="handleSelectionChange">
100
+         <el-table-column type="selection" width="55" align="center" />
101
+         <el-table-column label="日志编号" width="80" align="center" prop="jobLogId" />
102
+         <el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
103
+         <el-table-column label="任务组名" align="center" prop="jobGroup" :show-overflow-tooltip="true">
104
+            <template #default="scope">
105
+               <dict-tag :options="sys_job_group" :value="scope.row.jobGroup" />
106
+            </template>
107
+         </el-table-column>
108
+         <el-table-column label="调用目标字符串" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
109
+         <el-table-column label="日志信息" align="center" prop="jobMessage" :show-overflow-tooltip="true" />
110
+         <el-table-column label="执行状态" align="center" prop="status">
111
+            <template #default="scope">
112
+               <dict-tag :options="sys_common_status" :value="scope.row.status" />
113
+            </template>
114
+         </el-table-column>
115
+         <el-table-column label="执行时间" align="center" prop="createTime" width="180">
116
+            <template #default="scope">
117
+               <span>{{ parseTime(scope.row.createTime) }}</span>
118
+            </template>
119
+         </el-table-column>
120
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
121
+            <template #default="scope">
122
+               <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:job:query']">详细</el-button>
123
+            </template>
124
+         </el-table-column>
125
+      </el-table>
126
+
127
+      <pagination
128
+         v-show="total > 0"
129
+         :total="total"
130
+         v-model:page="queryParams.pageNum"
131
+         v-model:limit="queryParams.pageSize"
132
+         @pagination="getList"
133
+      />
134
+
135
+      <!-- 调度日志详细 -->
136
+      <el-dialog title="调度日志详细" v-model="open" width="700px" append-to-body>
137
+         <el-form :model="form" label-width="100px">
138
+            <el-row>
139
+               <el-col :span="12">
140
+                  <el-form-item label="日志序号:">{{ form.jobLogId }}</el-form-item>
141
+                  <el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
142
+               </el-col>
143
+               <el-col :span="12">
144
+                  <el-form-item label="任务分组:">{{ form.jobGroup }}</el-form-item>
145
+                  <el-form-item label="执行时间:">{{ form.createTime }}</el-form-item>
146
+               </el-col>
147
+               <el-col :span="24">
148
+                  <el-form-item label="调用方法:">{{ form.invokeTarget }}</el-form-item>
149
+               </el-col>
150
+               <el-col :span="24">
151
+                  <el-form-item label="日志信息:">{{ form.jobMessage }}</el-form-item>
152
+               </el-col>
153
+               <el-col :span="24">
154
+                  <el-form-item label="执行状态:">
155
+                     <div v-if="form.status == 0">正常</div>
156
+                     <div v-else-if="form.status == 1">失败</div>
157
+                  </el-form-item>
158
+               </el-col>
159
+               <el-col :span="24">
160
+                  <el-form-item label="异常信息:" v-if="form.status == 1">{{ form.exceptionInfo }}</el-form-item>
161
+               </el-col>
162
+            </el-row>
163
+         </el-form>
164
+         <template #footer>
165
+            <div class="dialog-footer">
166
+               <el-button @click="open = false">关 闭</el-button>
167
+            </div>
168
+         </template>
169
+      </el-dialog>
170
+   </div>
171
+</template>
172
+
173
+<script setup name="JobLog">
174
+import { getJob } from "@/api/monitor/job"
175
+import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog"
176
+
177
+const { proxy } = getCurrentInstance()
178
+const { sys_common_status, sys_job_group } = proxy.useDict("sys_common_status", "sys_job_group")
179
+
180
+const jobLogList = ref([])
181
+const open = ref(false)
182
+const loading = ref(true)
183
+const showSearch = ref(true)
184
+const ids = ref([])
185
+const multiple = ref(true)
186
+const total = ref(0)
187
+const dateRange = ref([])
188
+const route = useRoute()
189
+
190
+const data = reactive({
191
+  form: {},
192
+  queryParams: {
193
+    pageNum: 1,
194
+    pageSize: 10,
195
+    dictName: undefined,
196
+    dictType: undefined,
197
+    status: undefined
198
+  }
199
+})
200
+
201
+const { queryParams, form, rules } = toRefs(data)
202
+
203
+/** 查询调度日志列表 */
204
+function getList() {
205
+  loading.value = true
206
+  listJobLog(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
207
+    jobLogList.value = response.rows
208
+    total.value = response.total
209
+    loading.value = false
210
+  })
211
+}
212
+
213
+// 返回按钮
214
+function handleClose() {
215
+  const obj = { path: "/monitor/job" }
216
+  proxy.$tab.closeOpenPage(obj)
217
+}
218
+
219
+/** 搜索按钮操作 */
220
+function handleQuery() {
221
+  queryParams.value.pageNum = 1
222
+  getList()
223
+}
224
+
225
+/** 重置按钮操作 */
226
+function resetQuery() {
227
+  dateRange.value = []
228
+  proxy.resetForm("queryRef")
229
+  handleQuery()
230
+}
231
+
232
+// 多选框选中数据
233
+function handleSelectionChange(selection) {
234
+  ids.value = selection.map(item => item.jobLogId)
235
+  multiple.value = !selection.length
236
+}
237
+
238
+/** 详细按钮操作 */
239
+function handleView(row) {
240
+  open.value = true
241
+  form.value = row
242
+}
243
+
244
+/** 删除按钮操作 */
245
+function handleDelete(row) {
246
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
247
+    return delJobLog(ids.value)
248
+  }).then(() => {
249
+    getList()
250
+    proxy.$modal.msgSuccess("删除成功")
251
+  }).catch(() => {})
252
+}
253
+
254
+/** 清空按钮操作 */
255
+function handleClean() {
256
+  proxy.$modal.confirm("是否确认清空所有调度日志数据项?").then(function () {
257
+    return cleanJobLog()
258
+  }).then(() => {
259
+    getList()
260
+    proxy.$modal.msgSuccess("清空成功")
261
+  }).catch(() => {})
262
+}
263
+
264
+/** 导出按钮操作 */
265
+function handleExport() {
266
+  proxy.download("monitor/jobLog/export", {
267
+    ...queryParams.value,
268
+  }, `job_log_${new Date().getTime()}.xlsx`)
269
+}
270
+
271
+(() => {
272
+  const jobId = route.params && route.params.jobId
273
+  if (jobId !== undefined && jobId != 0) {
274
+    getJob(jobId).then(response => {
275
+      queryParams.value.jobName = response.data.jobName
276
+      queryParams.value.jobGroup = response.data.jobGroup
277
+      getList()
278
+    })
279
+  } else {
280
+    getList()
281
+  }
282
+})()
283
+</script>

+ 233 - 0
src/views/monitor/logininfor/index.vue

@@ -0,0 +1,233 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+         <el-form-item label="登录地址" prop="ipaddr">
5
+            <el-input
6
+               v-model="queryParams.ipaddr"
7
+               placeholder="请输入登录地址"
8
+               clearable
9
+               style="width: 240px;"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="用户名称" prop="userName">
14
+            <el-input
15
+               v-model="queryParams.userName"
16
+               placeholder="请输入用户名称"
17
+               clearable
18
+               style="width: 240px;"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="状态" prop="status">
23
+            <el-select
24
+               v-model="queryParams.status"
25
+               placeholder="登录状态"
26
+               clearable
27
+               style="width: 240px"
28
+            >
29
+               <el-option
30
+                  v-for="dict in sys_common_status"
31
+                  :key="dict.value"
32
+                  :label="dict.label"
33
+                  :value="dict.value"
34
+               />
35
+            </el-select>
36
+         </el-form-item>
37
+         <el-form-item label="登录时间" style="width: 308px">
38
+            <el-date-picker
39
+               v-model="dateRange"
40
+               value-format="YYYY-MM-DD HH:mm:ss"
41
+               type="daterange"
42
+               range-separator="-"
43
+               start-placeholder="开始日期"
44
+               end-placeholder="结束日期"
45
+               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
46
+            ></el-date-picker>
47
+         </el-form-item>
48
+         <el-form-item>
49
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
50
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
51
+         </el-form-item>
52
+      </el-form>
53
+
54
+      <el-row :gutter="10" class="mb8">
55
+         <el-col :span="1.5">
56
+            <el-button
57
+               type="danger"
58
+               plain
59
+               icon="Delete"
60
+               :disabled="multiple"
61
+               @click="handleDelete"
62
+               v-hasPermi="['monitor:logininfor:remove']"
63
+            >删除</el-button>
64
+         </el-col>
65
+         <el-col :span="1.5">
66
+            <el-button
67
+               type="danger"
68
+               plain
69
+               icon="Delete"
70
+               @click="handleClean"
71
+               v-hasPermi="['monitor:logininfor:remove']"
72
+            >清空</el-button>
73
+         </el-col>
74
+         <el-col :span="1.5">
75
+            <el-button
76
+               type="primary"
77
+               plain
78
+               icon="Unlock"
79
+               :disabled="single"
80
+               @click="handleUnlock"
81
+               v-hasPermi="['monitor:logininfor:unlock']"
82
+            >解锁</el-button>
83
+         </el-col>
84
+         <el-col :span="1.5">
85
+            <el-button
86
+               type="warning"
87
+               plain
88
+               icon="Download"
89
+               @click="handleExport"
90
+               v-hasPermi="['monitor:logininfor:export']"
91
+            >导出</el-button>
92
+         </el-col>
93
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
94
+      </el-row>
95
+
96
+      <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
97
+         <el-table-column type="selection" width="55" align="center" />
98
+         <el-table-column label="访问编号" align="center" prop="infoId" />
99
+         <el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
100
+         <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
101
+         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
102
+         <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
103
+         <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
104
+         <el-table-column label="登录状态" align="center" prop="status">
105
+            <template #default="scope">
106
+               <dict-tag :options="sys_common_status" :value="scope.row.status" />
107
+            </template>
108
+         </el-table-column>
109
+         <el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
110
+         <el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
111
+            <template #default="scope">
112
+               <span>{{ parseTime(scope.row.loginTime) }}</span>
113
+            </template>
114
+         </el-table-column>
115
+      </el-table>
116
+
117
+      <pagination
118
+         v-show="total > 0"
119
+         :total="total"
120
+         v-model:page="queryParams.pageNum"
121
+         v-model:limit="queryParams.pageSize"
122
+         @pagination="getList"
123
+      />
124
+   </div>
125
+</template>
126
+
127
+<script setup name="Logininfor">
128
+import { list, delLogininfor, cleanLogininfor, unlockLogininfor } from "@/api/monitor/logininfor"
129
+
130
+const { proxy } = getCurrentInstance()
131
+const { sys_common_status } = proxy.useDict("sys_common_status")
132
+
133
+const logininforList = ref([])
134
+const loading = ref(true)
135
+const showSearch = ref(true)
136
+const ids = ref([])
137
+const single = ref(true)
138
+const multiple = ref(true)
139
+const selectName = ref("")
140
+const total = ref(0)
141
+const dateRange = ref([])
142
+const defaultSort = ref({ prop: "loginTime", order: "descending" })
143
+
144
+// 查询参数
145
+const queryParams = ref({
146
+  pageNum: 1,
147
+  pageSize: 10,
148
+  ipaddr: undefined,
149
+  userName: undefined,
150
+  status: undefined,
151
+  orderByColumn: undefined,
152
+  isAsc: undefined
153
+})
154
+
155
+/** 查询登录日志列表 */
156
+function getList() {
157
+  loading.value = true
158
+  list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
159
+    logininforList.value = response.rows
160
+    total.value = response.total
161
+    loading.value = false
162
+  })
163
+}
164
+
165
+/** 搜索按钮操作 */
166
+function handleQuery() {
167
+  queryParams.value.pageNum = 1
168
+  getList()
169
+}
170
+
171
+/** 重置按钮操作 */
172
+function resetQuery() {
173
+  dateRange.value = []
174
+  proxy.resetForm("queryRef")
175
+  queryParams.value.pageNum = 1
176
+  proxy.$refs["logininforRef"].sort(defaultSort.value.prop, defaultSort.value.order)
177
+}
178
+
179
+/** 多选框选中数据 */
180
+function handleSelectionChange(selection) {
181
+  ids.value = selection.map(item => item.infoId)
182
+  multiple.value = !selection.length
183
+  single.value = selection.length != 1
184
+  selectName.value = selection.map(item => item.userName)
185
+}
186
+
187
+/** 排序触发事件 */
188
+function handleSortChange(column, prop, order) {
189
+  queryParams.value.orderByColumn = column.prop
190
+  queryParams.value.isAsc = column.order
191
+  getList()
192
+}
193
+
194
+/** 删除按钮操作 */
195
+function handleDelete(row) {
196
+  const infoIds = row.infoId || ids.value
197
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
198
+    return delLogininfor(infoIds)
199
+  }).then(() => {
200
+    getList()
201
+    proxy.$modal.msgSuccess("删除成功")
202
+  }).catch(() => {})
203
+}
204
+
205
+/** 清空按钮操作 */
206
+function handleClean() {
207
+  proxy.$modal.confirm("是否确认清空所有登录日志数据项?").then(function () {
208
+    return cleanLogininfor()
209
+  }).then(() => {
210
+    getList()
211
+    proxy.$modal.msgSuccess("清空成功")
212
+  }).catch(() => {})
213
+}
214
+
215
+/** 解锁按钮操作 */
216
+function handleUnlock() {
217
+  const username = selectName.value
218
+  proxy.$modal.confirm('是否确认解锁用户"' + username + '"数据项?').then(function () {
219
+    return unlockLogininfor(username)
220
+  }).then(() => {
221
+    proxy.$modal.msgSuccess("用户" + username + "解锁成功")
222
+  }).catch(() => {})
223
+}
224
+
225
+/** 导出按钮操作 */
226
+function handleExport() {
227
+  proxy.download("monitor/logininfor/export", {
228
+    ...queryParams.value,
229
+  }, `logininfor_${new Date().getTime()}.xlsx`)
230
+}
231
+
232
+getList()
233
+</script>

+ 109 - 0
src/views/monitor/online/index.vue

@@ -0,0 +1,109 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true">
4
+         <el-form-item label="登录地址" prop="ipaddr">
5
+            <el-input
6
+               v-model="queryParams.ipaddr"
7
+               placeholder="请输入登录地址"
8
+               clearable
9
+               style="width: 200px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="用户名称" prop="userName">
14
+            <el-input
15
+               v-model="queryParams.userName"
16
+               placeholder="请输入用户名称"
17
+               clearable
18
+               style="width: 200px"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item>
23
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
24
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
25
+         </el-form-item>
26
+      </el-form>
27
+      <el-table
28
+         v-loading="loading"
29
+         :data="onlineList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
30
+         style="width: 100%;"
31
+      >
32
+         <el-table-column label="序号" width="50" type="index" align="center">
33
+            <template #default="scope">
34
+               <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
35
+            </template>
36
+         </el-table-column>
37
+         <el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
38
+         <el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
39
+         <el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
40
+         <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
41
+         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
42
+         <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
43
+         <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
44
+         <el-table-column label="登录时间" align="center" prop="loginTime" width="180">
45
+            <template #default="scope">
46
+               <span>{{ parseTime(scope.row.loginTime) }}</span>
47
+            </template>
48
+         </el-table-column>
49
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
50
+            <template #default="scope">
51
+               <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">强退</el-button>
52
+            </template>
53
+         </el-table-column>
54
+      </el-table>
55
+
56
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
57
+   </div>
58
+</template>
59
+
60
+<script setup name="Online">
61
+import { forceLogout, list as initData } from "@/api/monitor/online"
62
+
63
+const { proxy } = getCurrentInstance()
64
+
65
+const onlineList = ref([])
66
+const loading = ref(true)
67
+const total = ref(0)
68
+const pageNum = ref(1)
69
+const pageSize = ref(10)
70
+
71
+const queryParams = ref({
72
+  ipaddr: undefined,
73
+  userName: undefined
74
+})
75
+
76
+/** 查询登录日志列表 */
77
+function getList() {
78
+  loading.value = true
79
+  initData(queryParams.value).then(response => {
80
+    onlineList.value = response.rows
81
+    total.value = response.total
82
+    loading.value = false
83
+  })
84
+}
85
+
86
+/** 搜索按钮操作 */
87
+function handleQuery() {
88
+  pageNum.value = 1
89
+  getList()
90
+}
91
+
92
+/** 重置按钮操作 */
93
+function resetQuery() {
94
+  proxy.resetForm("queryRef")
95
+  handleQuery()
96
+}
97
+
98
+/** 强退按钮操作 */
99
+function handleForceLogout(row) {
100
+    proxy.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?').then(function () {
101
+  return forceLogout(row.tokenId)
102
+  }).then(() => {
103
+    getList()
104
+    proxy.$modal.msgSuccess("删除成功")
105
+  }).catch(() => {})
106
+}
107
+
108
+getList()
109
+</script>

+ 310 - 0
src/views/monitor/operlog/index.vue

@@ -0,0 +1,310 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+         <el-form-item label="操作地址" prop="operIp">
5
+            <el-input
6
+               v-model="queryParams.operIp"
7
+               placeholder="请输入操作地址"
8
+               clearable
9
+               style="width: 240px;"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="系统模块" prop="title">
14
+            <el-input
15
+               v-model="queryParams.title"
16
+               placeholder="请输入系统模块"
17
+               clearable
18
+               style="width: 240px;"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="操作人员" prop="operName">
23
+            <el-input
24
+               v-model="queryParams.operName"
25
+               placeholder="请输入操作人员"
26
+               clearable
27
+               style="width: 240px;"
28
+               @keyup.enter="handleQuery"
29
+            />
30
+         </el-form-item>
31
+         <el-form-item label="类型" prop="businessType">
32
+            <el-select
33
+               v-model="queryParams.businessType"
34
+               placeholder="操作类型"
35
+               clearable
36
+               style="width: 240px"
37
+            >
38
+               <el-option
39
+                  v-for="dict in sys_oper_type"
40
+                  :key="dict.value"
41
+                  :label="dict.label"
42
+                  :value="dict.value"
43
+               />
44
+            </el-select>
45
+         </el-form-item>
46
+         <el-form-item label="状态" prop="status">
47
+            <el-select
48
+               v-model="queryParams.status"
49
+               placeholder="操作状态"
50
+               clearable
51
+               style="width: 240px"
52
+            >
53
+               <el-option
54
+                  v-for="dict in sys_common_status"
55
+                  :key="dict.value"
56
+                  :label="dict.label"
57
+                  :value="dict.value"
58
+               />
59
+            </el-select>
60
+         </el-form-item>
61
+         <el-form-item label="操作时间" style="width: 308px">
62
+            <el-date-picker
63
+               v-model="dateRange"
64
+               value-format="YYYY-MM-DD HH:mm:ss"
65
+               type="daterange"
66
+               range-separator="-"
67
+               start-placeholder="开始日期"
68
+               end-placeholder="结束日期"
69
+               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
70
+            ></el-date-picker>
71
+         </el-form-item>
72
+         <el-form-item>
73
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
74
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
75
+         </el-form-item>
76
+      </el-form>
77
+
78
+      <el-row :gutter="10" class="mb8">
79
+         <el-col :span="1.5">
80
+            <el-button
81
+               type="danger"
82
+               plain
83
+               icon="Delete"
84
+               :disabled="multiple"
85
+               @click="handleDelete"
86
+               v-hasPermi="['monitor:operlog:remove']"
87
+            >删除</el-button>
88
+         </el-col>
89
+         <el-col :span="1.5">
90
+            <el-button
91
+               type="danger"
92
+               plain
93
+               icon="Delete"
94
+               @click="handleClean"
95
+               v-hasPermi="['monitor:operlog:remove']"
96
+            >清空</el-button>
97
+         </el-col>
98
+         <el-col :span="1.5">
99
+            <el-button
100
+               type="warning"
101
+               plain
102
+               icon="Download"
103
+               @click="handleExport"
104
+               v-hasPermi="['monitor:operlog:export']"
105
+            >导出</el-button>
106
+         </el-col>
107
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
108
+      </el-row>
109
+
110
+      <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
111
+         <el-table-column type="selection" width="50" align="center" />
112
+         <el-table-column label="日志编号" align="center" prop="operId" />
113
+         <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
114
+         <el-table-column label="操作类型" align="center" prop="businessType">
115
+            <template #default="scope">
116
+               <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
117
+            </template>
118
+         </el-table-column>
119
+         <el-table-column label="操作人员" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
120
+         <el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
121
+         <el-table-column label="操作状态" align="center" prop="status">
122
+            <template #default="scope">
123
+               <dict-tag :options="sys_common_status" :value="scope.row.status" />
124
+            </template>
125
+         </el-table-column>
126
+         <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
127
+            <template #default="scope">
128
+               <span>{{ parseTime(scope.row.operTime) }}</span>
129
+            </template>
130
+         </el-table-column>
131
+         <el-table-column label="消耗时间" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
132
+            <template #default="scope">
133
+               <span>{{ scope.row.costTime }}毫秒</span>
134
+            </template>
135
+         </el-table-column>
136
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
137
+            <template #default="scope">
138
+               <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">详细</el-button>
139
+            </template>
140
+         </el-table-column>
141
+      </el-table>
142
+
143
+      <pagination
144
+         v-show="total > 0"
145
+         :total="total"
146
+         v-model:page="queryParams.pageNum"
147
+         v-model:limit="queryParams.pageSize"
148
+         @pagination="getList"
149
+      />
150
+
151
+      <!-- 操作日志详细 -->
152
+      <el-dialog title="操作日志详细" v-model="open" width="800px" append-to-body>
153
+         <el-form :model="form" label-width="100px">
154
+            <el-row>
155
+               <el-col :span="12">
156
+                  <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
157
+                  <el-form-item
158
+                    label="登录信息:"
159
+                  >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
160
+               </el-col>
161
+               <el-col :span="12">
162
+                  <el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
163
+                  <el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
164
+               </el-col>
165
+               <el-col :span="24">
166
+                  <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
167
+               </el-col>
168
+               <el-col :span="24">
169
+                  <el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
170
+               </el-col>
171
+               <el-col :span="24">
172
+                  <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
173
+               </el-col>
174
+               <el-col :span="8">
175
+                  <el-form-item label="操作状态:">
176
+                     <div v-if="form.status === 0">正常</div>
177
+                     <div v-else-if="form.status === 1">失败</div>
178
+                  </el-form-item>
179
+               </el-col>
180
+               <el-col :span="8">
181
+                  <el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
182
+               </el-col>
183
+               <el-col :span="8">
184
+                  <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
185
+               </el-col>
186
+               <el-col :span="24">
187
+                  <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
188
+               </el-col>
189
+            </el-row>
190
+         </el-form>
191
+         <template #footer>
192
+            <div class="dialog-footer">
193
+               <el-button @click="open = false">关 闭</el-button>
194
+            </div>
195
+         </template>
196
+      </el-dialog>
197
+   </div>
198
+</template>
199
+
200
+<script setup name="Operlog">
201
+import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
202
+
203
+const { proxy } = getCurrentInstance()
204
+const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type", "sys_common_status")
205
+
206
+const operlogList = ref([])
207
+const open = ref(false)
208
+const loading = ref(true)
209
+const showSearch = ref(true)
210
+const ids = ref([])
211
+const single = ref(true)
212
+const multiple = ref(true)
213
+const total = ref(0)
214
+const title = ref("")
215
+const dateRange = ref([])
216
+const defaultSort = ref({ prop: "operTime", order: "descending" })
217
+
218
+const data = reactive({
219
+  form: {},
220
+  queryParams: {
221
+    pageNum: 1,
222
+    pageSize: 10,
223
+    operIp: undefined,
224
+    title: undefined,
225
+    operName: undefined,
226
+    businessType: undefined,
227
+    status: undefined
228
+  }
229
+})
230
+
231
+const { queryParams, form } = toRefs(data)
232
+
233
+/** 查询登录日志 */
234
+function getList() {
235
+  loading.value = true
236
+  list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
237
+    operlogList.value = response.rows
238
+    total.value = response.total
239
+    loading.value = false
240
+  })
241
+}
242
+
243
+/** 操作日志类型字典翻译 */
244
+function typeFormat(row, column) {
245
+  return proxy.selectDictLabel(sys_oper_type.value, row.businessType)
246
+}
247
+
248
+/** 搜索按钮操作 */
249
+function handleQuery() {
250
+  queryParams.value.pageNum = 1
251
+  getList()
252
+}
253
+
254
+/** 重置按钮操作 */
255
+function resetQuery() {
256
+  dateRange.value = []
257
+  proxy.resetForm("queryRef")
258
+  queryParams.value.pageNum = 1
259
+  proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order)
260
+}
261
+
262
+/** 多选框选中数据 */
263
+function handleSelectionChange(selection) {
264
+  ids.value = selection.map(item => item.operId)
265
+  multiple.value = !selection.length
266
+}
267
+
268
+/** 排序触发事件 */
269
+function handleSortChange(column, prop, order) {
270
+  queryParams.value.orderByColumn = column.prop
271
+  queryParams.value.isAsc = column.order
272
+  getList()
273
+}
274
+
275
+/** 详细按钮操作 */
276
+function handleView(row) {
277
+  open.value = true
278
+  form.value = row
279
+}
280
+
281
+/** 删除按钮操作 */
282
+function handleDelete(row) {
283
+  const operIds = row.operId || ids.value
284
+  proxy.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?').then(function () {
285
+    return delOperlog(operIds)
286
+  }).then(() => {
287
+    getList()
288
+    proxy.$modal.msgSuccess("删除成功")
289
+  }).catch(() => {})
290
+}
291
+
292
+/** 清空按钮操作 */
293
+function handleClean() {
294
+  proxy.$modal.confirm("是否确认清空所有操作日志数据项?").then(function () {
295
+    return cleanOperlog()
296
+  }).then(() => {
297
+    getList()
298
+    proxy.$modal.msgSuccess("清空成功")
299
+  }).catch(() => {})
300
+}
301
+
302
+/** 导出按钮操作 */
303
+function handleExport() {
304
+  proxy.download("monitor/operlog/export",{
305
+    ...queryParams.value,
306
+  }, `config_${new Date().getTime()}.xlsx`)
307
+}
308
+
309
+getList()
310
+</script>

+ 187 - 0
src/views/monitor/server/index.vue

@@ -0,0 +1,187 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-row :gutter="10">
4
+      <el-col :span="12" class="card-box">
5
+        <el-card>
6
+          <template #header><Cpu style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">CPU</span></template>
7
+          <div class="el-table el-table--enable-row-hover el-table--medium">
8
+            <table cellspacing="0" style="width: 100%;">
9
+              <thead>
10
+                <tr>
11
+                  <th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
12
+                  <th class="el-table__cell is-leaf"><div class="cell">值</div></th>
13
+                </tr>
14
+              </thead>
15
+              <tbody>
16
+                <tr>
17
+                  <td class="el-table__cell is-leaf"><div class="cell">核心数</div></td>
18
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
19
+                </tr>
20
+                <tr>
21
+                  <td class="el-table__cell is-leaf"><div class="cell">用户使用率</div></td>
22
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
23
+                </tr>
24
+                <tr>
25
+                  <td class="el-table__cell is-leaf"><div class="cell">系统使用率</div></td>
26
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
27
+                </tr>
28
+                <tr>
29
+                  <td class="el-table__cell is-leaf"><div class="cell">当前空闲率</div></td>
30
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
31
+                </tr>
32
+              </tbody>
33
+            </table>
34
+          </div>
35
+        </el-card>
36
+      </el-col>
37
+
38
+      <el-col :span="12" class="card-box">
39
+        <el-card>
40
+          <template #header><Tickets style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存</span></template>
41
+          <div class="el-table el-table--enable-row-hover el-table--medium">
42
+            <table cellspacing="0" style="width: 100%;">
43
+              <thead>
44
+                <tr>
45
+                  <th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
46
+                  <th class="el-table__cell is-leaf"><div class="cell">内存</div></th>
47
+                  <th class="el-table__cell is-leaf"><div class="cell">JVM</div></th>
48
+                </tr>
49
+              </thead>
50
+              <tbody>
51
+                <tr>
52
+                  <td class="el-table__cell is-leaf"><div class="cell">总内存</div></td>
53
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
54
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
55
+                </tr>
56
+                <tr>
57
+                  <td class="el-table__cell is-leaf"><div class="cell">已用内存</div></td>
58
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
59
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
60
+                </tr>
61
+                <tr>
62
+                  <td class="el-table__cell is-leaf"><div class="cell">剩余内存</div></td>
63
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
64
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
65
+                </tr>
66
+                <tr>
67
+                  <td class="el-table__cell is-leaf"><div class="cell">使用率</div></td>
68
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
69
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
70
+                </tr>
71
+              </tbody>
72
+            </table>
73
+          </div>
74
+        </el-card>
75
+      </el-col>
76
+
77
+      <el-col :span="24" class="card-box">
78
+        <el-card>
79
+          <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">服务器信息</span></template>
80
+          <div class="el-table el-table--enable-row-hover el-table--medium">
81
+            <table cellspacing="0" style="width: 100%;">
82
+              <tbody>
83
+                <tr>
84
+                  <td class="el-table__cell is-leaf"><div class="cell">服务器名称</div></td>
85
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
86
+                  <td class="el-table__cell is-leaf"><div class="cell">操作系统</div></td>
87
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
88
+                </tr>
89
+                <tr>
90
+                  <td class="el-table__cell is-leaf"><div class="cell">服务器IP</div></td>
91
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
92
+                  <td class="el-table__cell is-leaf"><div class="cell">系统架构</div></td>
93
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
94
+                </tr>
95
+              </tbody>
96
+            </table>
97
+          </div>
98
+        </el-card>
99
+      </el-col>
100
+
101
+      <el-col :span="24" class="card-box">
102
+        <el-card>
103
+          <template #header><CoffeeCup style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">Java虚拟机信息</span></template>
104
+          <div class="el-table el-table--enable-row-hover el-table--medium">
105
+            <table cellspacing="0" style="width: 100%;table-layout:fixed;">
106
+              <tbody>
107
+                <tr>
108
+                  <td class="el-table__cell is-leaf"><div class="cell">Java名称</div></td>
109
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
110
+                  <td class="el-table__cell is-leaf"><div class="cell">Java版本</div></td>
111
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
112
+                </tr>
113
+                <tr>
114
+                  <td class="el-table__cell is-leaf"><div class="cell">启动时间</div></td>
115
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
116
+                  <td class="el-table__cell is-leaf"><div class="cell">运行时长</div></td>
117
+                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
118
+                </tr>
119
+                <tr>
120
+                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">安装路径</div></td>
121
+                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
122
+                </tr>
123
+                <tr>
124
+                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">项目路径</div></td>
125
+                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
126
+                </tr>
127
+                <tr>
128
+                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">运行参数</div></td>
129
+                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.inputArgs }}</div></td>
130
+                </tr>
131
+              </tbody>
132
+            </table>
133
+          </div>
134
+        </el-card>
135
+      </el-col>
136
+
137
+      <el-col :span="24" class="card-box">
138
+        <el-card>
139
+          <template #header><MessageBox style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">磁盘状态</span></template>
140
+          <div class="el-table el-table--enable-row-hover el-table--medium">
141
+            <table cellspacing="0" style="width: 100%;">
142
+              <thead>
143
+                <tr>
144
+                  <th class="el-table__cell el-table__cell is-leaf"><div class="cell">盘符路径</div></th>
145
+                  <th class="el-table__cell is-leaf"><div class="cell">文件系统</div></th>
146
+                  <th class="el-table__cell is-leaf"><div class="cell">盘符类型</div></th>
147
+                  <th class="el-table__cell is-leaf"><div class="cell">总大小</div></th>
148
+                  <th class="el-table__cell is-leaf"><div class="cell">可用大小</div></th>
149
+                  <th class="el-table__cell is-leaf"><div class="cell">已用大小</div></th>
150
+                  <th class="el-table__cell is-leaf"><div class="cell">已用百分比</div></th>
151
+                </tr>
152
+              </thead>
153
+              <tbody v-if="server.sysFiles">
154
+                <tr v-for="(sysFile, index) in server.sysFiles" :key="index">
155
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.dirName }}</div></td>
156
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.sysTypeName }}</div></td>
157
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.typeName }}</div></td>
158
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.total }}</div></td>
159
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.free }}</div></td>
160
+                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.used }}</div></td>
161
+                  <td class="el-table__cell is-leaf"><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
162
+                </tr>
163
+              </tbody>
164
+            </table>
165
+          </div>
166
+        </el-card>
167
+      </el-col>
168
+    </el-row>
169
+  </div>
170
+</template>
171
+
172
+<script setup>
173
+import { getServer } from '@/api/monitor/server'
174
+
175
+const server = ref([])
176
+const { proxy } = getCurrentInstance()
177
+
178
+function getList() {
179
+  proxy.$modal.loading("正在加载服务监控数据,请稍候!")
180
+  getServer().then(response => {
181
+    server.value = response.data
182
+    proxy.$modal.closeLoading()
183
+  })
184
+}
185
+
186
+getList()
187
+</script>

+ 14 - 0
src/views/redirect/index.vue

@@ -0,0 +1,14 @@
1
+<template>
2
+  <div></div>
3
+</template>
4
+
5
+<script setup>
6
+import { useRoute, useRouter } from 'vue-router'
7
+
8
+const route = useRoute()
9
+const router = useRouter()
10
+const { params, query } = route
11
+const { path } = params
12
+
13
+router.replace({ path: '/' + path, query })
14
+</script>

+ 220 - 0
src/views/register.vue

@@ -0,0 +1,220 @@
1
+<template>
2
+  <div class="register">
3
+    <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
4
+      <h3 class="title">{{ title }}</h3>
5
+      <el-form-item prop="username">
6
+        <el-input 
7
+          v-model="registerForm.username" 
8
+          type="text" 
9
+          size="large" 
10
+          auto-complete="off" 
11
+          placeholder="账号"
12
+        >
13
+          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
14
+        </el-input>
15
+      </el-form-item>
16
+      <el-form-item prop="password">
17
+        <el-input
18
+          v-model="registerForm.password"
19
+          type="password"
20
+          size="large" 
21
+          auto-complete="off"
22
+          placeholder="密码"
23
+          @keyup.enter="handleRegister"
24
+        >
25
+          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
26
+        </el-input>
27
+      </el-form-item>
28
+      <el-form-item prop="confirmPassword">
29
+        <el-input
30
+          v-model="registerForm.confirmPassword"
31
+          type="password"
32
+          size="large" 
33
+          auto-complete="off"
34
+          placeholder="确认密码"
35
+          @keyup.enter="handleRegister"
36
+        >
37
+          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
38
+        </el-input>
39
+      </el-form-item>
40
+      <el-form-item prop="code" v-if="captchaEnabled">
41
+        <el-input
42
+          size="large" 
43
+          v-model="registerForm.code"
44
+          auto-complete="off"
45
+          placeholder="验证码"
46
+          style="width: 63%"
47
+          @keyup.enter="handleRegister"
48
+        >
49
+          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
50
+        </el-input>
51
+        <div class="register-code">
52
+          <img :src="codeUrl" @click="getCode" class="register-code-img"/>
53
+        </div>
54
+      </el-form-item>
55
+      <el-form-item style="width:100%;">
56
+        <el-button
57
+          :loading="loading"
58
+          size="large" 
59
+          type="primary"
60
+          style="width:100%;"
61
+          @click.prevent="handleRegister"
62
+        >
63
+          <span v-if="!loading">注 册</span>
64
+          <span v-else>注 册 中...</span>
65
+        </el-button>
66
+        <div style="float: right;">
67
+          <router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
68
+        </div>
69
+      </el-form-item>
70
+    </el-form>
71
+    <!--  底部  -->
72
+    <div class="el-register-footer">
73
+      <span></span>
74
+    </div>
75
+  </div>
76
+</template>
77
+
78
+<script setup>
79
+import { ElMessageBox } from "element-plus"
80
+import { getCodeImg, register } from "@/api/login"
81
+
82
+const title = import.meta.env.VITE_APP_TITLE
83
+const router = useRouter()
84
+const { proxy } = getCurrentInstance()
85
+
86
+const registerForm = ref({
87
+  username: "",
88
+  password: "",
89
+  confirmPassword: "",
90
+  code: "",
91
+  uuid: ""
92
+})
93
+
94
+const equalToPassword = (rule, value, callback) => {
95
+  if (registerForm.value.password !== value) {
96
+    callback(new Error("两次输入的密码不一致"))
97
+  } else {
98
+    callback()
99
+  }
100
+}
101
+
102
+const registerRules = {
103
+  username: [
104
+    { required: true, trigger: "blur", message: "请输入您的账号" },
105
+    { min: 2, max: 20, message: "用户账号长度必须介于 2 和 20 之间", trigger: "blur" }
106
+  ],
107
+  password: [
108
+    { required: true, trigger: "blur", message: "请输入您的密码" },
109
+    { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" },
110
+    { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
111
+  ],
112
+  confirmPassword: [
113
+    { required: true, trigger: "blur", message: "请再次输入您的密码" },
114
+    { required: true, validator: equalToPassword, trigger: "blur" }
115
+  ],
116
+  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
117
+}
118
+
119
+const codeUrl = ref("")
120
+const loading = ref(false)
121
+const captchaEnabled = ref(true)
122
+
123
+function handleRegister() {
124
+  proxy.$refs.registerRef.validate(valid => {
125
+    if (valid) {
126
+      loading.value = true
127
+      register(registerForm.value).then(res => {
128
+        const username = registerForm.value.username
129
+        ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", "系统提示", {
130
+          dangerouslyUseHTMLString: true,
131
+          type: "success",
132
+        }).then(() => {
133
+          router.push("/login")
134
+        }).catch(() => {})
135
+      }).catch(() => {
136
+        loading.value = false
137
+        if (captchaEnabled) {
138
+          getCode()
139
+        }
140
+      })
141
+    }
142
+  })
143
+}
144
+
145
+function getCode() {
146
+  getCodeImg().then(res => {
147
+    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
148
+    if (captchaEnabled.value) {
149
+      codeUrl.value = "data:image/gif;base64," + res.img
150
+      registerForm.value.uuid = res.uuid
151
+    }
152
+  })
153
+}
154
+
155
+getCode()
156
+</script>
157
+
158
+<style lang='scss' scoped>
159
+.register {
160
+  display: flex;
161
+  justify-content: center;
162
+  align-items: center;
163
+  height: 100%;
164
+  background-image: url("../assets/images/login-background.jpg");
165
+  background-size: cover;
166
+}
167
+.title {
168
+  margin: 0px auto 30px auto;
169
+  text-align: center;
170
+  color: #707070;
171
+}
172
+
173
+.register-form {
174
+  border-radius: 6px;
175
+  background: #ffffff;
176
+  width: 400px;
177
+  padding: 25px 25px 5px 25px;
178
+  .el-input {
179
+    height: 40px;
180
+    input {
181
+      height: 40px;
182
+    }
183
+  }
184
+  .input-icon {
185
+    height: 39px;
186
+    width: 14px;
187
+    margin-left: 0px;
188
+  }
189
+}
190
+.register-tip {
191
+  font-size: 13px;
192
+  text-align: center;
193
+  color: #bfbfbf;
194
+}
195
+.register-code {
196
+  width: 33%;
197
+  height: 40px;
198
+  float: right;
199
+  img {
200
+    cursor: pointer;
201
+    vertical-align: middle;
202
+  }
203
+}
204
+.el-register-footer {
205
+  height: 40px;
206
+  line-height: 40px;
207
+  position: fixed;
208
+  bottom: 0;
209
+  width: 100%;
210
+  text-align: center;
211
+  color: #fff;
212
+  font-family: Arial;
213
+  font-size: 12px;
214
+  letter-spacing: 1px;
215
+}
216
+.register-code-img {
217
+  height: 40px;
218
+  padding-left: 12px;
219
+}
220
+</style>

+ 272 - 0
src/views/system/category/index.vue

@@ -0,0 +1,272 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="分类名称" prop="name">
5
+        <el-input
6
+          v-model="queryParams.name"
7
+          placeholder="请输入分类名称"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="编码" prop="code">
13
+        <el-input
14
+          v-model="queryParams.code"
15
+          placeholder="请输入编码"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item>
21
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
22
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
23
+      </el-form-item>
24
+    </el-form>
25
+
26
+    <el-row :gutter="10" class="mb8">
27
+      <el-col :span="1.5">
28
+        <el-button
29
+          type="primary"
30
+          plain
31
+          icon="Plus"
32
+          @click="handleAdd"
33
+          v-hasPermi="['system:category:add']"
34
+        >新增</el-button>
35
+      </el-col>
36
+      <el-col :span="1.5">
37
+        <el-button
38
+          type="info"
39
+          plain
40
+          icon="Sort"
41
+          @click="toggleExpandAll"
42
+        >展开/折叠</el-button>
43
+      </el-col>
44
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
45
+    </el-row>
46
+
47
+    <el-table
48
+      v-if="refreshTable"
49
+      v-loading="loading"
50
+      :data="categoryList"
51
+      row-key="id"
52
+      :default-expand-all="isExpandAll"
53
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
54
+    >
55
+      <el-table-column label="编码" align="center" prop="code" />
56
+      <el-table-column label="分类名称" align="center" prop="name" />
57
+      <!-- <el-table-column label="层级" align="center" prop="level" /> -->
58
+      <el-table-column label="显示顺序" align="center" prop="orderNum" />
59
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
60
+        <template #default="scope">
61
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:category:edit']">修改</el-button>
62
+          <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:category:add']">新增</el-button>
63
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:category:remove']">删除</el-button>
64
+        </template>
65
+      </el-table-column>
66
+    </el-table>
67
+
68
+    <!-- 添加或修改查获物品分类对话框 -->
69
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
70
+      <el-form ref="categoryRef" :model="form" :rules="rules" label-width="80px">
71
+        <el-form-item label="分类编码" prop="code" v-if="form.id">
72
+          <el-input v-model="form.code" disabled  />
73
+        </el-form-item>
74
+        <el-form-item label="分类名称" prop="name">
75
+          <el-input v-model="form.name" placeholder="请输入分类名称" />
76
+        </el-form-item>
77
+        <el-form-item label="上级类型" prop="parentId">
78
+          <el-tree-select
79
+            v-model="form.parentId"
80
+            :data="categoryOptions"
81
+            :props="{ value: 'id', label: 'name', children: 'children' }"
82
+            value-key="id"
83
+            placeholder="请选择上级类型"
84
+            check-strictly
85
+          />
86
+        </el-form-item>
87
+        <!-- <el-form-item label="层级" prop="level">
88
+          <el-input v-model="form.level" placeholder="请输入层级" />
89
+        </el-form-item> -->
90
+        <el-form-item label="显示顺序" prop="orderNum">
91
+          <el-input v-model="form.orderNum" placeholder="请输入显示顺序" />
92
+        </el-form-item>
93
+      </el-form>
94
+      <template #footer>
95
+        <div class="dialog-footer">
96
+          <el-button type="primary" @click="submitForm">确 定</el-button>
97
+          <el-button @click="cancel">取 消</el-button>
98
+        </div>
99
+      </template>
100
+    </el-dialog>
101
+  </div>
102
+</template>
103
+
104
+<script setup name="Category">
105
+import { listCategory, getCategory, delCategory, addCategory, updateCategory } from "@/api/system/category"
106
+
107
+const { proxy } = getCurrentInstance()
108
+
109
+const categoryList = ref([])
110
+const categoryOptions = ref([])
111
+const open = ref(false)
112
+const loading = ref(true)
113
+const showSearch = ref(true)
114
+const title = ref("")
115
+const isExpandAll = ref(true)
116
+const refreshTable = ref(true)
117
+
118
+const data = reactive({
119
+  form: {},
120
+  queryParams: {
121
+    name: null,
122
+    parentId: null,
123
+    level: null,
124
+    orderNum: null,
125
+    code: null
126
+  },
127
+  rules: {
128
+    name: [
129
+      { required: true, message: "分类名称不能为空", trigger: "blur" }
130
+    ],
131
+    parentId: [
132
+      { required: true, message: "上级类型不能为空", trigger: "blur" }
133
+    ],
134
+    level: [
135
+      { required: true, message: "层级不能为空", trigger: "blur" }
136
+    ],
137
+    orderNum: [
138
+      { required: true, message: "显示顺序不能为空", trigger: "blur" }
139
+    ],
140
+    code: [
141
+      { required: true, message: "编码不能为空", trigger: "blur" }
142
+    ]
143
+  }
144
+})
145
+
146
+const { queryParams, form, rules } = toRefs(data)
147
+
148
+/** 查询查获物品分类列表 */
149
+function getList() {
150
+  loading.value = true
151
+  listCategory(queryParams.value).then(response => {
152
+    categoryList.value = proxy.handleTree(response.data, "id", "parentId")
153
+    loading.value = false
154
+  })
155
+}
156
+
157
+/** 查询查获物品分类下拉树结构 */
158
+function getTreeselect() {
159
+  listCategory().then(response => {
160
+    categoryOptions.value = []
161
+    const data = { id: 0, name: '顶级节点', children: [] }
162
+    data.children = proxy.handleTree(response.data, "id", "parentId")
163
+    categoryOptions.value.push(data)
164
+  })
165
+}
166
+	
167
+// 取消按钮
168
+function cancel() {
169
+  open.value = false
170
+  reset()
171
+}
172
+
173
+// 表单重置
174
+function reset() {
175
+  form.value = {
176
+    tenantId: null,
177
+    revision: null,
178
+    createBy: null,
179
+    createTime: null,
180
+    updateBy: null,
181
+    updateTime: null,
182
+    name: null,
183
+    id: null,
184
+    parentId: null,
185
+    ancestors: null,
186
+    level: null,
187
+    orderNum: null,
188
+    code: null
189
+  }
190
+  proxy.resetForm("categoryRef")
191
+}
192
+
193
+/** 搜索按钮操作 */
194
+function handleQuery() {
195
+  getList()
196
+}
197
+
198
+/** 重置按钮操作 */
199
+function resetQuery() {
200
+  proxy.resetForm("queryRef")
201
+  handleQuery()
202
+}
203
+
204
+/** 新增按钮操作 */
205
+function handleAdd(row) {
206
+  reset()
207
+  getTreeselect()
208
+  if (row != null && row.id) {
209
+    form.value.parentId = row.id
210
+  } else {
211
+    form.value.parentId = 0
212
+  }
213
+  open.value = true
214
+  title.value = "添加查获物品分类"
215
+}
216
+
217
+/** 展开/折叠操作 */
218
+function toggleExpandAll() {
219
+  refreshTable.value = false
220
+  isExpandAll.value = !isExpandAll.value
221
+  nextTick(() => {
222
+    refreshTable.value = true
223
+  })
224
+}
225
+
226
+/** 修改按钮操作 */
227
+async function handleUpdate(row) {
228
+  reset()
229
+  await getTreeselect()
230
+  if (row != null) {
231
+    form.value.parentId = row.parentId
232
+  }
233
+  getCategory(row.id).then(response => {
234
+    form.value = response.data
235
+    open.value = true
236
+    title.value = "修改查获物品分类"
237
+  })
238
+}
239
+
240
+/** 提交按钮 */
241
+function submitForm() {
242
+  proxy.$refs["categoryRef"].validate(valid => {
243
+    if (valid) {
244
+      if (form.value.id != null) {
245
+        updateCategory(form.value).then(response => {
246
+          proxy.$modal.msgSuccess("修改成功")
247
+          open.value = false
248
+          getList()
249
+        })
250
+      } else {
251
+        addCategory(form.value).then(response => {
252
+          proxy.$modal.msgSuccess("新增成功")
253
+          open.value = false
254
+          getList()
255
+        })
256
+      }
257
+    }
258
+  })
259
+}
260
+
261
+/** 删除按钮操作 */
262
+function handleDelete(row) {
263
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
264
+    return delCategory(row.id)
265
+  }).then(() => {
266
+    getList()
267
+    proxy.$modal.msgSuccess("删除成功")
268
+  }).catch(() => {})
269
+}
270
+
271
+getList()
272
+</script>

+ 276 - 0
src/views/system/checkCategory/index.vue

@@ -0,0 +1,276 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="编码" prop="code">
5
+        <el-input
6
+          v-model="queryParams.code"
7
+          placeholder="请输入编码"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="分类名称" prop="name">
13
+        <el-input
14
+          v-model="queryParams.name"
15
+          placeholder="请输入分类名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item>
21
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
22
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
23
+      </el-form-item>
24
+    </el-form>
25
+
26
+    <el-row :gutter="10" class="mb8">
27
+      <el-col :span="1.5">
28
+        <el-button
29
+          type="primary"
30
+          plain
31
+          icon="Plus"
32
+          @click="handleAdd"
33
+          v-hasPermi="['system:checkCategory:add']"
34
+        >新增</el-button>
35
+      </el-col>
36
+      <el-col :span="1.5">
37
+        <el-button
38
+          type="info"
39
+          plain
40
+          icon="Sort"
41
+          @click="toggleExpandAll"
42
+        >展开/折叠</el-button>
43
+      </el-col>
44
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
45
+    </el-row>
46
+
47
+    <el-table
48
+      v-if="refreshTable"
49
+      v-loading="loading"
50
+      :data="checkCategoryList"
51
+      row-key="id"
52
+      :default-expand-all="isExpandAll"
53
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
54
+    >
55
+      <el-table-column label="编码" align="center" prop="code" />
56
+      <el-table-column label="分类名称" align="center" prop="name" />
57
+      <el-table-column label="显示顺序" align="center" prop="orderNum" />
58
+      <el-table-column label="备注" align="center" prop="remark" />
59
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
60
+        <template #default="scope">
61
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:checkCategory:edit']">修改</el-button>
62
+          <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:checkCategory:add']">新增</el-button>
63
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:checkCategory:remove']">删除</el-button>
64
+        </template>
65
+      </el-table-column>
66
+    </el-table>
67
+
68
+    <!-- 添加或修改检查项分类对话框 -->
69
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
70
+      <el-form ref="checkCategoryRef" :model="form" :rules="rules" label-width="80px">
71
+        <el-form-item label="编码" prop="code" v-if="form.id">
72
+          <el-input v-model="form.code" placeholder="请输入编码" disabled/>
73
+        </el-form-item>
74
+        <el-form-item label="分类名称" prop="name">
75
+          <el-input v-model="form.name" placeholder="请输入分类名称" />
76
+        </el-form-item>
77
+        <el-form-item label="上级分类" prop="parentId">
78
+          <el-tree-select
79
+            v-model="form.parentId"
80
+            :data="checkCategoryOptions"
81
+            :props="{ value: 'id', label: 'name', children: 'children' }"
82
+            value-key="id"
83
+            placeholder="请选择上级分类"
84
+            check-strictly
85
+          />
86
+        </el-form-item>
87
+        <el-form-item label="显示顺序" prop="orderNum">
88
+          <el-input v-model="form.orderNum" placeholder="请输入显示顺序" />
89
+        </el-form-item>
90
+
91
+        <el-form-item label="备注" prop="remark">
92
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
93
+        </el-form-item>
94
+      </el-form>
95
+      <template #footer>
96
+        <div class="dialog-footer">
97
+          <el-button type="primary" @click="submitForm">确 定</el-button>
98
+          <el-button @click="cancel">取 消</el-button>
99
+        </div>
100
+      </template>
101
+    </el-dialog>
102
+  </div>
103
+</template>
104
+
105
+<script setup name="CheckCategory">
106
+import { listCheckCategory, getCheckCategory, delCheckCategory, addCheckCategory, updateCheckCategory } from "@/api/system/checkCategory"
107
+
108
+const { proxy } = getCurrentInstance()
109
+
110
+const checkCategoryList = ref([])
111
+const checkCategoryOptions = ref([])
112
+const open = ref(false)
113
+const loading = ref(true)
114
+const showSearch = ref(true)
115
+const title = ref("")
116
+const isExpandAll = ref(true)
117
+const refreshTable = ref(true)
118
+
119
+const data = reactive({
120
+  form: {},
121
+  queryParams: {
122
+    name: null,
123
+    parentId: null,
124
+    orderNum: null,
125
+    code: null,
126
+  },
127
+  rules: {
128
+    name: [
129
+      { required: true, message: "分类名称不能为空", trigger: "blur" }
130
+    ],
131
+    parentId: [
132
+      { required: true, message: "上级分类不能为空", trigger: "blur" }
133
+    ],
134
+    ancestors: [
135
+      { required: true, message: "祖级列表不能为空", trigger: "blur" }
136
+    ],
137
+    level: [
138
+      { required: true, message: "层级不能为空", trigger: "blur" }
139
+    ],
140
+    orderNum: [
141
+      { required: true, message: "显示顺序不能为空", trigger: "blur" }
142
+    ],
143
+    code: [
144
+      { required: true, message: "编码不能为空", trigger: "blur" }
145
+    ],
146
+  }
147
+})
148
+
149
+const { queryParams, form, rules } = toRefs(data)
150
+
151
+/** 查询检查项分类列表 */
152
+function getList() {
153
+  loading.value = true
154
+  listCheckCategory(queryParams.value).then(response => {
155
+    checkCategoryList.value = proxy.handleTree(response.data, "id", "parentId")
156
+    loading.value = false
157
+  })
158
+}
159
+
160
+/** 查询检查项分类下拉树结构 */
161
+function getTreeselect() {
162
+  listCheckCategory().then(response => {
163
+    checkCategoryOptions.value = []
164
+    const data = { id: 0, name: '顶级节点', children: [] }
165
+    data.children = proxy.handleTree(response.data, "id", "parentId")
166
+    checkCategoryOptions.value.push(data)
167
+  })
168
+}
169
+	
170
+// 取消按钮
171
+function cancel() {
172
+  open.value = false
173
+  reset()
174
+}
175
+
176
+// 表单重置
177
+function reset() {
178
+  form.value = {
179
+    tenantId: null,
180
+    revision: null,
181
+    createBy: null,
182
+    createTime: null,
183
+    updateBy: null,
184
+    updateTime: null,
185
+    id: null,
186
+    name: null,
187
+    parentId: null,
188
+    ancestors: null,
189
+    level: null,
190
+    orderNum: null,
191
+    code: null,
192
+    remark: null
193
+  }
194
+  proxy.resetForm("checkCategoryRef")
195
+}
196
+
197
+/** 搜索按钮操作 */
198
+function handleQuery() {
199
+  getList()
200
+}
201
+
202
+/** 重置按钮操作 */
203
+function resetQuery() {
204
+  proxy.resetForm("queryRef")
205
+  handleQuery()
206
+}
207
+
208
+/** 新增按钮操作 */
209
+function handleAdd(row) {
210
+  reset()
211
+  getTreeselect()
212
+  if (row != null && row.id) {
213
+    form.value.parentId = row.id
214
+  } else {
215
+    form.value.parentId = 0
216
+  }
217
+  open.value = true
218
+  title.value = "添加检查项分类"
219
+}
220
+
221
+/** 展开/折叠操作 */
222
+function toggleExpandAll() {
223
+  refreshTable.value = false
224
+  isExpandAll.value = !isExpandAll.value
225
+  nextTick(() => {
226
+    refreshTable.value = true
227
+  })
228
+}
229
+
230
+/** 修改按钮操作 */
231
+async function handleUpdate(row) {
232
+  reset()
233
+  await getTreeselect()
234
+  if (row != null) {
235
+    form.value.parentId = row.parentId
236
+  }
237
+  getCheckCategory(row.id).then(response => {
238
+    form.value = response.data
239
+    open.value = true
240
+    title.value = "修改检查项分类"
241
+  })
242
+}
243
+
244
+/** 提交按钮 */
245
+function submitForm() {
246
+  proxy.$refs["checkCategoryRef"].validate(valid => {
247
+    if (valid) {
248
+      if (form.value.id != null) {
249
+        updateCheckCategory(form.value).then(response => {
250
+          proxy.$modal.msgSuccess("修改成功")
251
+          open.value = false
252
+          getList()
253
+        })
254
+      } else {
255
+        addCheckCategory(form.value).then(response => {
256
+          proxy.$modal.msgSuccess("新增成功")
257
+          open.value = false
258
+          getList()
259
+        })
260
+      }
261
+    }
262
+  })
263
+}
264
+
265
+/** 删除按钮操作 */
266
+function handleDelete(row) {
267
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
268
+    return delCheckCategory(row.id)
269
+  }).then(() => {
270
+    getList()
271
+    proxy.$modal.msgSuccess("删除成功")
272
+  }).catch(() => {})
273
+}
274
+
275
+getList()
276
+</script>

+ 316 - 0
src/views/system/config/index.vue

@@ -0,0 +1,316 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+         <el-form-item label="参数名称" prop="configName">
5
+            <el-input
6
+               v-model="queryParams.configName"
7
+               placeholder="请输入参数名称"
8
+               clearable
9
+               style="width: 240px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="参数键名" prop="configKey">
14
+            <el-input
15
+               v-model="queryParams.configKey"
16
+               placeholder="请输入参数键名"
17
+               clearable
18
+               style="width: 240px"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="系统内置" prop="configType">
23
+            <el-select v-model="queryParams.configType" placeholder="系统内置" clearable style="width: 240px">
24
+               <el-option
25
+                  v-for="dict in sys_yes_no"
26
+                  :key="dict.value"
27
+                  :label="dict.label"
28
+                  :value="dict.value"
29
+               />
30
+            </el-select>
31
+         </el-form-item>
32
+         <el-form-item label="创建时间" style="width: 308px;">
33
+            <el-date-picker
34
+               v-model="dateRange"
35
+               value-format="YYYY-MM-DD"
36
+               type="daterange"
37
+               range-separator="-"
38
+               start-placeholder="开始日期"
39
+               end-placeholder="结束日期"
40
+            ></el-date-picker>
41
+         </el-form-item>
42
+         <el-form-item>
43
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
44
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
45
+         </el-form-item>
46
+      </el-form>
47
+
48
+      <el-row :gutter="10" class="mb8">
49
+         <el-col :span="1.5">
50
+            <el-button
51
+               type="primary"
52
+               plain
53
+               icon="Plus"
54
+               @click="handleAdd"
55
+               v-hasPermi="['system:config:add']"
56
+            >新增</el-button>
57
+         </el-col>
58
+         <el-col :span="1.5">
59
+            <el-button
60
+               type="success"
61
+               plain
62
+               icon="Edit"
63
+               :disabled="single"
64
+               @click="handleUpdate"
65
+               v-hasPermi="['system:config:edit']"
66
+            >修改</el-button>
67
+         </el-col>
68
+         <el-col :span="1.5">
69
+            <el-button
70
+               type="danger"
71
+               plain
72
+               icon="Delete"
73
+               :disabled="multiple"
74
+               @click="handleDelete"
75
+               v-hasPermi="['system:config:remove']"
76
+            >删除</el-button>
77
+         </el-col>
78
+         <el-col :span="1.5">
79
+            <el-button
80
+               type="warning"
81
+               plain
82
+               icon="Download"
83
+               @click="handleExport"
84
+               v-hasPermi="['system:config:export']"
85
+            >导出</el-button>
86
+         </el-col>
87
+         <el-col :span="1.5">
88
+            <el-button
89
+               type="danger"
90
+               plain
91
+               icon="Refresh"
92
+               @click="handleRefreshCache"
93
+               v-hasPermi="['system:config:remove']"
94
+            >刷新缓存</el-button>
95
+         </el-col>
96
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
97
+      </el-row>
98
+
99
+      <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
100
+         <el-table-column type="selection" width="55" align="center" />
101
+         <el-table-column label="参数主键" align="center" prop="configId" />
102
+         <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
103
+         <el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
104
+         <el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
105
+         <el-table-column label="系统内置" align="center" prop="configType">
106
+            <template #default="scope">
107
+               <dict-tag :options="sys_yes_no" :value="scope.row.configType" />
108
+            </template>
109
+         </el-table-column>
110
+         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
111
+         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
112
+            <template #default="scope">
113
+               <span>{{ parseTime(scope.row.createTime) }}</span>
114
+            </template>
115
+         </el-table-column>
116
+         <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
117
+            <template #default="scope">
118
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']" >修改</el-button>
119
+               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']">删除</el-button>
120
+            </template>
121
+         </el-table-column>
122
+      </el-table>
123
+
124
+      <pagination
125
+         v-show="total > 0"
126
+         :total="total"
127
+         v-model:page="queryParams.pageNum"
128
+         v-model:limit="queryParams.pageSize"
129
+         @pagination="getList"
130
+      />
131
+
132
+      <!-- 添加或修改参数配置对话框 -->
133
+      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
134
+         <el-form ref="configRef" :model="form" :rules="rules" label-width="80px">
135
+            <el-form-item label="参数名称" prop="configName">
136
+               <el-input v-model="form.configName" placeholder="请输入参数名称" />
137
+            </el-form-item>
138
+            <el-form-item label="参数键名" prop="configKey">
139
+               <el-input v-model="form.configKey" placeholder="请输入参数键名" />
140
+            </el-form-item>
141
+            <el-form-item label="参数键值" prop="configValue">
142
+               <el-input v-model="form.configValue" type="textarea" placeholder="请输入参数键值" />
143
+            </el-form-item>
144
+            <el-form-item label="系统内置" prop="configType">
145
+               <el-radio-group v-model="form.configType">
146
+                  <el-radio
147
+                     v-for="dict in sys_yes_no"
148
+                     :key="dict.value"
149
+                     :value="dict.value"
150
+                  >{{ dict.label }}</el-radio>
151
+               </el-radio-group>
152
+            </el-form-item>
153
+            <el-form-item label="备注" prop="remark">
154
+               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
155
+            </el-form-item>
156
+         </el-form>
157
+         <template #footer>
158
+            <div class="dialog-footer">
159
+               <el-button type="primary" @click="submitForm">确 定</el-button>
160
+               <el-button @click="cancel">取 消</el-button>
161
+            </div>
162
+         </template>
163
+      </el-dialog>
164
+   </div>
165
+</template>
166
+
167
+<script setup name="Config">
168
+import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config"
169
+
170
+const { proxy } = getCurrentInstance()
171
+const { sys_yes_no } = proxy.useDict("sys_yes_no")
172
+
173
+const configList = ref([])
174
+const open = ref(false)
175
+const loading = ref(true)
176
+const showSearch = ref(true)
177
+const ids = ref([])
178
+const single = ref(true)
179
+const multiple = ref(true)
180
+const total = ref(0)
181
+const title = ref("")
182
+const dateRange = ref([])
183
+
184
+const data = reactive({
185
+  form: {},
186
+  queryParams: {
187
+    pageNum: 1,
188
+    pageSize: 10,
189
+    configName: undefined,
190
+    configKey: undefined,
191
+    configType: undefined
192
+  },
193
+  rules: {
194
+    configName: [{ required: true, message: "参数名称不能为空", trigger: "blur" }],
195
+    configKey: [{ required: true, message: "参数键名不能为空", trigger: "blur" }],
196
+    configValue: [{ required: true, message: "参数键值不能为空", trigger: "blur" }]
197
+  }
198
+})
199
+
200
+const { queryParams, form, rules } = toRefs(data)
201
+
202
+/** 查询参数列表 */
203
+function getList() {
204
+  loading.value = true
205
+  listConfig(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
206
+    configList.value = response.rows
207
+    total.value = response.total
208
+    loading.value = false
209
+  })
210
+}
211
+
212
+/** 取消按钮 */
213
+function cancel() {
214
+  open.value = false
215
+  reset()
216
+}
217
+
218
+/** 表单重置 */
219
+function reset() {
220
+  form.value = {
221
+    configId: undefined,
222
+    configName: undefined,
223
+    configKey: undefined,
224
+    configValue: undefined,
225
+    configType: "Y",
226
+    remark: undefined
227
+  }
228
+  proxy.resetForm("configRef")
229
+}
230
+
231
+/** 搜索按钮操作 */
232
+function handleQuery() {
233
+  queryParams.value.pageNum = 1
234
+  getList()
235
+}
236
+
237
+/** 重置按钮操作 */
238
+function resetQuery() {
239
+  dateRange.value = []
240
+  proxy.resetForm("queryRef")
241
+  handleQuery()
242
+}
243
+
244
+/** 多选框选中数据 */
245
+function handleSelectionChange(selection) {
246
+  ids.value = selection.map(item => item.configId)
247
+  single.value = selection.length != 1
248
+  multiple.value = !selection.length
249
+}
250
+
251
+/** 新增按钮操作 */
252
+function handleAdd() {
253
+  reset()
254
+  open.value = true
255
+  title.value = "添加参数"
256
+}
257
+
258
+/** 修改按钮操作 */
259
+function handleUpdate(row) {
260
+  reset()
261
+  const configId = row.configId || ids.value
262
+  getConfig(configId).then(response => {
263
+    form.value = response.data
264
+    open.value = true
265
+    title.value = "修改参数"
266
+  })
267
+}
268
+
269
+/** 提交按钮 */
270
+function submitForm() {
271
+  proxy.$refs["configRef"].validate(valid => {
272
+    if (valid) {
273
+      if (form.value.configId != undefined) {
274
+        updateConfig(form.value).then(response => {
275
+          proxy.$modal.msgSuccess("修改成功")
276
+          open.value = false
277
+          getList()
278
+        })
279
+      } else {
280
+        addConfig(form.value).then(response => {
281
+          proxy.$modal.msgSuccess("新增成功")
282
+          open.value = false
283
+          getList()
284
+        })
285
+      }
286
+    }
287
+  })
288
+}
289
+
290
+/** 删除按钮操作 */
291
+function handleDelete(row) {
292
+  const configIds = row.configId || ids.value
293
+  proxy.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?').then(function () {
294
+    return delConfig(configIds)
295
+  }).then(() => {
296
+    getList()
297
+    proxy.$modal.msgSuccess("删除成功")
298
+  }).catch(() => {})
299
+}
300
+
301
+/** 导出按钮操作 */
302
+function handleExport() {
303
+  proxy.download("system/config/export", {
304
+    ...queryParams.value
305
+  }, `config_${new Date().getTime()}.xlsx`)
306
+}
307
+
308
+/** 刷新缓存按钮操作 */
309
+function handleRefreshCache() {
310
+  refreshCache().then(() => {
311
+    proxy.$modal.msgSuccess("刷新缓存成功")
312
+  })
313
+}
314
+
315
+getList()
316
+</script>

+ 314 - 0
src/views/system/dept/index.vue

@@ -0,0 +1,314 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+         <el-form-item label="部门名称" prop="deptName">
5
+            <el-input
6
+               v-model="queryParams.deptName"
7
+               placeholder="请输入部门名称"
8
+               clearable
9
+               style="width: 200px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="部门类型" prop="deptType">
14
+            <el-select v-model="queryParams.deptType" placeholder="请选择部门类型" clearable style="width: 200px">
15
+               <el-option
16
+                  v-for="dict in base_dept_type"
17
+                  :key="dict.value"
18
+                  :label="dict.label"
19
+                  :value="dict.value"
20
+               />
21
+            </el-select>
22
+         </el-form-item>
23
+         <el-form-item label="状态" prop="status">
24
+            <el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
25
+               <el-option
26
+                  v-for="dict in sys_normal_disable"
27
+                  :key="dict.value"
28
+                  :label="dict.label"
29
+                  :value="dict.value"
30
+               />
31
+            </el-select>
32
+         </el-form-item>
33
+         <el-form-item>
34
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
35
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
36
+         </el-form-item>
37
+      </el-form>
38
+
39
+      <el-row :gutter="10" class="mb8">
40
+         <el-col :span="1.5">
41
+            <el-button
42
+               type="primary"
43
+               plain
44
+               icon="Plus"
45
+               @click="handleAdd"
46
+               v-hasPermi="['system:dept:add']"
47
+            >新增</el-button>
48
+         </el-col>
49
+         <el-col :span="1.5">
50
+            <el-button
51
+               type="info"
52
+               plain
53
+               icon="Sort"
54
+               @click="toggleExpandAll"
55
+            >展开/折叠</el-button>
56
+         </el-col>
57
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
58
+      </el-row>
59
+
60
+      <el-table
61
+         v-if="refreshTable"
62
+         v-loading="loading"
63
+         :data="deptList"
64
+         row-key="deptId"
65
+         :default-expand-all="isExpandAll"
66
+         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
67
+      >
68
+         <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
69
+         <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
70
+         <el-table-column prop="status" label="状态" width="100">
71
+            <template #default="scope">
72
+               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
73
+            </template>
74
+         </el-table-column>
75
+         <el-table-column prop="status" label="部门类型" width="200">
76
+            <template #default="scope">
77
+               <dict-tag :options="base_dept_type" :value="scope.row.deptType" />
78
+            </template>
79
+         </el-table-column>
80
+         <el-table-column label="创建时间" align="center" prop="createTime" width="200">
81
+            <template #default="scope">
82
+               <span>{{ parseTime(scope.row.createTime) }}</span>
83
+            </template>
84
+         </el-table-column>
85
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
86
+            <template #default="scope">
87
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">修改</el-button>
88
+               <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">新增</el-button>
89
+               <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">删除</el-button>
90
+            </template>
91
+         </el-table-column>
92
+      </el-table>
93
+
94
+      <!-- 添加或修改部门对话框 -->
95
+      <el-dialog :title="title" v-model="open" width="600px" append-to-body>
96
+         <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
97
+            <el-row>
98
+               <el-col :span="24" v-if="form.parentId !== 0">
99
+                  <el-form-item label="上级部门" prop="parentId">
100
+                     <el-tree-select
101
+                        v-model="form.parentId"
102
+                        :data="deptOptions"
103
+                        :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
104
+                        value-key="deptId"
105
+                        placeholder="选择上级部门"
106
+                        check-strictly
107
+                     />
108
+                  </el-form-item>
109
+               </el-col>
110
+               <el-col :span="12">
111
+                  <el-form-item label="部门名称" prop="deptName">
112
+                     <el-input v-model="form.deptName" placeholder="请输入部门名称" />
113
+                  </el-form-item>
114
+               </el-col>
115
+               <el-col :span="12"> 
116
+                  <el-form-item label="部门类型" prop="deptType">
117
+                     <el-select v-model="form.deptType" placeholder="请选择部门类型">
118
+                        <el-option
119
+                        v-for="dict in base_dept_type"
120
+                        :key="dict.value"
121
+                        :label="dict.label"
122
+                        :value="dict.value"
123
+                        ></el-option>
124
+                     </el-select>
125
+                  </el-form-item>
126
+               </el-col>
127
+               <el-col :span="12">
128
+                  <el-form-item label="显示排序" prop="orderNum">
129
+                     <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
130
+                  </el-form-item>
131
+               </el-col>
132
+               <el-col :span="12">
133
+                  <el-form-item label="负责人" prop="leader">
134
+                     <el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
135
+                  </el-form-item>
136
+               </el-col>
137
+               <el-col :span="12">
138
+                  <el-form-item label="联系电话" prop="phone">
139
+                     <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
140
+                  </el-form-item>
141
+               </el-col>
142
+               <el-col :span="12">
143
+                  <el-form-item label="邮箱" prop="email">
144
+                     <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
145
+                  </el-form-item>
146
+               </el-col>
147
+               <el-col :span="12">
148
+                  <el-form-item label="部门状态">
149
+                     <el-radio-group v-model="form.status">
150
+                        <el-radio
151
+                           v-for="dict in sys_normal_disable"
152
+                           :key="dict.value"
153
+                           :value="dict.value"
154
+                        >{{ dict.label }}</el-radio>
155
+                     </el-radio-group>
156
+                  </el-form-item>
157
+               </el-col>
158
+            </el-row>
159
+         </el-form>
160
+         <template #footer>
161
+            <div class="dialog-footer">
162
+               <el-button type="primary" @click="submitForm">确 定</el-button>
163
+               <el-button @click="cancel">取 消</el-button>
164
+            </div>
165
+         </template>
166
+      </el-dialog>
167
+   </div>
168
+</template>
169
+
170
+<script setup name="Dept">
171
+import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
172
+
173
+const { proxy } = getCurrentInstance()
174
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
175
+const { base_dept_type } = proxy.useDict('base_dept_type')
176
+const deptList = ref([])
177
+const open = ref(false)
178
+const loading = ref(true)
179
+const showSearch = ref(true)
180
+const title = ref("")
181
+const deptOptions = ref([])
182
+const isExpandAll = ref(true)
183
+const refreshTable = ref(true)
184
+
185
+const data = reactive({
186
+  form: {},
187
+  queryParams: {
188
+    deptName: undefined,
189
+    status: undefined
190
+  },
191
+  rules: {
192
+    parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
193
+    deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
194
+    orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
195
+    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
196
+    deptType: [{ required: true, message: "请选择部门类型", trigger: ["blur", "change"] }],
197
+    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
198
+  },
199
+})
200
+
201
+const { queryParams, form, rules } = toRefs(data)
202
+
203
+/** 查询部门列表 */
204
+function getList() {
205
+  loading.value = true
206
+  listDept(queryParams.value).then(response => {
207
+    deptList.value = proxy.handleTree(response.data, "deptId")
208
+    loading.value = false
209
+  })
210
+}
211
+
212
+/** 取消按钮 */
213
+function cancel() {
214
+  open.value = false
215
+  reset()
216
+}
217
+
218
+/** 表单重置 */
219
+function reset() {
220
+  form.value = {
221
+    deptId: undefined,
222
+    parentId: undefined,
223
+    deptName: undefined,
224
+    deptType: undefined,
225
+    orderNum: 0,
226
+    leader: undefined,
227
+    phone: undefined,
228
+    email: undefined,
229
+    status: "0"
230
+  }
231
+  proxy.resetForm("deptRef")
232
+}
233
+
234
+/** 搜索按钮操作 */
235
+function handleQuery() {
236
+  getList()
237
+}
238
+
239
+/** 重置按钮操作 */
240
+function resetQuery() {
241
+  proxy.resetForm("queryRef")
242
+  handleQuery()
243
+}
244
+
245
+/** 新增按钮操作 */
246
+function handleAdd(row) {
247
+  reset()
248
+  listDept().then(response => {
249
+    const data = { deptId: 0, deptName: '顶级节点', children: [] }
250
+    data.children = proxy.handleTree(response.data, "deptId")
251
+    deptOptions.value.push(data)
252
+  })
253
+  if (row != undefined) {
254
+    form.value.parentId = row.deptId
255
+  }
256
+  open.value = true
257
+  title.value = "添加部门"
258
+}
259
+
260
+/** 展开/折叠操作 */
261
+function toggleExpandAll() {
262
+  refreshTable.value = false
263
+  isExpandAll.value = !isExpandAll.value
264
+  nextTick(() => {
265
+    refreshTable.value = true
266
+  })
267
+}
268
+
269
+/** 修改按钮操作 */
270
+function handleUpdate(row) {
271
+  reset()
272
+  listDeptExcludeChild(row.deptId).then(response => {
273
+    deptOptions.value = proxy.handleTree(response.data, "deptId")
274
+  })
275
+  getDept(row.deptId).then(response => {
276
+    form.value = response.data
277
+    open.value = true
278
+    title.value = "修改部门"
279
+  })
280
+}
281
+
282
+/** 提交按钮 */
283
+function submitForm() {
284
+  proxy.$refs["deptRef"].validate(valid => {
285
+    if (valid) {
286
+      if (form.value.deptId != undefined) {
287
+        updateDept(form.value).then(response => {
288
+          proxy.$modal.msgSuccess("修改成功")
289
+          open.value = false
290
+          getList()
291
+        })
292
+      } else {
293
+        addDept(form.value).then(response => {
294
+          proxy.$modal.msgSuccess("新增成功")
295
+          open.value = false
296
+          getList()
297
+        })
298
+      }
299
+    }
300
+  })
301
+}
302
+
303
+/** 删除按钮操作 */
304
+function handleDelete(row) {
305
+  proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
306
+    return delDept(row.deptId)
307
+  }).then(() => {
308
+    getList()
309
+    proxy.$modal.msgSuccess("删除成功")
310
+  }).catch(() => {})
311
+}
312
+
313
+getList()
314
+</script>

+ 362 - 0
src/views/system/dict/data.vue

@@ -0,0 +1,362 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+         <el-form-item label="字典名称" prop="dictType">
5
+            <el-select v-model="queryParams.dictType" style="width: 200px">
6
+               <el-option
7
+                  v-for="item in typeOptions"
8
+                  :key="item.dictId"
9
+                  :label="item.dictName"
10
+                  :value="item.dictType"
11
+               />
12
+            </el-select>
13
+         </el-form-item>
14
+         <el-form-item label="字典标签" prop="dictLabel">
15
+            <el-input
16
+               v-model="queryParams.dictLabel"
17
+               placeholder="请输入字典标签"
18
+               clearable
19
+               style="width: 200px"
20
+               @keyup.enter="handleQuery"
21
+            />
22
+         </el-form-item>
23
+         <el-form-item label="状态" prop="status">
24
+            <el-select v-model="queryParams.status" placeholder="数据状态" clearable style="width: 200px">
25
+               <el-option
26
+                  v-for="dict in sys_normal_disable"
27
+                  :key="dict.value"
28
+                  :label="dict.label"
29
+                  :value="dict.value"
30
+               />
31
+            </el-select>
32
+         </el-form-item>
33
+         <el-form-item>
34
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
35
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
36
+         </el-form-item>
37
+      </el-form>
38
+
39
+      <el-row :gutter="10" class="mb8">
40
+         <el-col :span="1.5">
41
+            <el-button
42
+               type="primary"
43
+               plain
44
+               icon="Plus"
45
+               @click="handleAdd"
46
+               v-hasPermi="['system:dict:add']"
47
+            >新增</el-button>
48
+         </el-col>
49
+         <el-col :span="1.5">
50
+            <el-button
51
+               type="success"
52
+               plain
53
+               icon="Edit"
54
+               :disabled="single"
55
+               @click="handleUpdate"
56
+               v-hasPermi="['system:dict:edit']"
57
+            >修改</el-button>
58
+         </el-col>
59
+         <el-col :span="1.5">
60
+            <el-button
61
+               type="danger"
62
+               plain
63
+               icon="Delete"
64
+               :disabled="multiple"
65
+               @click="handleDelete"
66
+               v-hasPermi="['system:dict:remove']"
67
+            >删除</el-button>
68
+         </el-col>
69
+         <el-col :span="1.5">
70
+            <el-button
71
+               type="warning"
72
+               plain
73
+               icon="Download"
74
+               @click="handleExport"
75
+               v-hasPermi="['system:dict:export']"
76
+            >导出</el-button>
77
+         </el-col>
78
+         <el-col :span="1.5">
79
+            <el-button
80
+               type="warning"
81
+               plain
82
+               icon="Close"
83
+               @click="handleClose"
84
+            >关闭</el-button>
85
+         </el-col>
86
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
87
+      </el-row>
88
+
89
+      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
90
+         <el-table-column type="selection" width="55" align="center" />
91
+         <el-table-column label="字典编码" align="center" prop="dictCode" />
92
+         <el-table-column label="字典标签" align="center" prop="dictLabel">
93
+            <template #default="scope">
94
+               <span v-if="(scope.row.listClass == '' || scope.row.listClass == 'default') && (scope.row.cssClass == '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
95
+               <el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
96
+            </template>
97
+         </el-table-column>
98
+         <el-table-column label="字典键值" align="center" prop="dictValue" />
99
+         <el-table-column label="字典排序" align="center" prop="dictSort" />
100
+         <el-table-column label="状态" align="center" prop="status">
101
+            <template #default="scope">
102
+               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
103
+            </template>
104
+         </el-table-column>
105
+         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
106
+         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
107
+            <template #default="scope">
108
+               <span>{{ parseTime(scope.row.createTime) }}</span>
109
+            </template>
110
+         </el-table-column>
111
+         <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
112
+            <template #default="scope">
113
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
114
+               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
115
+            </template>
116
+         </el-table-column>
117
+      </el-table>
118
+
119
+      <pagination
120
+         v-show="total > 0"
121
+         :total="total"
122
+         v-model:page="queryParams.pageNum"
123
+         v-model:limit="queryParams.pageSize"
124
+         @pagination="getList"
125
+      />
126
+
127
+      <!-- 添加或修改参数配置对话框 -->
128
+      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
129
+         <el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
130
+            <el-form-item label="字典类型">
131
+               <el-input v-model="form.dictType" :disabled="true" />
132
+            </el-form-item>
133
+            <el-form-item label="数据标签" prop="dictLabel">
134
+               <el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
135
+            </el-form-item>
136
+            <el-form-item label="数据键值" prop="dictValue">
137
+               <el-input v-model="form.dictValue" placeholder="请输入数据键值" />
138
+            </el-form-item>
139
+            <el-form-item label="样式属性" prop="cssClass">
140
+               <el-input v-model="form.cssClass" placeholder="请输入样式属性" />
141
+            </el-form-item>
142
+            <el-form-item label="显示排序" prop="dictSort">
143
+               <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
144
+            </el-form-item>
145
+            <el-form-item label="回显样式" prop="listClass">
146
+               <el-select v-model="form.listClass">
147
+                  <el-option
148
+                     v-for="item in listClassOptions"
149
+                     :key="item.value"
150
+                     :label="item.label + '(' + item.value + ')'"
151
+                     :value="item.value"
152
+                  ></el-option>
153
+               </el-select>
154
+            </el-form-item>
155
+            <el-form-item label="状态" prop="status">
156
+               <el-radio-group v-model="form.status">
157
+                  <el-radio
158
+                     v-for="dict in sys_normal_disable"
159
+                     :key="dict.value"
160
+                     :value="dict.value"
161
+                  >{{ dict.label }}</el-radio>
162
+               </el-radio-group>
163
+            </el-form-item>
164
+            <el-form-item label="备注" prop="remark">
165
+               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
166
+            </el-form-item>
167
+         </el-form>
168
+         <template #footer>
169
+            <div class="dialog-footer">
170
+               <el-button type="primary" @click="submitForm">确 定</el-button>
171
+               <el-button @click="cancel">取 消</el-button>
172
+            </div>
173
+         </template>
174
+      </el-dialog>
175
+   </div>
176
+</template>
177
+
178
+<script setup name="Data">
179
+import useDictStore from '@/store/modules/dict'
180
+import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type"
181
+import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data"
182
+
183
+const { proxy } = getCurrentInstance()
184
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
185
+
186
+const dataList = ref([])
187
+const open = ref(false)
188
+const loading = ref(true)
189
+const showSearch = ref(true)
190
+const ids = ref([])
191
+const single = ref(true)
192
+const multiple = ref(true)
193
+const total = ref(0)
194
+const title = ref("")
195
+const defaultDictType = ref("")
196
+const typeOptions = ref([])
197
+const route = useRoute()
198
+// 数据标签回显样式
199
+const listClassOptions = ref([
200
+  { value: "default", label: "默认" }, 
201
+  { value: "primary", label: "主要" }, 
202
+  { value: "success", label: "成功" },
203
+  { value: "info", label: "信息" },
204
+  { value: "warning", label: "警告" },
205
+  { value: "danger", label: "危险" }
206
+])
207
+
208
+const data = reactive({
209
+  form: {},
210
+  queryParams: {
211
+    pageNum: 1,
212
+    pageSize: 10,
213
+    dictType: undefined,
214
+    dictLabel: undefined,
215
+    status: undefined
216
+  },
217
+  rules: {
218
+    dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
219
+    dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
220
+    dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
221
+  }
222
+})
223
+
224
+const { queryParams, form, rules } = toRefs(data)
225
+
226
+/** 查询字典类型详细 */
227
+function getTypes(dictId) {
228
+  getType(dictId).then(response => {
229
+    queryParams.value.dictType = response.data.dictType
230
+    defaultDictType.value = response.data.dictType
231
+    getList()
232
+  })
233
+}
234
+
235
+/** 查询字典类型列表 */
236
+function getTypeList() {
237
+  getDictOptionselect().then(response => {
238
+    typeOptions.value = response.data
239
+  })
240
+}
241
+
242
+/** 查询字典数据列表 */
243
+function getList() {
244
+  loading.value = true
245
+  listData(queryParams.value).then(response => {
246
+    dataList.value = response.rows
247
+    total.value = response.total
248
+    loading.value = false
249
+  })
250
+}
251
+
252
+/** 取消按钮 */
253
+function cancel() {
254
+  open.value = false
255
+  reset()
256
+}
257
+
258
+/** 表单重置 */
259
+function reset() {
260
+  form.value = {
261
+    dictCode: undefined,
262
+    dictLabel: undefined,
263
+    dictValue: undefined,
264
+    cssClass: undefined,
265
+    listClass: "default",
266
+    dictSort: 0,
267
+    status: "0",
268
+    remark: undefined
269
+  }
270
+  proxy.resetForm("dataRef")
271
+}
272
+
273
+/** 搜索按钮操作 */
274
+function handleQuery() {
275
+  queryParams.value.pageNum = 1
276
+  getList()
277
+}
278
+
279
+/** 返回按钮操作 */
280
+function handleClose() {
281
+  const obj = { path: "/system/dict" }
282
+  proxy.$tab.closeOpenPage(obj)
283
+}
284
+
285
+/** 重置按钮操作 */
286
+function resetQuery() {
287
+  proxy.resetForm("queryRef")
288
+  queryParams.value.dictType = defaultDictType.value
289
+  handleQuery()
290
+}
291
+
292
+/** 新增按钮操作 */
293
+function handleAdd() {
294
+  reset()
295
+  open.value = true
296
+  title.value = "添加字典数据"
297
+  form.value.dictType = queryParams.value.dictType
298
+}
299
+
300
+/** 多选框选中数据 */
301
+function handleSelectionChange(selection) {
302
+  ids.value = selection.map(item => item.dictCode)
303
+  single.value = selection.length != 1
304
+  multiple.value = !selection.length
305
+}
306
+
307
+/** 修改按钮操作 */
308
+function handleUpdate(row) {
309
+  reset()
310
+  const dictCode = row.dictCode || ids.value
311
+  getData(dictCode).then(response => {
312
+    form.value = response.data
313
+    open.value = true
314
+    title.value = "修改字典数据"
315
+  })
316
+}
317
+
318
+/** 提交按钮 */
319
+function submitForm() {
320
+  proxy.$refs["dataRef"].validate(valid => {
321
+    if (valid) {
322
+      if (form.value.dictCode != undefined) {
323
+        updateData(form.value).then(response => {
324
+          useDictStore().removeDict(queryParams.value.dictType)
325
+          proxy.$modal.msgSuccess("修改成功")
326
+          open.value = false
327
+          getList()
328
+        })
329
+      } else {
330
+        addData(form.value).then(response => {
331
+          useDictStore().removeDict(queryParams.value.dictType)
332
+          proxy.$modal.msgSuccess("新增成功")
333
+          open.value = false
334
+          getList()
335
+        })
336
+      }
337
+    }
338
+  })
339
+}
340
+
341
+/** 删除按钮操作 */
342
+function handleDelete(row) {
343
+  const dictCodes = row.dictCode || ids.value
344
+  proxy.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?').then(function() {
345
+    return delData(dictCodes)
346
+  }).then(() => {
347
+    getList()
348
+    proxy.$modal.msgSuccess("删除成功")
349
+    useDictStore().removeDict(queryParams.value.dictType)
350
+  }).catch(() => {})
351
+}
352
+
353
+/** 导出按钮操作 */
354
+function handleExport() {
355
+  proxy.download("system/dict/data/export", {
356
+    ...queryParams.value
357
+  }, `dict_data_${new Date().getTime()}.xlsx`)
358
+}
359
+
360
+getTypes(route.params && route.params.dictId)
361
+getTypeList()
362
+</script>

+ 323 - 0
src/views/system/dict/index.vue

@@ -0,0 +1,323 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+         <el-form-item label="字典名称" prop="dictName">
5
+            <el-input
6
+               v-model="queryParams.dictName"
7
+               placeholder="请输入字典名称"
8
+               clearable
9
+               style="width: 240px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="字典类型" prop="dictType">
14
+            <el-input
15
+               v-model="queryParams.dictType"
16
+               placeholder="请输入字典类型"
17
+               clearable
18
+               style="width: 240px"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="状态" prop="status">
23
+            <el-select
24
+               v-model="queryParams.status"
25
+               placeholder="字典状态"
26
+               clearable
27
+               style="width: 240px"
28
+            >
29
+               <el-option
30
+                  v-for="dict in sys_normal_disable"
31
+                  :key="dict.value"
32
+                  :label="dict.label"
33
+                  :value="dict.value"
34
+               />
35
+            </el-select>
36
+         </el-form-item>
37
+         <el-form-item label="创建时间" style="width: 308px">
38
+            <el-date-picker
39
+               v-model="dateRange"
40
+               value-format="YYYY-MM-DD"
41
+               type="daterange"
42
+               range-separator="-"
43
+               start-placeholder="开始日期"
44
+               end-placeholder="结束日期"
45
+            ></el-date-picker>
46
+         </el-form-item>
47
+         <el-form-item>
48
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
49
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
50
+         </el-form-item>
51
+      </el-form>
52
+
53
+      <el-row :gutter="10" class="mb8">
54
+         <el-col :span="1.5">
55
+            <el-button
56
+               type="primary"
57
+               plain
58
+               icon="Plus"
59
+               @click="handleAdd"
60
+               v-hasPermi="['system:dict:add']"
61
+            >新增</el-button>
62
+         </el-col>
63
+         <el-col :span="1.5">
64
+            <el-button
65
+               type="success"
66
+               plain
67
+               icon="Edit"
68
+               :disabled="single"
69
+               @click="handleUpdate"
70
+               v-hasPermi="['system:dict:edit']"
71
+            >修改</el-button>
72
+         </el-col>
73
+         <el-col :span="1.5">
74
+            <el-button
75
+               type="danger"
76
+               plain
77
+               icon="Delete"
78
+               :disabled="multiple"
79
+               @click="handleDelete"
80
+               v-hasPermi="['system:dict:remove']"
81
+            >删除</el-button>
82
+         </el-col>
83
+         <el-col :span="1.5">
84
+            <el-button
85
+               type="warning"
86
+               plain
87
+               icon="Download"
88
+               @click="handleExport"
89
+               v-hasPermi="['system:dict:export']"
90
+            >导出</el-button>
91
+         </el-col>
92
+         <el-col :span="1.5">
93
+            <el-button
94
+               type="danger"
95
+               plain
96
+               icon="Refresh"
97
+               @click="handleRefreshCache"
98
+               v-hasPermi="['system:dict:remove']"
99
+            >刷新缓存</el-button>
100
+         </el-col>
101
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
102
+      </el-row>
103
+
104
+      <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
105
+         <el-table-column type="selection" width="55" align="center" />
106
+         <el-table-column label="字典编号" align="center" prop="dictId" />
107
+         <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true"/>
108
+         <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
109
+            <template #default="scope">
110
+               <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
111
+                  <span>{{ scope.row.dictType }}</span>
112
+               </router-link>
113
+            </template>
114
+         </el-table-column>
115
+         <el-table-column label="状态" align="center" prop="status">
116
+            <template #default="scope">
117
+               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
118
+            </template>
119
+         </el-table-column>
120
+         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
121
+         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
122
+            <template #default="scope">
123
+               <span>{{ parseTime(scope.row.createTime) }}</span>
124
+            </template>
125
+         </el-table-column>
126
+         <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
127
+            <template #default="scope">
128
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
129
+               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
130
+            </template>
131
+         </el-table-column>
132
+      </el-table>
133
+
134
+      <pagination
135
+         v-show="total > 0"
136
+         :total="total"
137
+         v-model:page="queryParams.pageNum"
138
+         v-model:limit="queryParams.pageSize"
139
+         @pagination="getList"
140
+      />
141
+
142
+      <!-- 添加或修改参数配置对话框 -->
143
+      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
144
+         <el-form ref="dictRef" :model="form" :rules="rules" label-width="80px">
145
+            <el-form-item label="字典名称" prop="dictName">
146
+               <el-input v-model="form.dictName" placeholder="请输入字典名称" />
147
+            </el-form-item>
148
+            <el-form-item label="字典类型" prop="dictType">
149
+               <el-input v-model="form.dictType" placeholder="请输入字典类型" />
150
+            </el-form-item>
151
+            <el-form-item label="状态" prop="status">
152
+               <el-radio-group v-model="form.status">
153
+                  <el-radio
154
+                     v-for="dict in sys_normal_disable"
155
+                     :key="dict.value"
156
+                     :value="dict.value"
157
+                  >{{ dict.label }}</el-radio>
158
+               </el-radio-group>
159
+            </el-form-item>
160
+            <el-form-item label="备注" prop="remark">
161
+               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
162
+            </el-form-item>
163
+         </el-form>
164
+         <template #footer>
165
+            <div class="dialog-footer">
166
+               <el-button type="primary" @click="submitForm">确 定</el-button>
167
+               <el-button @click="cancel">取 消</el-button>
168
+            </div>
169
+         </template>
170
+      </el-dialog>
171
+   </div>
172
+</template>
173
+
174
+<script setup name="Dict">
175
+import useDictStore from '@/store/modules/dict'
176
+import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type"
177
+
178
+const { proxy } = getCurrentInstance()
179
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
180
+
181
+const typeList = ref([])
182
+const open = ref(false)
183
+const loading = ref(true)
184
+const showSearch = ref(true)
185
+const ids = ref([])
186
+const single = ref(true)
187
+const multiple = ref(true)
188
+const total = ref(0)
189
+const title = ref("")
190
+const dateRange = ref([])
191
+
192
+const data = reactive({
193
+  form: {},
194
+  queryParams: {
195
+    pageNum: 1,
196
+    pageSize: 10,
197
+    dictName: undefined,
198
+    dictType: undefined,
199
+    status: undefined
200
+  },
201
+  rules: {
202
+    dictName: [{ required: true, message: "字典名称不能为空", trigger: "blur" }],
203
+    dictType: [{ required: true, message: "字典类型不能为空", trigger: "blur" }]
204
+  },
205
+})
206
+
207
+const { queryParams, form, rules } = toRefs(data)
208
+
209
+/** 查询字典类型列表 */
210
+function getList() {
211
+  loading.value = true
212
+  listType(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
213
+    typeList.value = response.rows
214
+    total.value = response.total
215
+    loading.value = false
216
+  })
217
+}
218
+
219
+/** 取消按钮 */
220
+function cancel() {
221
+  open.value = false
222
+  reset()
223
+}
224
+
225
+/** 表单重置 */
226
+function reset() {
227
+  form.value = {
228
+    dictId: undefined,
229
+    dictName: undefined,
230
+    dictType: undefined,
231
+    status: "0",
232
+    remark: undefined
233
+  }
234
+  proxy.resetForm("dictRef")
235
+}
236
+
237
+/** 搜索按钮操作 */
238
+function handleQuery() {
239
+  queryParams.value.pageNum = 1
240
+  getList()
241
+}
242
+
243
+/** 重置按钮操作 */
244
+function resetQuery() {
245
+  dateRange.value = []
246
+  proxy.resetForm("queryRef")
247
+  handleQuery()
248
+}
249
+
250
+/** 新增按钮操作 */
251
+function handleAdd() {
252
+  reset()
253
+  open.value = true
254
+  title.value = "添加字典类型"
255
+}
256
+
257
+/** 多选框选中数据 */
258
+function handleSelectionChange(selection) {
259
+  ids.value = selection.map(item => item.dictId)
260
+  single.value = selection.length != 1
261
+  multiple.value = !selection.length
262
+}
263
+
264
+/** 修改按钮操作 */
265
+function handleUpdate(row) {
266
+  reset()
267
+  const dictId = row.dictId || ids.value
268
+  getType(dictId).then(response => {
269
+    form.value = response.data
270
+    open.value = true
271
+    title.value = "修改字典类型"
272
+  })
273
+}
274
+
275
+/** 提交按钮 */
276
+function submitForm() {
277
+  proxy.$refs["dictRef"].validate(valid => {
278
+    if (valid) {
279
+      if (form.value.dictId != undefined) {
280
+        updateType(form.value).then(response => {
281
+          proxy.$modal.msgSuccess("修改成功")
282
+          open.value = false
283
+          getList()
284
+        })
285
+      } else {
286
+        addType(form.value).then(response => {
287
+          proxy.$modal.msgSuccess("新增成功")
288
+          open.value = false
289
+          getList()
290
+        })
291
+      }
292
+    }
293
+  })
294
+}
295
+
296
+/** 删除按钮操作 */
297
+function handleDelete(row) {
298
+  const dictIds = row.dictId || ids.value
299
+  proxy.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?').then(function() {
300
+    return delType(dictIds)
301
+  }).then(() => {
302
+    getList()
303
+    proxy.$modal.msgSuccess("删除成功")
304
+  }).catch(() => {})
305
+}
306
+
307
+/** 导出按钮操作 */
308
+function handleExport() {
309
+  proxy.download("system/dict/type/export", {
310
+    ...queryParams.value
311
+  }, `dict_${new Date().getTime()}.xlsx`)
312
+}
313
+
314
+/** 刷新缓存按钮操作 */
315
+function handleRefreshCache() {
316
+  refreshCache().then(() => {
317
+    proxy.$modal.msgSuccess("刷新成功")
318
+    useDictStore().cleanDict()
319
+  })
320
+}
321
+
322
+getList()
323
+</script>

+ 230 - 0
src/views/system/generator/index.vue

@@ -0,0 +1,230 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="标识" prop="stub">
5
+        <el-input
6
+          v-model="queryParams.stub"
7
+          placeholder="请输入标识"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item>
13
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
14
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
15
+      </el-form-item>
16
+    </el-form>
17
+
18
+    <el-row :gutter="10" class="mb8">
19
+      <el-col :span="1.5">
20
+        <el-button
21
+          type="primary"
22
+          plain
23
+          icon="Plus"
24
+          @click="handleAdd"
25
+          v-hasPermi="['system:generator:add']"
26
+        >新增</el-button>
27
+      </el-col>
28
+      <el-col :span="1.5">
29
+        <el-button
30
+          type="success"
31
+          plain
32
+          icon="Edit"
33
+          :disabled="single"
34
+          @click="handleUpdate"
35
+          v-hasPermi="['system:generator:edit']"
36
+        >修改</el-button>
37
+      </el-col>
38
+      <el-col :span="1.5">
39
+        <el-button
40
+          type="danger"
41
+          plain
42
+          icon="Delete"
43
+          :disabled="multiple"
44
+          @click="handleDelete"
45
+          v-hasPermi="['system:generator:remove']"
46
+        >删除</el-button>
47
+      </el-col>
48
+      <el-col :span="1.5">
49
+        <el-button
50
+          type="warning"
51
+          plain
52
+          icon="Download"
53
+          @click="handleExport"
54
+          v-hasPermi="['system:generator:export']"
55
+        >导出</el-button>
56
+      </el-col>
57
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
58
+    </el-row>
59
+
60
+    <el-table v-loading="loading" :data="generatorList" @selection-change="handleSelectionChange">
61
+      <el-table-column type="selection" width="55" align="center" />
62
+      <el-table-column label="主键" align="center" prop="id" />
63
+      <el-table-column label="标识" align="center" prop="stub" />
64
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
65
+        <template #default="scope">
66
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:generator:edit']">修改</el-button>
67
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:generator:remove']">删除</el-button>
68
+        </template>
69
+      </el-table-column>
70
+    </el-table>
71
+    
72
+    <pagination
73
+      v-show="total>0"
74
+      :total="total"
75
+      v-model:page="queryParams.pageNum"
76
+      v-model:limit="queryParams.pageSize"
77
+      @pagination="getList"
78
+    />
79
+
80
+    <!-- 添加或修改编码code自增对话框 -->
81
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
82
+      <el-form ref="generatorRef" :model="form" :rules="rules" label-width="80px">
83
+        <el-form-item label="标识" prop="stub">
84
+          <el-input v-model="form.stub" placeholder="请输入标识" />
85
+        </el-form-item>
86
+      </el-form>
87
+      <template #footer>
88
+        <div class="dialog-footer">
89
+          <el-button type="primary" @click="submitForm">确 定</el-button>
90
+          <el-button @click="cancel">取 消</el-button>
91
+        </div>
92
+      </template>
93
+    </el-dialog>
94
+  </div>
95
+</template>
96
+
97
+<script setup name="Generator">
98
+import { listGenerator, getGenerator, delGenerator, addGenerator, updateGenerator } from "@/api/system/generator"
99
+
100
+const { proxy } = getCurrentInstance()
101
+
102
+const generatorList = ref([])
103
+const open = ref(false)
104
+const loading = ref(true)
105
+const showSearch = ref(true)
106
+const ids = ref([])
107
+const single = ref(true)
108
+const multiple = ref(true)
109
+const total = ref(0)
110
+const title = ref("")
111
+
112
+const data = reactive({
113
+  form: {},
114
+  queryParams: {
115
+    pageNum: 1,
116
+    pageSize: 10,
117
+    stub: null
118
+  },
119
+  rules: {
120
+    stub: [
121
+      { required: true, message: "标识不能为空", trigger: "blur" }
122
+    ]
123
+  }
124
+})
125
+
126
+const { queryParams, form, rules } = toRefs(data)
127
+
128
+/** 查询编码code自增列表 */
129
+function getList() {
130
+  loading.value = true
131
+  listGenerator(queryParams.value).then(response => {
132
+    generatorList.value = response.rows
133
+    total.value = response.total
134
+    loading.value = false
135
+  })
136
+}
137
+
138
+// 取消按钮
139
+function cancel() {
140
+  open.value = false
141
+  reset()
142
+}
143
+
144
+// 表单重置
145
+function reset() {
146
+  form.value = {
147
+    id: null,
148
+    stub: null
149
+  }
150
+  proxy.resetForm("generatorRef")
151
+}
152
+
153
+/** 搜索按钮操作 */
154
+function handleQuery() {
155
+  queryParams.value.pageNum = 1
156
+  getList()
157
+}
158
+
159
+/** 重置按钮操作 */
160
+function resetQuery() {
161
+  proxy.resetForm("queryRef")
162
+  handleQuery()
163
+}
164
+
165
+// 多选框选中数据
166
+function handleSelectionChange(selection) {
167
+  ids.value = selection.map(item => item.id)
168
+  single.value = selection.length != 1
169
+  multiple.value = !selection.length
170
+}
171
+
172
+/** 新增按钮操作 */
173
+function handleAdd() {
174
+  reset()
175
+  open.value = true
176
+  title.value = "添加编码code自增"
177
+}
178
+
179
+/** 修改按钮操作 */
180
+function handleUpdate(row) {
181
+  reset()
182
+  const _id = row.id || ids.value
183
+  getGenerator(_id).then(response => {
184
+    form.value = response.data
185
+    open.value = true
186
+    title.value = "修改编码code自增"
187
+  })
188
+}
189
+
190
+/** 提交按钮 */
191
+function submitForm() {
192
+  proxy.$refs["generatorRef"].validate(valid => {
193
+    if (valid) {
194
+      if (form.value.id != null) {
195
+        updateGenerator(form.value).then(response => {
196
+          proxy.$modal.msgSuccess("修改成功")
197
+          open.value = false
198
+          getList()
199
+        })
200
+      } else {
201
+        addGenerator(form.value).then(response => {
202
+          proxy.$modal.msgSuccess("新增成功")
203
+          open.value = false
204
+          getList()
205
+        })
206
+      }
207
+    }
208
+  })
209
+}
210
+
211
+/** 删除按钮操作 */
212
+function handleDelete(row) {
213
+  const _ids = row.id || ids.value
214
+  proxy.$modal.confirm('是否确认删除编码code自增编号为"' + _ids + '"的数据项?').then(function() {
215
+    return delGenerator(_ids)
216
+  }).then(() => {
217
+    getList()
218
+    proxy.$modal.msgSuccess("删除成功")
219
+  }).catch(() => {})
220
+}
221
+
222
+/** 导出按钮操作 */
223
+function handleExport() {
224
+  proxy.download('system/generator/export', {
225
+    ...queryParams.value
226
+  }, `generator_${new Date().getTime()}.xlsx`)
227
+}
228
+
229
+getList()
230
+</script>

+ 338 - 0
src/views/system/item/index.vue

@@ -0,0 +1,338 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="物品编码" prop="code">
5
+        <el-input
6
+          v-model="queryParams.code"
7
+          placeholder="请输入编码"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="物品名称" prop="name">
13
+        <el-input
14
+          v-model="queryParams.name"
15
+          placeholder="请输入物品名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+      <el-form-item label="危险等级" prop="dangerLevel">
21
+        <el-select v-model="queryParams.dangerLevel" placeholder="请选择危险等级" clearable style="width: 150px">
22
+          <el-option
23
+            v-for="dict in base_item_danger_level"
24
+            :key="dict.value"
25
+            :label="dict.label"
26
+            :value="dict.value"
27
+          />
28
+        </el-select>
29
+      </el-form-item>
30
+      <el-form-item label="分类名称" prop="categoryName">
31
+        <el-input
32
+          v-model="queryParams.categoryName"
33
+          placeholder="请输入分类名称"
34
+          clearable
35
+          @keyup.enter="handleQuery"
36
+        />
37
+      </el-form-item>
38
+
39
+      <el-form-item>
40
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
41
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
42
+      </el-form-item>
43
+    </el-form>
44
+
45
+    <el-row :gutter="10" class="mb8">
46
+      <el-col :span="1.5">
47
+        <el-button
48
+          type="primary"
49
+          plain
50
+          icon="Plus"
51
+          @click="handleAdd"
52
+          v-hasPermi="['system:item:add']"
53
+        >新增</el-button>
54
+      </el-col>
55
+      <el-col :span="1.5">
56
+        <el-button
57
+          type="success"
58
+          plain
59
+          icon="Edit"
60
+          :disabled="single"
61
+          @click="handleUpdate"
62
+          v-hasPermi="['system:item:edit']"
63
+        >修改</el-button>
64
+      </el-col>
65
+      <el-col :span="1.5">
66
+        <el-button
67
+          type="danger"
68
+          plain
69
+          icon="Delete"
70
+          :disabled="multiple"
71
+          @click="handleDelete"
72
+          v-hasPermi="['system:item:remove']"
73
+        >删除</el-button>
74
+      </el-col>
75
+      <el-col :span="1.5">
76
+        <el-button
77
+          type="warning"
78
+          plain
79
+          icon="Download"
80
+          @click="handleExport"
81
+          v-hasPermi="['system:item:export']"
82
+        >导出</el-button>
83
+      </el-col>
84
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
85
+    </el-row>
86
+
87
+    <el-table v-loading="loading" :data="itemList" @selection-change="handleSelectionChange">
88
+      <el-table-column type="selection" width="55" align="center" />
89
+      <el-table-column label="物品编码" align="center" prop="code" />
90
+      <el-table-column label="物品名称" align="center" prop="name" />
91
+      <el-table-column label="分类名称" align="center" prop="categoryName" />
92
+      <el-table-column label="危险等级" align="center" prop="dangerLevel">
93
+        <template #default="scope">
94
+          <dict-tag :options="base_item_danger_level" :value="scope.row.dangerLevel"/>
95
+        </template>
96
+      </el-table-column>
97
+      <el-table-column label="备注" align="center" prop="remark" />
98
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
99
+        <template #default="scope">
100
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:item:edit']">修改</el-button>
101
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:item:remove']">删除</el-button>
102
+        </template>
103
+      </el-table-column>
104
+    </el-table>
105
+    
106
+    <pagination
107
+      v-show="total>0"
108
+      :total="total"
109
+      v-model:page="queryParams.pageNum"
110
+      v-model:limit="queryParams.pageSize"
111
+      @pagination="getList"
112
+    />
113
+
114
+    <!-- 添加或修改查获物品对话框 -->
115
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
116
+      <el-form ref="itemRef" :model="form" :rules="rules" label-width="80px">
117
+        <el-form-item label="物品编码" prop="code" v-if="form.id">
118
+          <el-input v-model="form.code" placeholder="请输入编码" disabled/>
119
+        </el-form-item>
120
+        <el-form-item label="物品名称" prop="name">
121
+          <el-input v-model="form.name" placeholder="请输入物品名称" />
122
+        </el-form-item>
123
+        <el-form-item label="危险等级" prop="dangerLevel">
124
+          <el-select v-model="form.dangerLevel" placeholder="请选择危险等级">
125
+            <el-option
126
+              v-for="dict in base_item_danger_level"
127
+              :key="dict.value"
128
+              :label="dict.label"
129
+              :value="dict.value"
130
+            ></el-option>
131
+          </el-select>
132
+        </el-form-item>
133
+        <el-form-item label="所属分类" prop="categoryCode">
134
+          <el-tree-select v-model="form.categoryCode"  
135
+            :data="enableCategoryOptions" 
136
+            :props="{ value: 'code', label: 'label', children: 'children' }" 
137
+            value-key="id" 
138
+            placeholder="请选择所属分类" 
139
+            @node-click="handleNodeClick"
140
+
141
+           />
142
+        </el-form-item>
143
+        <el-form-item label="备注" prop="remark">
144
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
145
+        </el-form-item>
146
+
147
+      </el-form>
148
+      <template #footer>
149
+        <div class="dialog-footer">
150
+          <el-button type="primary" @click="submitForm">确 定</el-button>
151
+          <el-button @click="cancel">取 消</el-button>
152
+        </div>
153
+      </template>
154
+    </el-dialog>
155
+  </div>
156
+</template>
157
+
158
+<script setup name="Item">
159
+import { listItem, getItem, delItem, addItem, updateItem } from "@/api/system/item"
160
+import { treeSelectByType } from "@/api/system/common"
161
+
162
+const { proxy } = getCurrentInstance()
163
+const { base_item_danger_level } = proxy.useDict('base_item_danger_level')
164
+
165
+const itemList = ref([])
166
+const open = ref(false)
167
+const loading = ref(true)
168
+const showSearch = ref(true)
169
+const ids = ref([])
170
+const single = ref(true)
171
+const multiple = ref(true)
172
+const total = ref(0)
173
+const title = ref("")
174
+const enableCategoryOptions = ref(undefined)
175
+
176
+const data = reactive({
177
+  form: {},
178
+  queryParams: {
179
+    pageNum: 1,
180
+    pageSize: 10,
181
+    name: null,
182
+    categoryCode: null,
183
+    dangerLevel: null,
184
+    categoryName: null,
185
+    code: null,
186
+  },
187
+  rules: {
188
+    name: [
189
+      { required: true, message: "物品名称不能为空", trigger: "blur" }
190
+    ],
191
+    categoryCode: [
192
+      { required: true, message: "分类编码不能为空", trigger: "change" }
193
+    ],
194
+    dangerLevel: [
195
+      { required: true, message: "危险等级不能为空", trigger: "change" }
196
+    ],
197
+    categoryName: [
198
+      { required: true, message: "分类名称不能为空", trigger: "blur" }
199
+    ],
200
+    code: [
201
+      { required: true, message: "编码不能为空", trigger: "blur" }
202
+    ],
203
+    dangerLevelDesc: [
204
+      { required: true, message: "危险等级名称不能为空", trigger: "blur" }
205
+    ]
206
+  }
207
+})
208
+
209
+const { queryParams, form, rules } = toRefs(data)
210
+
211
+/** 查询查获物品列表 */
212
+function getList() {
213
+  loading.value = true
214
+  listItem(queryParams.value).then(response => {
215
+    itemList.value = response.rows
216
+    total.value = response.total
217
+    loading.value = false
218
+  })
219
+}
220
+
221
+// 取消按钮
222
+function cancel() {
223
+  open.value = false
224
+  reset()
225
+}
226
+
227
+// 表单重置
228
+function reset() {
229
+  form.value = {
230
+    tenantId: null,
231
+    revision: null,
232
+    createBy: null,
233
+    createTime: null,
234
+    updateBy: null,
235
+    updateTime: null,
236
+    name: null,
237
+    categoryCode: null,
238
+    dangerLevel: null,
239
+    id: null,
240
+    categoryName: null,
241
+    remark: null,
242
+    code: null,
243
+    dangerLevelDesc: null
244
+  }
245
+  proxy.resetForm("itemRef")
246
+}
247
+
248
+/** 搜索按钮操作 */
249
+function handleQuery() {
250
+  queryParams.value.pageNum = 1
251
+  getList()
252
+}
253
+
254
+/** 重置按钮操作 */
255
+function resetQuery() {
256
+  proxy.resetForm("queryRef")
257
+  handleQuery()
258
+}
259
+
260
+// 多选框选中数据
261
+function handleSelectionChange(selection) {
262
+  ids.value = selection.map(item => item.id)
263
+  single.value = selection.length != 1
264
+  multiple.value = !selection.length
265
+}
266
+
267
+/** 新增按钮操作 */
268
+function handleAdd() {
269
+  reset()
270
+  open.value = true
271
+  title.value = "添加查获物品"
272
+}
273
+
274
+/** 修改按钮操作 */
275
+function handleUpdate(row) {
276
+  reset()
277
+  const _id = row.id || ids.value
278
+  getItem(_id).then(response => {
279
+    form.value = response.data
280
+    open.value = true
281
+    title.value = "修改查获物品"
282
+  })
283
+}
284
+
285
+/** 提交按钮 */
286
+function submitForm() {
287
+  proxy.$refs["itemRef"].validate(valid => {
288
+    if (valid) {
289
+      if (form.value.id != null) {
290
+        updateItem(form.value).then(response => {
291
+          proxy.$modal.msgSuccess("修改成功")
292
+          open.value = false
293
+          getList()
294
+        })
295
+      } else {
296
+        addItem(form.value).then(response => {
297
+          proxy.$modal.msgSuccess("新增成功")
298
+          open.value = false
299
+          getList()
300
+        })
301
+      }
302
+    }
303
+  })
304
+}
305
+
306
+/** 删除按钮操作 */
307
+function handleDelete(row) {
308
+  const _ids = row.id || ids.value
309
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
310
+    return delItem(_ids)
311
+  }).then(() => {
312
+    getList()
313
+    proxy.$modal.msgSuccess("删除成功")
314
+  }).catch(() => {})
315
+}
316
+
317
+/** 导出按钮操作 */
318
+function handleExport() {
319
+  proxy.download('system/item/export', {
320
+    ...queryParams.value
321
+  }, `item_${new Date().getTime()}.xlsx`)
322
+}
323
+
324
+/** 查询分类下拉树结构 */
325
+function getCategoryTree() {
326
+  treeSelectByType("ITEM_CATEGORY",2).then(response => {
327
+    enableCategoryOptions.value = response.data
328
+  })
329
+}
330
+
331
+function handleNodeClick(data) {
332
+  form.value.categoryName=data.label;
333
+}
334
+
335
+getCategoryTree()
336
+getList()
337
+
338
+</script>

+ 452 - 0
src/views/system/menu/index.vue

@@ -0,0 +1,452 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+         <el-form-item label="菜单名称" prop="menuName">
5
+            <el-input
6
+               v-model="queryParams.menuName"
7
+               placeholder="请输入菜单名称"
8
+               clearable
9
+               style="width: 200px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="状态" prop="status">
14
+            <el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
15
+               <el-option
16
+                  v-for="dict in sys_normal_disable"
17
+                  :key="dict.value"
18
+                  :label="dict.label"
19
+                  :value="dict.value"
20
+               />
21
+            </el-select>
22
+         </el-form-item>
23
+         <el-form-item>
24
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
25
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
26
+         </el-form-item>
27
+      </el-form>
28
+
29
+      <el-row :gutter="10" class="mb8">
30
+         <el-col :span="1.5">
31
+            <el-button
32
+               type="primary"
33
+               plain
34
+               icon="Plus"
35
+               @click="handleAdd"
36
+               v-hasPermi="['system:menu:add']"
37
+            >新增</el-button>
38
+         </el-col>
39
+         <el-col :span="1.5">
40
+            <el-button 
41
+               type="info"
42
+               plain
43
+               icon="Sort"
44
+               @click="toggleExpandAll"
45
+            >展开/折叠</el-button>
46
+         </el-col>
47
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
48
+      </el-row>
49
+
50
+      <el-table
51
+         v-if="refreshTable"
52
+         v-loading="loading"
53
+         :data="menuList"
54
+         row-key="menuId"
55
+         :default-expand-all="isExpandAll"
56
+         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
57
+      >
58
+         <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
59
+         <el-table-column prop="icon" label="图标" align="center" width="100">
60
+            <template #default="scope">
61
+               <svg-icon :icon-class="scope.row.icon" />
62
+            </template>
63
+         </el-table-column>
64
+         <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
65
+         <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
66
+         <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
67
+         <el-table-column prop="status" label="状态" width="80">
68
+            <template #default="scope">
69
+               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
70
+            </template>
71
+         </el-table-column>
72
+         <el-table-column label="创建时间" align="center" width="160" prop="createTime">
73
+            <template #default="scope">
74
+               <span>{{ parseTime(scope.row.createTime) }}</span>
75
+            </template>
76
+         </el-table-column>
77
+         <el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
78
+            <template #default="scope">
79
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button>
80
+               <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">新增</el-button>
81
+               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']">删除</el-button>
82
+            </template>
83
+         </el-table-column>
84
+      </el-table>
85
+
86
+      <!-- 添加或修改菜单对话框 -->
87
+      <el-dialog :title="title" v-model="open" width="680px" append-to-body>
88
+         <el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
89
+            <el-row>
90
+               <el-col :span="24">
91
+                  <el-form-item label="上级菜单">
92
+                     <el-tree-select
93
+                        v-model="form.parentId"
94
+                        :data="menuOptions"
95
+                        :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
96
+                        value-key="menuId"
97
+                        placeholder="选择上级菜单"
98
+                        check-strictly
99
+                     />
100
+                  </el-form-item>
101
+               </el-col>
102
+               <el-col :span="24">
103
+                  <el-form-item label="菜单类型" prop="menuType">
104
+                     <el-radio-group v-model="form.menuType">
105
+                        <el-radio value="M">目录</el-radio>
106
+                        <el-radio value="C">菜单</el-radio>
107
+                        <el-radio value="F">按钮</el-radio>
108
+                     </el-radio-group>
109
+                  </el-form-item>
110
+               </el-col>
111
+               <el-col :span="12" v-if="form.menuType != 'F'">
112
+                  <el-form-item label="菜单图标" prop="icon">
113
+                     <el-popover
114
+                        placement="bottom-start"
115
+                        :width="540"
116
+                        trigger="click"
117
+                     >
118
+                        <template #reference>
119
+                           <el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly>
120
+                              <template #prefix>
121
+                                 <svg-icon
122
+                                    v-if="form.icon"
123
+                                    :icon-class="form.icon"
124
+                                    class="el-input__icon"
125
+                                    style="height: 32px;width: 16px;"
126
+                                 />
127
+                                 <el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
128
+                              </template>
129
+                           </el-input>
130
+                        </template>
131
+                        <icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
132
+                     </el-popover>
133
+                  </el-form-item>
134
+               </el-col>
135
+               <el-col :span="12">
136
+                  <el-form-item label="显示排序" prop="orderNum">
137
+                     <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
138
+                  </el-form-item>
139
+               </el-col>
140
+               <el-col :span="12">
141
+                  <el-form-item label="菜单名称" prop="menuName">
142
+                     <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
143
+                  </el-form-item>
144
+               </el-col>
145
+               <el-col :span="12" v-if="form.menuType == 'C'">
146
+                  <el-form-item prop="routeName">
147
+                     <template #label>
148
+                        <span>
149
+                           <el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`(注意:因为router会删除名称相同路由,为避免名字的冲突,特殊情况下请自定义,保证唯一性)" placement="top">
150
+                              <el-icon><question-filled /></el-icon>
151
+                           </el-tooltip>
152
+                           路由名称
153
+                        </span>
154
+                     </template>
155
+                     <el-input v-model="form.routeName" placeholder="请输入路由名称" />
156
+                  </el-form-item>
157
+               </el-col>
158
+               <el-col :span="12" v-if="form.menuType != 'F'">
159
+                  <el-form-item>
160
+                     <template #label>
161
+                        <span>
162
+                           <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
163
+                              <el-icon><question-filled /></el-icon>
164
+                           </el-tooltip>是否外链
165
+                        </span>
166
+                     </template>
167
+                     <el-radio-group v-model="form.isFrame">
168
+                        <el-radio value="0">是</el-radio>
169
+                        <el-radio value="1">否</el-radio>
170
+                     </el-radio-group>
171
+                  </el-form-item>
172
+               </el-col>
173
+               <el-col :span="12" v-if="form.menuType != 'F'">
174
+                  <el-form-item prop="path">
175
+                     <template #label>
176
+                        <span>
177
+                           <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
178
+                              <el-icon><question-filled /></el-icon>
179
+                           </el-tooltip>
180
+                           路由地址
181
+                        </span>
182
+                     </template>
183
+                     <el-input v-model="form.path" placeholder="请输入路由地址" />
184
+                  </el-form-item>
185
+               </el-col>
186
+               <el-col :span="12" v-if="form.menuType == 'C'">
187
+                  <el-form-item prop="component">
188
+                     <template #label>
189
+                        <span>
190
+                           <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
191
+                              <el-icon><question-filled /></el-icon>
192
+                           </el-tooltip>
193
+                           组件路径
194
+                        </span>
195
+                     </template>
196
+                     <el-input v-model="form.component" placeholder="请输入组件路径" />
197
+                  </el-form-item>
198
+               </el-col>
199
+               <el-col :span="12" v-if="form.menuType != 'M'">
200
+                  <el-form-item>
201
+                     <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
202
+                     <template #label>
203
+                        <span>
204
+                           <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
205
+                              <el-icon><question-filled /></el-icon>
206
+                           </el-tooltip>
207
+                           权限字符
208
+                        </span>
209
+                     </template>
210
+                  </el-form-item>
211
+               </el-col>
212
+               <el-col :span="12" v-if="form.menuType == 'C'">
213
+                  <el-form-item>
214
+                     <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
215
+                     <template #label>
216
+                        <span>
217
+                           <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
218
+                              <el-icon><question-filled /></el-icon>
219
+                           </el-tooltip>
220
+                           路由参数
221
+                        </span>
222
+                     </template>
223
+                  </el-form-item>
224
+               </el-col>
225
+               <el-col :span="12" v-if="form.menuType == 'C'">
226
+                  <el-form-item>
227
+                     <template #label>
228
+                        <span>
229
+                           <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
230
+                              <el-icon><question-filled /></el-icon>
231
+                           </el-tooltip>
232
+                           是否缓存
233
+                        </span>
234
+                     </template>
235
+                     <el-radio-group v-model="form.isCache">
236
+                        <el-radio value="0">缓存</el-radio>
237
+                        <el-radio value="1">不缓存</el-radio>
238
+                     </el-radio-group>
239
+                  </el-form-item>
240
+               </el-col>
241
+               <el-col :span="12" v-if="form.menuType != 'F'">
242
+                  <el-form-item>
243
+                     <template #label>
244
+                        <span>
245
+                           <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
246
+                              <el-icon><question-filled /></el-icon>
247
+                           </el-tooltip>
248
+                           显示状态
249
+                        </span>
250
+                     </template>
251
+                     <el-radio-group v-model="form.visible">
252
+                        <el-radio
253
+                           v-for="dict in sys_show_hide"
254
+                           :key="dict.value"
255
+                           :value="dict.value"
256
+                        >{{ dict.label }}</el-radio>
257
+                     </el-radio-group>
258
+                  </el-form-item>
259
+               </el-col>
260
+               <el-col :span="12">
261
+                  <el-form-item>
262
+                     <template #label>
263
+                        <span>
264
+                           <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
265
+                              <el-icon><question-filled /></el-icon>
266
+                           </el-tooltip>
267
+                           菜单状态
268
+                        </span>
269
+                     </template>
270
+                     <el-radio-group v-model="form.status">
271
+                        <el-radio
272
+                           v-for="dict in sys_normal_disable"
273
+                           :key="dict.value"
274
+                           :value="dict.value"
275
+                        >{{ dict.label }}</el-radio>
276
+                     </el-radio-group>
277
+                  </el-form-item>
278
+               </el-col>
279
+            </el-row>
280
+         </el-form>
281
+         <template #footer>
282
+            <div class="dialog-footer">
283
+               <el-button type="primary" @click="submitForm">确 定</el-button>
284
+               <el-button @click="cancel">取 消</el-button>
285
+            </div>
286
+         </template>
287
+      </el-dialog>
288
+   </div>
289
+</template>
290
+
291
+<script setup name="Menu">
292
+import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu"
293
+import SvgIcon from "@/components/SvgIcon"
294
+import IconSelect from "@/components/IconSelect"
295
+
296
+const { proxy } = getCurrentInstance()
297
+const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
298
+
299
+const menuList = ref([])
300
+const open = ref(false)
301
+const loading = ref(true)
302
+const showSearch = ref(true)
303
+const title = ref("")
304
+const menuOptions = ref([])
305
+const isExpandAll = ref(false)
306
+const refreshTable = ref(true)
307
+const iconSelectRef = ref(null)
308
+
309
+const data = reactive({
310
+  form: {},
311
+  queryParams: {
312
+    menuName: undefined,
313
+    visible: undefined
314
+  },
315
+  rules: {
316
+    menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
317
+    orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
318
+    path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
319
+  },
320
+})
321
+
322
+const { queryParams, form, rules } = toRefs(data)
323
+
324
+/** 查询菜单列表 */
325
+function getList() {
326
+  loading.value = true
327
+  listMenu(queryParams.value).then(response => {
328
+    menuList.value = proxy.handleTree(response.data, "menuId")
329
+    loading.value = false
330
+  })
331
+}
332
+
333
+/** 查询菜单下拉树结构 */
334
+function getTreeselect() {
335
+  menuOptions.value = []
336
+  listMenu().then(response => {
337
+    const menu = { menuId: 0, menuName: "主类目", children: [] }
338
+    menu.children = proxy.handleTree(response.data, "menuId")
339
+    menuOptions.value.push(menu)
340
+  })
341
+}
342
+
343
+/** 取消按钮 */
344
+function cancel() {
345
+  open.value = false
346
+  reset()
347
+}
348
+
349
+/** 表单重置 */
350
+function reset() {
351
+  form.value = {
352
+    menuId: undefined,
353
+    parentId: 0,
354
+    menuName: undefined,
355
+    icon: undefined,
356
+    menuType: "M",
357
+    orderNum: undefined,
358
+    isFrame: "1",
359
+    isCache: "0",
360
+    visible: "0",
361
+    status: "0"
362
+  }
363
+  proxy.resetForm("menuRef")
364
+}
365
+
366
+/** 展示下拉图标 */
367
+function showSelectIcon() {
368
+  iconSelectRef.value.reset()
369
+}
370
+
371
+/** 选择图标 */
372
+function selected(name) {
373
+  form.value.icon = name
374
+}
375
+
376
+/** 搜索按钮操作 */
377
+function handleQuery() {
378
+  getList()
379
+}
380
+
381
+/** 重置按钮操作 */
382
+function resetQuery() {
383
+  proxy.resetForm("queryRef")
384
+  handleQuery()
385
+}
386
+
387
+/** 新增按钮操作 */
388
+function handleAdd(row) {
389
+  reset()
390
+  getTreeselect()
391
+  if (row != null && row.menuId) {
392
+    form.value.parentId = row.menuId
393
+  } else {
394
+    form.value.parentId = 0
395
+  }
396
+  open.value = true
397
+  title.value = "添加菜单"
398
+}
399
+
400
+/** 展开/折叠操作 */
401
+function toggleExpandAll() {
402
+  refreshTable.value = false
403
+  isExpandAll.value = !isExpandAll.value
404
+  nextTick(() => {
405
+    refreshTable.value = true
406
+  })
407
+}
408
+
409
+/** 修改按钮操作 */
410
+async function handleUpdate(row) {
411
+  reset()
412
+  await getTreeselect()
413
+  getMenu(row.menuId).then(response => {
414
+    form.value = response.data
415
+    open.value = true
416
+    title.value = "修改菜单"
417
+  })
418
+}
419
+
420
+/** 提交按钮 */
421
+function submitForm() {
422
+  proxy.$refs["menuRef"].validate(valid => {
423
+    if (valid) {
424
+      if (form.value.menuId != undefined) {
425
+        updateMenu(form.value).then(response => {
426
+          proxy.$modal.msgSuccess("修改成功")
427
+          open.value = false
428
+          getList()
429
+        })
430
+      } else {
431
+        addMenu(form.value).then(response => {
432
+          proxy.$modal.msgSuccess("新增成功")
433
+          open.value = false
434
+          getList()
435
+        })
436
+      }
437
+    }
438
+  })
439
+}
440
+
441
+/** 删除按钮操作 */
442
+function handleDelete(row) {
443
+  proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
444
+    return delMenu(row.menuId)
445
+  }).then(() => {
446
+    getList()
447
+    proxy.$modal.msgSuccess("删除成功")
448
+  }).catch(() => {})
449
+}
450
+
451
+getList()
452
+</script>

+ 292 - 0
src/views/system/notice/index.vue

@@ -0,0 +1,292 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+         <el-form-item label="公告标题" prop="noticeTitle">
5
+            <el-input
6
+               v-model="queryParams.noticeTitle"
7
+               placeholder="请输入公告标题"
8
+               clearable
9
+               style="width: 200px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="操作人员" prop="createBy">
14
+            <el-input
15
+               v-model="queryParams.createBy"
16
+               placeholder="请输入操作人员"
17
+               clearable
18
+               style="width: 200px"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="类型" prop="noticeType">
23
+            <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable style="width: 200px">
24
+               <el-option
25
+                  v-for="dict in sys_notice_type"
26
+                  :key="dict.value"
27
+                  :label="dict.label"
28
+                  :value="dict.value"
29
+               />
30
+            </el-select>
31
+         </el-form-item>
32
+         <el-form-item>
33
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
34
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
35
+         </el-form-item>
36
+      </el-form>
37
+
38
+      <el-row :gutter="10" class="mb8">
39
+         <el-col :span="1.5">
40
+            <el-button
41
+               type="primary"
42
+               plain
43
+               icon="Plus"
44
+               @click="handleAdd"
45
+               v-hasPermi="['system:notice:add']"
46
+            >新增</el-button>
47
+         </el-col>
48
+         <el-col :span="1.5">
49
+            <el-button
50
+               type="success"
51
+               plain
52
+               icon="Edit"
53
+               :disabled="single"
54
+               @click="handleUpdate"
55
+               v-hasPermi="['system:notice:edit']"
56
+            >修改</el-button>
57
+         </el-col>
58
+         <el-col :span="1.5">
59
+            <el-button
60
+               type="danger"
61
+               plain
62
+               icon="Delete"
63
+               :disabled="multiple"
64
+               @click="handleDelete"
65
+               v-hasPermi="['system:notice:remove']"
66
+            >删除</el-button>
67
+         </el-col>
68
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
69
+      </el-row>
70
+
71
+      <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
72
+         <el-table-column type="selection" width="55" align="center" />
73
+         <el-table-column label="序号" align="center" prop="noticeId" width="100" />
74
+         <el-table-column
75
+            label="公告标题"
76
+            align="center"
77
+            prop="noticeTitle"
78
+            :show-overflow-tooltip="true"
79
+         />
80
+         <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
81
+            <template #default="scope">
82
+               <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
83
+            </template>
84
+         </el-table-column>
85
+         <el-table-column label="状态" align="center" prop="status" width="100">
86
+            <template #default="scope">
87
+               <dict-tag :options="sys_notice_status" :value="scope.row.status" />
88
+            </template>
89
+         </el-table-column>
90
+         <el-table-column label="创建者" align="center" prop="createBy" width="100" />
91
+         <el-table-column label="创建时间" align="center" prop="createTime" width="100">
92
+            <template #default="scope">
93
+               <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
94
+            </template>
95
+         </el-table-column>
96
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
97
+            <template #default="scope">
98
+               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']">修改</el-button>
99
+               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']" >删除</el-button>
100
+            </template>
101
+         </el-table-column>
102
+      </el-table>
103
+
104
+      <pagination
105
+         v-show="total > 0"
106
+         :total="total"
107
+         v-model:page="queryParams.pageNum"
108
+         v-model:limit="queryParams.pageSize"
109
+         @pagination="getList"
110
+      />
111
+
112
+      <!-- 添加或修改公告对话框 -->
113
+      <el-dialog :title="title" v-model="open" width="780px" append-to-body>
114
+         <el-form ref="noticeRef" :model="form" :rules="rules" label-width="80px">
115
+            <el-row>
116
+               <el-col :span="12">
117
+                  <el-form-item label="公告标题" prop="noticeTitle">
118
+                     <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
119
+                  </el-form-item>
120
+               </el-col>
121
+               <el-col :span="12">
122
+                  <el-form-item label="公告类型" prop="noticeType">
123
+                     <el-select v-model="form.noticeType" placeholder="请选择">
124
+                        <el-option
125
+                           v-for="dict in sys_notice_type"
126
+                           :key="dict.value"
127
+                           :label="dict.label"
128
+                           :value="dict.value"
129
+                        ></el-option>
130
+                     </el-select>
131
+                  </el-form-item>
132
+               </el-col>
133
+               <el-col :span="24">
134
+                  <el-form-item label="状态">
135
+                     <el-radio-group v-model="form.status">
136
+                        <el-radio
137
+                           v-for="dict in sys_notice_status"
138
+                           :key="dict.value"
139
+                           :value="dict.value"
140
+                        >{{ dict.label }}</el-radio>
141
+                     </el-radio-group>
142
+                  </el-form-item>
143
+               </el-col>
144
+               <el-col :span="24">
145
+                  <el-form-item label="内容">
146
+                    <editor v-model="form.noticeContent" :min-height="192"/>
147
+                  </el-form-item>
148
+               </el-col>
149
+            </el-row>
150
+         </el-form>
151
+         <template #footer>
152
+            <div class="dialog-footer">
153
+               <el-button type="primary" @click="submitForm">确 定</el-button>
154
+               <el-button @click="cancel">取 消</el-button>
155
+            </div>
156
+         </template>
157
+      </el-dialog>
158
+   </div>
159
+</template>
160
+
161
+<script setup name="Notice">
162
+import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
163
+
164
+const { proxy } = getCurrentInstance()
165
+const { sys_notice_status, sys_notice_type } = proxy.useDict("sys_notice_status", "sys_notice_type")
166
+
167
+const noticeList = ref([])
168
+const open = ref(false)
169
+const loading = ref(true)
170
+const showSearch = ref(true)
171
+const ids = ref([])
172
+const single = ref(true)
173
+const multiple = ref(true)
174
+const total = ref(0)
175
+const title = ref("")
176
+
177
+const data = reactive({
178
+  form: {},
179
+  queryParams: {
180
+    pageNum: 1,
181
+    pageSize: 10,
182
+    noticeTitle: undefined,
183
+    createBy: undefined,
184
+    status: undefined
185
+  },
186
+  rules: {
187
+    noticeTitle: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
188
+    noticeType: [{ required: true, message: "公告类型不能为空", trigger: "change" }]
189
+  },
190
+})
191
+
192
+const { queryParams, form, rules } = toRefs(data)
193
+
194
+/** 查询公告列表 */
195
+function getList() {
196
+  loading.value = true
197
+  listNotice(queryParams.value).then(response => {
198
+    noticeList.value = response.rows
199
+    total.value = response.total
200
+    loading.value = false
201
+  })
202
+}
203
+
204
+/** 取消按钮 */
205
+function cancel() {
206
+  open.value = false
207
+  reset()
208
+}
209
+
210
+/** 表单重置 */
211
+function reset() {
212
+  form.value = {
213
+    noticeId: undefined,
214
+    noticeTitle: undefined,
215
+    noticeType: undefined,
216
+    noticeContent: undefined,
217
+    status: "0"
218
+  }
219
+  proxy.resetForm("noticeRef")
220
+}
221
+
222
+/** 搜索按钮操作 */
223
+function handleQuery() {
224
+  queryParams.value.pageNum = 1
225
+  getList()
226
+}
227
+
228
+/** 重置按钮操作 */
229
+function resetQuery() {
230
+  proxy.resetForm("queryRef")
231
+  handleQuery()
232
+}
233
+
234
+/** 多选框选中数据 */
235
+function handleSelectionChange(selection) {
236
+  ids.value = selection.map(item => item.noticeId)
237
+  single.value = selection.length != 1
238
+  multiple.value = !selection.length
239
+}
240
+
241
+/** 新增按钮操作 */
242
+function handleAdd() {
243
+  reset()
244
+  open.value = true
245
+  title.value = "添加公告"
246
+}
247
+
248
+/**修改按钮操作 */
249
+function handleUpdate(row) {
250
+  reset()
251
+  const noticeId = row.noticeId || ids.value
252
+  getNotice(noticeId).then(response => {
253
+    form.value = response.data
254
+    open.value = true
255
+    title.value = "修改公告"
256
+  })
257
+}
258
+
259
+/** 提交按钮 */
260
+function submitForm() {
261
+  proxy.$refs["noticeRef"].validate(valid => {
262
+    if (valid) {
263
+      if (form.value.noticeId != undefined) {
264
+        updateNotice(form.value).then(response => {
265
+          proxy.$modal.msgSuccess("修改成功")
266
+          open.value = false
267
+          getList()
268
+        })
269
+      } else {
270
+        addNotice(form.value).then(response => {
271
+          proxy.$modal.msgSuccess("新增成功")
272
+          open.value = false
273
+          getList()
274
+        })
275
+      }
276
+    }
277
+  })
278
+}
279
+
280
+/** 删除按钮操作 */
281
+function handleDelete(row) {
282
+  const noticeIds = row.noticeId || ids.value
283
+  proxy.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?').then(function() {
284
+    return delNotice(noticeIds)
285
+  }).then(() => {
286
+    getList()
287
+    proxy.$modal.msgSuccess("删除成功")
288
+  }).catch(() => {})
289
+}
290
+
291
+getList()
292
+</script>

+ 280 - 0
src/views/system/point/index.vue

@@ -0,0 +1,280 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+           
5
+      <el-form-item label="部位编码" prop="code">
6
+        <el-input
7
+          v-model="queryParams.code"
8
+          placeholder="请输入编码"
9
+          clearable
10
+          @keyup.enter="handleQuery"
11
+        />
12
+      </el-form-item>
13
+      <el-form-item label="部位名称" prop="name">
14
+        <el-input
15
+          v-model="queryParams.name"
16
+          placeholder="请输入部位名称"
17
+          clearable
18
+          @keyup.enter="handleQuery"
19
+        />
20
+      </el-form-item>
21
+      <el-form-item>
22
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
23
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
24
+      </el-form-item>
25
+    </el-form>
26
+
27
+    <el-row :gutter="10" class="mb8">
28
+      <el-col :span="1.5">
29
+        <el-button
30
+          type="primary"
31
+          plain
32
+          icon="Plus"
33
+          @click="handleAdd"
34
+          v-hasPermi="['system:point:add']"
35
+        >新增</el-button>
36
+      </el-col>
37
+      <el-col :span="1.5">
38
+        <el-button
39
+          type="info"
40
+          plain
41
+          icon="Sort"
42
+          @click="toggleExpandAll"
43
+        >展开/折叠</el-button>
44
+      </el-col>
45
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
46
+    </el-row>
47
+
48
+    <el-table
49
+      v-if="refreshTable"
50
+      v-loading="loading"
51
+      :data="pointList"
52
+      row-key="id"
53
+      :default-expand-all="isExpandAll"
54
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
55
+    >
56
+      <el-table-column label="部位编码" align="center" prop="code" />
57
+      <el-table-column label="部位名称" align="center" prop="name" />
58
+      <el-table-column label="显示顺序" align="center" prop="orderNum" />
59
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
60
+        <template #default="scope">
61
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:point:edit']">修改</el-button>
62
+          <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:point:add']">新增</el-button>
63
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:point:remove']">删除</el-button>
64
+        </template>
65
+      </el-table-column>
66
+    </el-table>
67
+
68
+    <!-- 添加或修改检查部位对话框 -->
69
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
70
+      <el-form ref="pointRef" :model="form" :rules="rules" label-width="80px">
71
+        <el-row>
72
+          <el-col :span="24" v-if="form.id">
73
+            <el-form-item label="部位编码" prop="code" >
74
+              <el-input v-model="form.code" placeholder="请输入编码" disabled/>
75
+            </el-form-item>
76
+          </el-col>
77
+          <el-col :span="24">
78
+            <el-form-item label="上级部位" prop="parentId">
79
+              <el-tree-select
80
+                v-model="form.parentId"
81
+                :data="pointOptions"
82
+                :props="{ value: 'id', label: 'name', children: 'children' }"
83
+                value-key="id"
84
+                placeholder="请选择上级部位"
85
+                check-strictly
86
+              />
87
+            </el-form-item>
88
+          </el-col>
89
+          <el-col :span="24">
90
+            <el-form-item label="部位名称" prop="name">
91
+              <el-input v-model="form.name" placeholder="请输入部位名称" />
92
+            </el-form-item>
93
+          </el-col>
94
+
95
+          <el-col :span="24">
96
+            <el-form-item label="显示顺序" prop="orderNum">
97
+              <el-input v-model="form.orderNum" placeholder="请输入显示顺序" />
98
+            </el-form-item>
99
+          </el-col>
100
+        </el-row>
101
+      </el-form>
102
+      <template #footer>
103
+        <div class="dialog-footer">
104
+          <el-button type="primary" @click="submitForm">确 定</el-button>
105
+          <el-button @click="cancel">取 消</el-button>
106
+        </div>
107
+      </template>
108
+    </el-dialog>
109
+  </div>
110
+</template>
111
+
112
+<script setup name="Point">
113
+import { listPoint, getPoint, delPoint, addPoint, updatePoint } from "@/api/system/point"
114
+
115
+const { proxy } = getCurrentInstance()
116
+
117
+const pointList = ref([])
118
+const pointOptions = ref([])
119
+const open = ref(false)
120
+const loading = ref(true)
121
+const showSearch = ref(true)
122
+const title = ref("")
123
+const isExpandAll = ref(true)
124
+const refreshTable = ref(true)
125
+
126
+const data = reactive({
127
+  form: {},
128
+  queryParams: {
129
+    name: null,
130
+    level: null,
131
+    orderNum: null,
132
+    code: null
133
+  },
134
+  rules: {
135
+    name: [
136
+      { required: true, message: "部位名称不能为空", trigger: "blur" }
137
+    ],
138
+    parentId: [
139
+      { required: true, message: "上级部位不能为空", trigger: "blur" }
140
+    ],
141
+    ancestors: [
142
+      { required: true, message: "祖级列表不能为空", trigger: "blur" }
143
+    ],
144
+    level: [
145
+      { required: true, message: "层级不能为空", trigger: "blur" }
146
+    ],
147
+    orderNum: [
148
+      { required: true, message: "显示顺序不能为空", trigger: "blur" }
149
+    ],
150
+  }
151
+})
152
+
153
+const { queryParams, form, rules } = toRefs(data)
154
+
155
+/** 查询检查部位列表 */
156
+function getList() {
157
+  loading.value = true
158
+  listPoint(queryParams.value).then(response => {
159
+    pointList.value = proxy.handleTree(response.data, "id", "parentId")
160
+    loading.value = false
161
+  })
162
+}
163
+
164
+/** 查询检查部位下拉树结构 */
165
+function getTreeselect() {
166
+  listPoint().then(response => {
167
+    pointOptions.value = []
168
+    const data = { id: 0, name: '顶级节点', children: [] }
169
+    data.children = proxy.handleTree(response.data, "id", "parentId")
170
+    pointOptions.value.push(data)
171
+  })
172
+}
173
+	
174
+// 取消按钮
175
+function cancel() {
176
+  open.value = false
177
+  reset()
178
+}
179
+
180
+// 表单重置
181
+function reset() {
182
+  form.value = {
183
+    tenantId: null,
184
+    revision: null,
185
+    createBy: null,
186
+    createTime: null,
187
+    updateBy: null,
188
+    updateTime: null,
189
+    id: null,
190
+    name: null,
191
+    parentId: null,
192
+    ancestors: null,
193
+    level: null,
194
+    orderNum: null,
195
+    code: null
196
+  }
197
+  proxy.resetForm("pointRef")
198
+}
199
+
200
+/** 搜索按钮操作 */
201
+function handleQuery() {
202
+  getList()
203
+}
204
+
205
+/** 重置按钮操作 */
206
+function resetQuery() {
207
+  proxy.resetForm("queryRef")
208
+  handleQuery()
209
+}
210
+
211
+/** 新增按钮操作 */
212
+function handleAdd(row) {
213
+  reset()
214
+  getTreeselect()
215
+  if (row != null && row.id) {
216
+    form.value.parentId = row.id
217
+  } else {
218
+    form.value.parentId = 0
219
+  }
220
+  open.value = true
221
+  title.value = "添加检查部位"
222
+}
223
+
224
+/** 展开/折叠操作 */
225
+function toggleExpandAll() {
226
+  refreshTable.value = false
227
+  isExpandAll.value = !isExpandAll.value
228
+  nextTick(() => {
229
+    refreshTable.value = true
230
+  })
231
+}
232
+
233
+/** 修改按钮操作 */
234
+async function handleUpdate(row) {
235
+  reset()
236
+  await getTreeselect()
237
+  if (row != null) {
238
+    form.value.parentId = row.parentId
239
+  }
240
+  getPoint(row.id).then(response => {
241
+    form.value = response.data
242
+    open.value = true
243
+    title.value = "修改检查部位"
244
+  })
245
+}
246
+
247
+/** 提交按钮 */
248
+function submitForm() {
249
+  proxy.$refs["pointRef"].validate(valid => {
250
+    if (valid) {
251
+      if (form.value.id != null) {
252
+        updatePoint(form.value).then(response => {
253
+          proxy.$modal.msgSuccess("修改成功")
254
+          open.value = false
255
+          getList()
256
+        })
257
+      } else {
258
+        addPoint(form.value).then(response => {
259
+          proxy.$modal.msgSuccess("新增成功")
260
+          open.value = false
261
+          getList()
262
+        })
263
+      }
264
+    }
265
+  })
266
+}
267
+
268
+/** 删除按钮操作 */
269
+function handleDelete(row) {
270
+  console.log("row",row)
271
+  proxy.$modal.confirm('是否确认删除检查部位编号为"' + row.code + '"的数据项?').then(function() {
272
+    return delPoint(row.id)
273
+  }).then(() => {
274
+    getList()
275
+    proxy.$modal.msgSuccess("删除成功")
276
+  }).catch(() => {})
277
+}
278
+
279
+getList()
280
+</script>

+ 366 - 0
src/views/system/position/index.vue

@@ -0,0 +1,366 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="编码" prop="code">
5
+        <el-input
6
+          v-model="queryParams.code"
7
+          placeholder="请输入编码"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="名称" prop="name">
13
+        <el-input
14
+          v-model="queryParams.name"
15
+          placeholder="请输入名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+
21
+      <el-form-item label="位置类型" prop="positionType">
22
+        <el-select v-model="queryParams.positionType" placeholder="请选择位置类型" @change="onPositionTypeChange" clearable style="width: 150px">
23
+          <el-option
24
+            v-for="dict in base_position_type"
25
+            :key="dict.value"
26
+            :label="dict.label"
27
+            :value="dict.value"
28
+          />
29
+        </el-select>
30
+      </el-form-item>
31
+      <el-form-item label="通道类型" prop="channelType">
32
+        <el-select v-model="queryParams.channelType" placeholder="请选择通道类型" clearable style="width: 150px">
33
+          <el-option
34
+            v-for="dict in base_channel_type"
35
+            :key="dict.value"
36
+            :label="dict.label"
37
+            :value="dict.value"
38
+          />
39
+        </el-select>
40
+      </el-form-item>
41
+      <el-form-item>
42
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
43
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
44
+      </el-form-item>
45
+    </el-form>
46
+
47
+    <el-row :gutter="10" class="mb8">
48
+      <el-col :span="1.5">
49
+        <el-button
50
+          type="primary"
51
+          plain
52
+          icon="Plus"
53
+          @click="handleAdd"
54
+          v-hasPermi="['system:position:add']"
55
+        >新增</el-button>
56
+      </el-col>
57
+      <el-col :span="1.5">
58
+        <el-button
59
+          type="info"
60
+          plain
61
+          icon="Sort"
62
+          @click="toggleExpandAll"
63
+        >展开/折叠</el-button>
64
+      </el-col>
65
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
66
+    </el-row>
67
+
68
+    <el-table
69
+      v-if="refreshTable"
70
+      v-loading="loading"
71
+      :data="positionList"
72
+      row-key="id"
73
+      :default-expand-all="isExpandAll"
74
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
75
+    >
76
+      <el-table-column label="编码" align="center" prop="code" />
77
+      <el-table-column label="名称" align="center" prop="name" />
78
+      <!-- <el-table-column label="层级" align="center" prop="level" /> -->
79
+      <el-table-column label="显示顺序" align="center" prop="orderNum" />
80
+      <el-table-column label="位置类型" align="center" prop="positionType">
81
+        <template #default="scope">
82
+          <dict-tag :options="base_position_type" :value="scope.row.positionType"/>
83
+        </template>
84
+      </el-table-column>
85
+      <el-table-column label="通道类型" align="center" prop="channelType">
86
+        <template #default="scope">
87
+          <dict-tag :options="base_channel_type" :value="scope.row.channelType"/>
88
+        </template>
89
+      </el-table-column>
90
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
91
+        <template #default="scope">
92
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:position:edit']">修改</el-button>
93
+          <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:position:add']">新增</el-button>
94
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:position:remove']">删除</el-button>
95
+        </template>
96
+      </el-table-column>
97
+    </el-table>
98
+
99
+    <!-- 添加或修改位置对话框 -->
100
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
101
+      <el-form ref="positionRef" :model="form" :rules="rules" label-width="80px">
102
+        <el-form-item label="编码" prop="code" v-if="form.id">
103
+          <el-input v-model="form.code" placeholder="请输入编码"  disabled/>
104
+        </el-form-item>
105
+        <el-form-item label="名称" prop="name">
106
+          <el-input v-model="form.name" placeholder="请输入名称" />
107
+        </el-form-item>
108
+        <el-form-item label="上级位置" prop="parentId">
109
+          <el-tree-select
110
+            v-model="form.parentId"
111
+            :data="positionOptions"
112
+            :props="{ value: 'id', label: 'name', children: 'children' }"
113
+            value-key="id"
114
+            placeholder="请选择上级位置"
115
+            check-strictly
116
+          />
117
+        </el-form-item>
118
+        <!-- <el-form-item label="层级" prop="level">
119
+          <el-input v-model="form.level" placeholder="请输入层级" />
120
+        </el-form-item> -->
121
+        <el-form-item label="显示顺序" prop="orderNum">
122
+          <el-input v-model="form.orderNum" placeholder="请输入显示顺序" />
123
+        </el-form-item>
124
+        <el-form-item label="位置类型" prop="positionType">
125
+          <el-select v-model="form.positionType" placeholder="请选择位置类型" @change="onPositionTypeChange">
126
+            <el-option
127
+              v-for="dict in base_position_type"
128
+              :key="dict.value"
129
+              :label="dict.label"
130
+              :value="dict.value"
131
+            ></el-option>
132
+          </el-select>
133
+        </el-form-item>
134
+        <el-form-item label="通道类型" prop="channelType" v-if="form.positionType === 'CHANNEL'">
135
+          <el-select v-model="form.channelType" placeholder="请选择通道类型" >
136
+            <el-option
137
+              v-for="dict in base_channel_type"
138
+              :key="dict.value"
139
+              :label="dict.label"
140
+              :value="dict.value"
141
+            ></el-option>
142
+          </el-select>
143
+        </el-form-item>
144
+
145
+        <!-- <el-form-item label="位置类型名称" prop="positionTypeDesc">
146
+          <el-input v-model="form.positionTypeDesc" placeholder="请输入位置类型名称" />
147
+        </el-form-item>
148
+        <el-form-item label="通道类型名称" prop="channelTypeDesc">
149
+          <el-input v-model="form.channelTypeDesc" placeholder="请输入通道类型名称" />
150
+        </el-form-item> -->
151
+      </el-form>
152
+      <template #footer>
153
+        <div class="dialog-footer">
154
+          <el-button type="primary" @click="submitForm">确 定</el-button>
155
+          <el-button @click="cancel">取 消</el-button>
156
+        </div>
157
+      </template>
158
+    </el-dialog>
159
+  </div>
160
+</template>
161
+
162
+<script setup name="Position">
163
+import { listPosition, getPosition, delPosition, addPosition, updatePosition } from "@/api/system/position"
164
+
165
+const { proxy } = getCurrentInstance()
166
+const { base_position_type, base_channel_type } = proxy.useDict('base_position_type', 'base_channel_type')
167
+
168
+const positionList = ref([])
169
+const positionOptions = ref([])
170
+const open = ref(false)
171
+const loading = ref(true)
172
+const showSearch = ref(true)
173
+const title = ref("")
174
+const isExpandAll = ref(true)
175
+const refreshTable = ref(true)
176
+
177
+const data = reactive({
178
+  form: {},
179
+  queryParams: {
180
+    name: null,
181
+    level: null,
182
+    orderNum: null,
183
+    positionType: null,
184
+    channelType: null,
185
+    code: null,
186
+    positionTypeDesc: null,
187
+    channelTypeDesc: null
188
+  },
189
+  rules: {
190
+    name: [
191
+      { required: true, message: "名称不能为空", trigger: "blur" }
192
+    ],
193
+    parentId: [
194
+      { required: true, message: "上级位置不能为空", trigger: "blur" }
195
+    ],
196
+    ancestors: [
197
+      { required: true, message: "祖级列表不能为空", trigger: "blur" }
198
+    ],
199
+    level: [
200
+      { required: true, message: "层级不能为空", trigger: "blur" }
201
+    ],
202
+    orderNum: [
203
+      { required: true, message: "显示顺序不能为空", trigger: "blur" }
204
+    ],
205
+    positionType: [
206
+      { required: true, message: "位置类型不能为空", trigger: "change" }
207
+    ],
208
+    channelType: [
209
+      { required: true, message: "通道类型不能为空", trigger: "change" }
210
+    ],
211
+    code: [
212
+      { required: true, message: "编码不能为空", trigger: "blur" }
213
+    ],
214
+    positionTypeDesc: [
215
+      { required: true, message: "位置类型名称不能为空", trigger: "blur" }
216
+    ],
217
+    channelTypeDesc: [
218
+      { required: true, message: "通道类型名称不能为空", trigger: "blur" }
219
+    ]
220
+  }
221
+})
222
+
223
+const { queryParams, form, rules } = toRefs(data)
224
+
225
+/** 查询位置列表 */
226
+function getList() {
227
+  loading.value = true
228
+  listPosition(queryParams.value).then(response => {
229
+    positionList.value = proxy.handleTree(response.data, "id", "parentId")
230
+    loading.value = false
231
+  })
232
+}
233
+
234
+/** 查询位置下拉树结构 */
235
+function getTreeselect() {
236
+  listPosition().then(response => {
237
+    positionOptions.value = []
238
+    const data = { id: 0, name: '顶级节点', children: [] }
239
+    data.children = proxy.handleTree(response.data, "id", "parentId")
240
+    positionOptions.value.push(data)
241
+  })
242
+}
243
+	
244
+// 取消按钮
245
+function cancel() {
246
+  open.value = false
247
+  reset()
248
+}
249
+
250
+// 表单重置
251
+function reset() {
252
+  form.value = {
253
+    tenantId: null,
254
+    revision: null,
255
+    createBy: null,
256
+    createTime: null,
257
+    updateBy: null,
258
+    updateTime: null,
259
+    id: null,
260
+    name: null,
261
+    parentId: null,
262
+    ancestors: null,
263
+    level: null,
264
+    orderNum: null,
265
+    positionType: null,
266
+    channelType: null,
267
+    code: null,
268
+    positionTypeDesc: null,
269
+    channelTypeDesc: null
270
+  }
271
+  proxy.resetForm("positionRef")
272
+}
273
+
274
+/** 搜索按钮操作 */
275
+function handleQuery() {
276
+  getList()
277
+}
278
+
279
+/** 重置按钮操作 */
280
+function resetQuery() {
281
+  proxy.resetForm("queryRef")
282
+  handleQuery()
283
+}
284
+
285
+/** 新增按钮操作 */
286
+function handleAdd(row) {
287
+  reset()
288
+  getTreeselect()
289
+  if (row != null && row.id) {
290
+    form.value.parentId = row.id
291
+  } else {
292
+    form.value.parentId = 0
293
+  }
294
+  open.value = true
295
+  title.value = "添加位置"
296
+}
297
+
298
+/** 展开/折叠操作 */
299
+function toggleExpandAll() {
300
+  refreshTable.value = false
301
+  isExpandAll.value = !isExpandAll.value
302
+  nextTick(() => {
303
+    refreshTable.value = true
304
+  })
305
+}
306
+
307
+/** 修改按钮操作 */
308
+async function handleUpdate(row) {
309
+  reset()
310
+  await getTreeselect()
311
+  if (row != null) {
312
+    form.value.parentId = row.parentId
313
+  }
314
+  getPosition(row.id).then(response => {
315
+    form.value = response.data
316
+    open.value = true
317
+    title.value = "修改位置"
318
+  })
319
+}
320
+
321
+/** 提交按钮 */
322
+function submitForm() {
323
+  proxy.$refs["positionRef"].validate(valid => {
324
+    if (valid) {
325
+      // 类型名称转换
326
+      form.value.positionTypeDesc = base_position_type.value.find(item => item.value === form.value.positionType).label;
327
+      if(form.value.channelType){
328
+        form.value.channelTypeDesc = base_channel_type.value.find(item => item.value === form.value.channelType).label;
329
+      }
330
+      if (form.value.id != null) {
331
+        updatePosition(form.value).then(response => {
332
+          proxy.$modal.msgSuccess("修改成功")
333
+          open.value = false
334
+          getList()
335
+        })
336
+      } else {
337
+        addPosition(form.value).then(response => {
338
+          proxy.$modal.msgSuccess("新增成功")
339
+          open.value = false
340
+          getList()
341
+        })
342
+      }
343
+    }
344
+  })
345
+}
346
+
347
+function onPositionTypeChange(val) {
348
+  // 只要不是 CHANNEL,就清空通道类型
349
+  if (val !== 'CHANNEL') {
350
+    form.value.channelType = null
351
+    form.value.channelTypeDesc = ''
352
+  }
353
+}
354
+
355
+/** 删除按钮操作 */
356
+function handleDelete(row) {
357
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
358
+    return delPosition(row.id)
359
+  }).then(() => {
360
+    getList()
361
+    proxy.$modal.msgSuccess("删除成功")
362
+  }).catch(() => {})
363
+}
364
+
365
+getList()
366
+</script>

+ 285 - 0
src/views/system/post/index.vue

@@ -0,0 +1,285 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+      <el-form-item label="岗位编码" prop="postCode">
5
+        <el-input
6
+          v-model="queryParams.postCode"
7
+          placeholder="请输入岗位编码"
8
+          clearable
9
+          style="width: 200px"
10
+          @keyup.enter="handleQuery" />
11
+      </el-form-item>
12
+      <el-form-item label="岗位名称" prop="postName">
13
+        <el-input
14
+          v-model="queryParams.postName"
15
+          placeholder="请输入岗位名称"
16
+          clearable
17
+          style="width: 200px"
18
+          @keyup.enter="handleQuery" />
19
+      </el-form-item>
20
+      <el-form-item label="状态" prop="status">
21
+        <el-select v-model="queryParams.status" placeholder="岗位状态" clearable style="width: 200px">
22
+          <el-option
23
+            v-for=" dict in sys_normal_disable "
24
+            :key="dict.value"
25
+            :label="dict.label"
26
+            :value="dict.value" />
27
+        </el-select>
28
+      </el-form-item>
29
+      <el-form-item>
30
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
31
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
32
+      </el-form-item>
33
+    </el-form>
34
+
35
+    <el-row :gutter="10" class="mb8">
36
+      <el-col :span="1.5">
37
+        <el-button
38
+          type="primary"
39
+          plain
40
+          icon="Plus"
41
+          @click="handleAdd"
42
+          v-hasPermi="[ 'system:post:add' ]">新增</el-button>
43
+      </el-col>
44
+      <el-col :span="1.5">
45
+        <el-button
46
+          type="success"
47
+          plain
48
+          icon="Edit"
49
+          :disabled="single"
50
+          @click="handleUpdate"
51
+          v-hasPermi="[ 'system:post:edit' ]">修改</el-button>
52
+      </el-col>
53
+      <el-col :span="1.5">
54
+        <el-button
55
+          type="danger"
56
+          plain
57
+          icon="Delete"
58
+          :disabled="multiple"
59
+          @click="handleDelete"
60
+          v-hasPermi="[ 'system:post:remove' ]">删除</el-button>
61
+      </el-col>
62
+      <el-col :span="1.5">
63
+        <el-button
64
+          type="warning"
65
+          plain
66
+          icon="Download"
67
+          @click="handleExport"
68
+          v-hasPermi="[ 'system:post:export' ]">导出</el-button>
69
+      </el-col>
70
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
71
+    </el-row>
72
+
73
+    <el-table 
74
+      v-loading="loading"
75
+      :data="postList"
76
+      :tree-props="treeProps"
77
+      row-key="postId"
78
+      default-expand-all 
79
+      @selection-change="handleSelectionChange"
80
+    >
81
+      <el-table-column type="selection" width="55" align="center" />
82
+      <el-table-column label="岗位编号" align="center" prop="postId" />
83
+      <el-table-column label="岗位编码" align="center" prop="postCode" />
84
+      <el-table-column label="岗位名称" align="center" prop="postName" />
85
+      <el-table-column label="岗位排序" align="center" prop="postSort" />
86
+      <el-table-column label="状态" align="center" prop="status">
87
+        <template #default=" scope ">
88
+          <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
89
+        </template>
90
+      </el-table-column>
91
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
92
+        <template #default=" scope ">
93
+          <span>{{ parseTime( scope.row.createTime ) }}</span>
94
+        </template>
95
+      </el-table-column>
96
+      <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
97
+        <template #default=" scope ">
98
+          <el-button link type="primary" icon="Edit" @click="handleUpdate( scope.row )"
99
+            v-hasPermi="[ 'system:post:edit' ]">修改</el-button>
100
+          <el-button link type="primary" icon="Delete" @click="handleDelete( scope.row )"
101
+            v-hasPermi="[ 'system:post:remove' ]">删除</el-button>
102
+        </template>
103
+      </el-table-column>
104
+    </el-table>
105
+
106
+    <!-- 添加或修改岗位对话框 -->
107
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
108
+      <el-form ref="postRef" :model="form" :rules="rules" label-width="80px">
109
+        <el-form-item label="上级岗位" prop="parentId">
110
+          <el-select v-model="form.parentId" placeholder="请选择上级岗位" clearable>
111
+            <el-option v-for="(post, index) of postList" :key="index" :label="post.postName" :value="post.postId" />
112
+          </el-select>
113
+        </el-form-item>
114
+        <el-form-item label="岗位名称" prop="postName">
115
+          <el-input v-model="form.postName" placeholder="请输入岗位名称" />
116
+        </el-form-item>
117
+        <el-form-item label="岗位编码" prop="postCode">
118
+          <el-input v-model="form.postCode" placeholder="请输入编码名称" />
119
+        </el-form-item>
120
+        <el-form-item label="岗位顺序" prop="postSort">
121
+          <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
122
+        </el-form-item>
123
+        <el-form-item label="岗位状态" prop="status">
124
+          <el-radio-group v-model="form.status">
125
+            <el-radio v-for=" dict in sys_normal_disable " :key="dict.value" :value="dict.value">{{ dict.label
126
+              }}</el-radio>
127
+          </el-radio-group>
128
+        </el-form-item>
129
+        <el-form-item label="备注" prop="remark">
130
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
131
+        </el-form-item>
132
+      </el-form>
133
+      <template #footer>
134
+        <div class="dialog-footer">
135
+          <el-button type="primary" @click="submitForm">确 定</el-button>
136
+          <el-button @click="cancel">取 消</el-button>
137
+        </div>
138
+      </template>
139
+    </el-dialog>
140
+  </div>
141
+</template>
142
+
143
+<script setup name="Post">
144
+import { addPost, delPost, getPost, updatePost, listAllTree } from "@/api/system/post"
145
+
146
+const { proxy } = getCurrentInstance()
147
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
148
+
149
+const postList = ref([])
150
+const open = ref(false)
151
+const loading = ref(true)
152
+const showSearch = ref(true)
153
+const ids = ref([])
154
+const single = ref(true)
155
+const multiple = ref(true)
156
+const title = ref("")
157
+
158
+const data = reactive({
159
+  form: {},
160
+  queryParams: {
161
+    pageNum: 1,
162
+    pageSize: 10,
163
+    postCode: undefined,
164
+    postName: undefined,
165
+    status: undefined
166
+  },
167
+  rules: {
168
+    postName: [ { required: true, message: "岗位名称不能为空", trigger: "blur" } ],
169
+    postCode: [ { required: true, message: "岗位编码不能为空", trigger: "blur" } ],
170
+    postSort: [ { required: true, message: "岗位顺序不能为空", trigger: "blur" } ],
171
+  }
172
+})
173
+
174
+const { queryParams, form, rules } = toRefs(data)
175
+
176
+const treeProps = reactive({
177
+  checkStrictly: false,
178
+})
179
+
180
+/** 查询岗位列表 */
181
+function getList () {
182
+  loading.value = true
183
+  listAllTree(queryParams.value).then(response => {
184
+    postList.value = response.data
185
+    loading.value = false
186
+  })
187
+}
188
+
189
+/** 取消按钮 */
190
+function cancel () {
191
+  open.value = false
192
+  reset()
193
+}
194
+
195
+/** 表单重置 */
196
+function reset () {
197
+  form.value = {
198
+    postId: undefined,
199
+    postCode: undefined,
200
+    postName: undefined,
201
+    postSort: 0,
202
+    status: "0",
203
+    remark: undefined
204
+  }
205
+  proxy.resetForm("postRef")
206
+}
207
+
208
+/** 搜索按钮操作 */
209
+function handleQuery () {
210
+  queryParams.value.pageNum = 1
211
+  getList()
212
+}
213
+
214
+/** 重置按钮操作 */
215
+function resetQuery () {
216
+  proxy.resetForm("queryRef")
217
+  handleQuery()
218
+}
219
+
220
+/** 多选框选中数据 */
221
+function handleSelectionChange (selection) {
222
+  ids.value = selection.map(item => item.postId)
223
+  single.value = selection.length != 1
224
+  multiple.value = !selection.length
225
+}
226
+
227
+/** 新增按钮操作 */
228
+function handleAdd () {
229
+  reset()
230
+  open.value = true
231
+  title.value = "添加岗位"
232
+}
233
+
234
+/** 修改按钮操作 */
235
+function handleUpdate (row) {
236
+  reset()
237
+  const postId = row.postId || ids.value
238
+  getPost(postId).then(response => {
239
+    form.value = response.data
240
+    open.value = true
241
+    title.value = "修改岗位"
242
+  })
243
+}
244
+
245
+/** 提交按钮 */
246
+function submitForm () {
247
+  proxy.$refs[ "postRef" ].validate(valid => {
248
+    if (valid) {
249
+      if (form.value.postId != undefined) {
250
+        updatePost(form.value).then(response => {
251
+          proxy.$modal.msgSuccess("修改成功")
252
+          open.value = false
253
+          getList()
254
+        })
255
+      } else {
256
+        addPost(form.value).then(response => {
257
+          proxy.$modal.msgSuccess("新增成功")
258
+          open.value = false
259
+          getList()
260
+        })
261
+      }
262
+    }
263
+  })
264
+}
265
+
266
+/** 删除按钮操作 */
267
+function handleDelete (row) {
268
+  const postIds = row.postId || ids.value
269
+  proxy.$modal.confirm('是否确认删除数据项?').then(function () {
270
+    return delPost(postIds)
271
+  }).then(() => {
272
+    getList()
273
+    proxy.$modal.msgSuccess("删除成功")
274
+  }).catch(() => { })
275
+}
276
+
277
+/** 导出按钮操作 */
278
+function handleExport () {
279
+  proxy.download("system/post/export", {
280
+    ...queryParams.value
281
+  }, `post_${new Date().getTime()}.xlsx`)
282
+}
283
+
284
+getList()
285
+</script>

+ 367 - 0
src/views/system/project/index.vue

@@ -0,0 +1,367 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="100px">
4
+      <el-form-item label="编码" prop="code">
5
+        <el-input
6
+          v-model="queryParams.code"
7
+          placeholder="请输入编码"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item label="检查项目名称" prop="name">
13
+        <el-input
14
+          v-model="queryParams.name"
15
+          placeholder="请输入检查项目名称"
16
+          clearable
17
+          @keyup.enter="handleQuery"
18
+        />
19
+      </el-form-item>
20
+
21
+      <el-form-item label="重要程度" prop="importance">
22
+        <el-select v-model="queryParams.importance" placeholder="请选择重要程度" clearable style="width: 180px;">
23
+          <el-option
24
+            v-for="dict in base_check_importance"
25
+            :key="dict.value"
26
+            :label="dict.label"
27
+            :value="dict.value"
28
+          />
29
+        </el-select>
30
+      </el-form-item>
31
+      <el-form-item>
32
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
33
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
34
+      </el-form-item>
35
+    </el-form>
36
+
37
+    <el-row :gutter="10" class="mb8">
38
+      <el-col :span="1.5">
39
+        <el-button
40
+          type="primary"
41
+          plain
42
+          icon="Plus"
43
+          @click="handleAdd"
44
+          v-hasPermi="['system:project:add']"
45
+        >新增</el-button>
46
+      </el-col>
47
+      <el-col :span="1.5">
48
+        <el-button
49
+          type="success"
50
+          plain
51
+          icon="Edit"
52
+          :disabled="single"
53
+          @click="handleUpdate"
54
+          v-hasPermi="['system:project:edit']"
55
+        >修改</el-button>
56
+      </el-col>
57
+      <el-col :span="1.5">
58
+        <el-button
59
+          type="danger"
60
+          plain
61
+          icon="Delete"
62
+          :disabled="multiple"
63
+          @click="handleDelete"
64
+          v-hasPermi="['system:project:remove']"
65
+        >删除</el-button>
66
+      </el-col>
67
+      <el-col :span="1.5">
68
+        <el-button
69
+          type="warning"
70
+          plain
71
+          icon="Download"
72
+          @click="handleExport"
73
+          v-hasPermi="['system:project:export']"
74
+        >导出</el-button>
75
+      </el-col>
76
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
77
+    </el-row>
78
+
79
+    <el-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
80
+      <el-table-column type="selection" width="55" align="center" />
81
+      <el-table-column label="编码" align="center" prop="code" />
82
+      <el-table-column label="检查项目名称" align="center" prop="name" />
83
+      <el-table-column label="所属分类" align="center" prop="categoryName" />
84
+      <!-- <el-table-column label="检查标准" align="center" prop="checkStandard" />
85
+      <el-table-column label="检查方法" align="center" prop="checkMethod" /> -->
86
+      <el-table-column label="重要程度" align="center" prop="importance">
87
+        <template #default="scope">
88
+          <dict-tag :options="base_check_importance" :value="scope.row.importance"/>
89
+        </template>
90
+      </el-table-column>
91
+      <!-- <el-table-column label="状态" align="center" prop="status" >
92
+        <template #default="scope">
93
+          <dict-tag :options="base_check_status" :value="scope.row.status"/>
94
+        </template>
95
+      </el-table-column> -->
96
+      <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
97
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
98
+        <template #default="scope">
99
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:project:edit']">修改</el-button>
100
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:project:remove']">删除</el-button>
101
+        </template>
102
+      </el-table-column>
103
+    </el-table>
104
+    
105
+    <pagination
106
+      v-show="total>0"
107
+      :total="total"
108
+      v-model:page="queryParams.pageNum"
109
+      v-model:limit="queryParams.pageSize"
110
+      @pagination="getList"
111
+    />
112
+
113
+    <!-- 添加或修改检查项目对话框 -->
114
+    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
115
+      <el-form ref="projectRef" :model="form" :rules="rules" label-width="120px">
116
+        <el-form-item label="编码" prop="code" v-if="form.id">
117
+          <el-input v-model="form.code" placeholder="请输入编码" disabled/>
118
+        </el-form-item>
119
+        <el-form-item label="检查项目名称" prop="name">
120
+          <el-input v-model="form.name" placeholder="请输入检查项目名称" />
121
+        </el-form-item>
122
+        <el-form-item label="所属分类" prop="categoryCode">
123
+          <el-tree-select v-model="form.categoryCode"  
124
+            :data="enableCategoryOptions" 
125
+            :props="{ value: 'code', label: 'label', children: 'children' }" 
126
+            value-key="id" 
127
+            placeholder="请选择所属分类" 
128
+            @node-click="handleNodeClick"
129
+
130
+           />
131
+        </el-form-item>
132
+        <el-form-item label="检查级别" prop="checkLevel">
133
+          <el-select v-model="form.checkLevel" placeholder="请选择检查级别" multiple>
134
+            <el-option v-for="dict in check_level" :key="dict.value" :label="dict.label" :value="dict.value" />
135
+          </el-select>
136
+        </el-form-item>
137
+        <el-form-item label="检查标准" prop="checkStandard">
138
+          <el-input v-model="form.checkStandard" type="textarea" placeholder="请输入内容" />
139
+        </el-form-item>
140
+        <el-form-item label="检查方法" prop="checkMethod">
141
+          <el-input v-model="form.checkMethod" type="textarea" placeholder="请输入内容" />
142
+        </el-form-item>
143
+        <el-form-item label="重要程度" prop="importance">
144
+          <el-select v-model="form.importance" placeholder="请选择重要程度">
145
+            <el-option
146
+              v-for="dict in base_check_importance"
147
+              :key="dict.value"
148
+              :label="dict.label"
149
+              :value="dict.value"
150
+            ></el-option>
151
+          </el-select>
152
+        </el-form-item>
153
+
154
+        <el-form-item label="备注" prop="remark">
155
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
156
+        </el-form-item>
157
+      </el-form>
158
+      <template #footer>
159
+        <div class="dialog-footer">
160
+          <el-button type="primary" @click="submitForm">确 定</el-button>
161
+          <el-button @click="cancel">取 消</el-button>
162
+        </div>
163
+      </template>
164
+    </el-dialog>
165
+  </div>
166
+</template>
167
+
168
+<script setup name="Project">
169
+import { listProject, getProject, delProject, addProject, updateProject } from "@/api/system/project"
170
+import { treeSelectByType } from "@/api/system/common"
171
+
172
+const { proxy } = getCurrentInstance()
173
+const { base_check_importance,check_level,check_checked_level } = proxy.useDict('base_check_importance','check_level','check_checked_level')
174
+
175
+const projectList = ref([])
176
+const open = ref(false)
177
+const loading = ref(true)
178
+const showSearch = ref(true)
179
+const ids = ref([])
180
+const single = ref(true)
181
+const multiple = ref(true)
182
+const total = ref(0)
183
+const title = ref("")
184
+const base_check_status = ref([{"value": "0","label":"启用"},{"value": "1","label":"停用"}])
185
+const enableCategoryOptions = ref(undefined)
186
+
187
+const data = reactive({
188
+  form: {},
189
+  queryParams: {
190
+    pageNum: 1,
191
+    pageSize: 10,
192
+    name: null,
193
+    categoryCode: null,
194
+    checkStandard: null,
195
+    checkMethod: null,
196
+    importance: null,
197
+    status: null,
198
+    categoryName: null,
199
+    code: null,
200
+    importanceDesc: null
201
+  },
202
+  rules: {
203
+    name: [
204
+      { required: true, message: "检查项目名称不能为空", trigger: "blur" }
205
+    ],
206
+    categoryCode: [
207
+      { required: true, message: "分类编码不能为空", trigger: "blur" }
208
+    ],
209
+    importance: [
210
+      { required: true, message: "重要程度不能为空", trigger: "change" }
211
+    ],
212
+    status: [
213
+      { required: true, message: "状态不能为空", trigger: "change" }
214
+    ],
215
+    categoryName: [
216
+      { required: true, message: "分类名称不能为空", trigger: "blur" }
217
+    ],
218
+    code: [
219
+      { required: true, message: "编码不能为空", trigger: "blur" }
220
+    ],
221
+    importanceDesc: [
222
+      { required: true, message: "重要程度名称不能为空", trigger: "blur" }
223
+    ]
224
+  }
225
+})
226
+
227
+const { queryParams, form, rules } = toRefs(data)
228
+
229
+/** 查询检查项目列表 */
230
+function getList() {
231
+  loading.value = true
232
+  listProject(queryParams.value).then(response => {
233
+    projectList.value = response.rows
234
+    total.value = response.total
235
+    loading.value = false
236
+  })
237
+}
238
+
239
+// 取消按钮
240
+function cancel() {
241
+  open.value = false
242
+  reset()
243
+}
244
+
245
+// 表单重置
246
+function reset() {
247
+  form.value = {
248
+    tenantId: null,
249
+    revision: null,
250
+    createBy: null,
251
+    createTime: null,
252
+    updateBy: null,
253
+    updateTime: null,
254
+    id: null,
255
+    name: null,
256
+    categoryCode: null,
257
+    checkStandard: null,
258
+    checkMethod: null,
259
+    importance: null,
260
+    status: null,
261
+    categoryName: null,
262
+    remark: null,
263
+    code: null,
264
+    importanceDesc: null
265
+  }
266
+  proxy.resetForm("projectRef")
267
+}
268
+
269
+/** 搜索按钮操作 */
270
+function handleQuery() {
271
+  queryParams.value.pageNum = 1
272
+  getList()
273
+}
274
+
275
+/** 重置按钮操作 */
276
+function resetQuery() {
277
+  proxy.resetForm("queryRef")
278
+  handleQuery()
279
+}
280
+
281
+// 多选框选中数据
282
+function handleSelectionChange(selection) {
283
+  ids.value = selection.map(item => item.id)
284
+  single.value = selection.length != 1
285
+  multiple.value = !selection.length
286
+}
287
+
288
+/** 新增按钮操作 */
289
+function handleAdd() {
290
+  reset()
291
+  open.value = true
292
+  title.value = "添加检查项目"
293
+}
294
+
295
+/** 修改按钮操作 */
296
+function handleUpdate(row) {
297
+  reset()
298
+  const _id = row.id || ids.value
299
+  getProject(_id).then(response => {
300
+    form.value = {
301
+      ...response.data,
302
+      checkLevel: response.data?.checkLevel?.split(","),
303
+    }
304
+    open.value = true
305
+    title.value = "修改检查项目"
306
+  })
307
+}
308
+
309
+/** 提交按钮 */
310
+function submitForm() {
311
+  proxy.$refs["projectRef"].validate(valid => {
312
+    if (valid) {
313
+      // 名称转换
314
+      form.value.importanceDesc = base_check_importance.value.find(item => item.value === form.value.importance).label;
315
+      // form.value.checkLevelDesc = check_level.value.find(item => item.value === form.value.checkLevel).label;
316
+      let res = {
317
+        ...form.value,
318
+        checkLevel: form.value.checkLevel.join(","),
319
+      }
320
+      if (form.value.id != null) {
321
+        updateProject(res).then(response => {
322
+          proxy.$modal.msgSuccess("修改成功")
323
+          open.value = false
324
+          getList()
325
+        })
326
+      } else {
327
+        addProject(res).then(response => {
328
+          proxy.$modal.msgSuccess("新增成功")
329
+          open.value = false
330
+          getList()
331
+        })
332
+      }
333
+    }
334
+  })
335
+}
336
+
337
+/** 删除按钮操作 */
338
+function handleDelete(row) {
339
+  const _ids = row.id || ids.value
340
+  proxy.$modal.confirm('是否确认删除数据项?').then(function() {
341
+    return delProject(_ids)
342
+  }).then(() => {
343
+    getList()
344
+    proxy.$modal.msgSuccess("删除成功")
345
+  }).catch(() => {})
346
+}
347
+
348
+/** 导出按钮操作 */
349
+function handleExport() {
350
+  proxy.download('system/project/export', {
351
+    ...queryParams.value
352
+  }, `project_${new Date().getTime()}.xlsx`)
353
+}
354
+/** 查询分类下拉树结构 */
355
+function getCategoryTree() {
356
+  treeSelectByType("CHECK_CATEGORY",2).then(response => {
357
+    enableCategoryOptions.value = response.data
358
+  })
359
+}
360
+
361
+function handleNodeClick(data) {
362
+  form.value.categoryName=data.label;
363
+}
364
+
365
+getCategoryTree()
366
+getList()
367
+</script>

+ 179 - 0
src/views/system/role/authUser.vue

@@ -0,0 +1,179 @@
1
+
2
+<template>
3
+   <div class="app-container">
4
+      <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true">
5
+         <el-form-item label="用户名称" prop="userName">
6
+            <el-input
7
+               v-model="queryParams.userName"
8
+               placeholder="请输入用户名称"
9
+               clearable
10
+               style="width: 240px"
11
+               @keyup.enter="handleQuery"
12
+            />
13
+         </el-form-item>
14
+         <el-form-item label="手机号码" prop="phonenumber">
15
+            <el-input
16
+               v-model="queryParams.phonenumber"
17
+               placeholder="请输入手机号码"
18
+               clearable
19
+               style="width: 240px"
20
+               @keyup.enter="handleQuery"
21
+            />
22
+         </el-form-item>
23
+         <el-form-item>
24
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
25
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
26
+         </el-form-item>
27
+      </el-form>
28
+
29
+      <el-row :gutter="10" class="mb8">
30
+         <el-col :span="1.5">
31
+            <el-button
32
+               type="primary"
33
+               plain
34
+               icon="Plus"
35
+               @click="openSelectUser"
36
+               v-hasPermi="['system:role:add']"
37
+            >添加用户</el-button>
38
+         </el-col>
39
+         <el-col :span="1.5">
40
+            <el-button
41
+               type="danger"
42
+               plain
43
+               icon="CircleClose"
44
+               :disabled="multiple"
45
+               @click="cancelAuthUserAll"
46
+               v-hasPermi="['system:role:remove']"
47
+            >批量取消授权</el-button>
48
+         </el-col>
49
+         <el-col :span="1.5">
50
+            <el-button 
51
+               type="warning" 
52
+               plain 
53
+               icon="Close"
54
+               @click="handleClose"
55
+            >关闭</el-button>
56
+         </el-col>
57
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
58
+      </el-row>
59
+
60
+      <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
61
+         <el-table-column type="selection" width="55" align="center" />
62
+         <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
63
+         <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
64
+         <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
65
+         <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
66
+         <el-table-column label="状态" align="center" prop="status">
67
+            <template #default="scope">
68
+               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
69
+            </template>
70
+         </el-table-column>
71
+         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
72
+            <template #default="scope">
73
+               <span>{{ parseTime(scope.row.createTime) }}</span>
74
+            </template>
75
+         </el-table-column>
76
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
77
+            <template #default="scope">
78
+               <el-button link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)" v-hasPermi="['system:role:remove']">取消授权</el-button>
79
+            </template>
80
+         </el-table-column>
81
+      </el-table>
82
+
83
+      <pagination
84
+         v-show="total > 0"
85
+         :total="total"
86
+         v-model:page="queryParams.pageNum"
87
+         v-model:limit="queryParams.pageSize"
88
+         @pagination="getList"
89
+      />
90
+      <select-user ref="selectRef" :roleId="queryParams.roleId" @ok="handleQuery" />
91
+   </div>
92
+</template>
93
+
94
+<script setup name="AuthUser">
95
+import selectUser from "./selectUser"
96
+import { allocatedUserList, authUserCancel, authUserCancelAll } from "@/api/system/role"
97
+
98
+const route = useRoute()
99
+const { proxy } = getCurrentInstance()
100
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
101
+
102
+const userList = ref([])
103
+const loading = ref(true)
104
+const showSearch = ref(true)
105
+const multiple = ref(true)
106
+const total = ref(0)
107
+const userIds = ref([])
108
+
109
+const queryParams = reactive({
110
+  pageNum: 1,
111
+  pageSize: 10,
112
+  roleId: route.params.roleId,
113
+  userName: undefined,
114
+  phonenumber: undefined,
115
+})
116
+
117
+/** 查询授权用户列表 */
118
+function getList() {
119
+  loading.value = true
120
+  allocatedUserList(queryParams).then(response => {
121
+    userList.value = response.rows
122
+    total.value = response.total
123
+    loading.value = false
124
+  })
125
+}
126
+
127
+/** 返回按钮 */
128
+function handleClose() {
129
+  const obj = { path: "/system/role" }
130
+  proxy.$tab.closeOpenPage(obj)
131
+}
132
+
133
+/** 搜索按钮操作 */
134
+function handleQuery() {
135
+  queryParams.pageNum = 1
136
+  getList()
137
+}
138
+
139
+/** 重置按钮操作 */
140
+function resetQuery() {
141
+  proxy.resetForm("queryRef")
142
+  handleQuery()
143
+}
144
+
145
+/** 多选框选中数据 */
146
+function handleSelectionChange(selection) {
147
+  userIds.value = selection.map(item => item.userId)
148
+  multiple.value = !selection.length
149
+}
150
+
151
+/** 打开授权用户表弹窗 */
152
+function openSelectUser() {
153
+  proxy.$refs["selectRef"].show()
154
+}
155
+
156
+/** 取消授权按钮操作 */
157
+function cancelAuthUser(row) {
158
+  proxy.$modal.confirm('确认要取消该用户"' + row.userName + '"角色吗?').then(function () {
159
+    return authUserCancel({ userId: row.userId, roleId: queryParams.roleId })
160
+  }).then(() => {
161
+    getList()
162
+    proxy.$modal.msgSuccess("取消授权成功")
163
+  }).catch(() => {})
164
+}
165
+
166
+/** 批量取消授权按钮操作 */
167
+function cancelAuthUserAll(row) {
168
+  const roleId = queryParams.roleId
169
+  const uIds = userIds.value.join(",")
170
+  proxy.$modal.confirm("是否取消选中用户授权数据项?").then(function () {
171
+    return authUserCancelAll({ roleId: roleId, userIds: uIds })
172
+  }).then(() => {
173
+    getList()
174
+    proxy.$modal.msgSuccess("取消授权成功")
175
+  }).catch(() => {})
176
+}
177
+
178
+getList()
179
+</script>

+ 584 - 0
src/views/system/role/index.vue

@@ -0,0 +1,584 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
4
+         <el-form-item label="角色名称" prop="roleName">
5
+            <el-input
6
+               v-model="queryParams.roleName"
7
+               placeholder="请输入角色名称"
8
+               clearable
9
+               style="width: 240px"
10
+               @keyup.enter="handleQuery"
11
+            />
12
+         </el-form-item>
13
+         <el-form-item label="权限字符" prop="roleKey">
14
+            <el-input
15
+               v-model="queryParams.roleKey"
16
+               placeholder="请输入权限字符"
17
+               clearable
18
+               style="width: 240px"
19
+               @keyup.enter="handleQuery"
20
+            />
21
+         </el-form-item>
22
+         <el-form-item label="状态" prop="status">
23
+            <el-select
24
+               v-model="queryParams.status"
25
+               placeholder="角色状态"
26
+               clearable
27
+               style="width: 240px"
28
+            >
29
+               <el-option
30
+                  v-for="dict in sys_normal_disable"
31
+                  :key="dict.value"
32
+                  :label="dict.label"
33
+                  :value="dict.value"
34
+               />
35
+            </el-select>
36
+         </el-form-item>
37
+         <el-form-item label="创建时间" style="width: 308px">
38
+            <el-date-picker
39
+               v-model="dateRange"
40
+               value-format="YYYY-MM-DD"
41
+               type="daterange"
42
+               range-separator="-"
43
+               start-placeholder="开始日期"
44
+               end-placeholder="结束日期"
45
+            ></el-date-picker>
46
+         </el-form-item>
47
+         <el-form-item>
48
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
49
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
50
+         </el-form-item>
51
+      </el-form>
52
+      <el-row :gutter="10" class="mb8">
53
+         <el-col :span="1.5">
54
+            <el-button
55
+               type="primary"
56
+               plain
57
+               icon="Plus"
58
+               @click="handleAdd"
59
+               v-hasPermi="['system:role:add']"
60
+            >新增</el-button>
61
+         </el-col>
62
+         <el-col :span="1.5">
63
+            <el-button
64
+               type="success"
65
+               plain
66
+               icon="Edit"
67
+               :disabled="single"
68
+               @click="handleUpdate"
69
+               v-hasPermi="['system:role:edit']"
70
+            >修改</el-button>
71
+         </el-col>
72
+         <el-col :span="1.5">
73
+            <el-button
74
+               type="danger"
75
+               plain
76
+               icon="Delete"
77
+               :disabled="multiple"
78
+               @click="handleDelete"
79
+               v-hasPermi="['system:role:remove']"
80
+            >删除</el-button>
81
+         </el-col>
82
+         <el-col :span="1.5">
83
+            <el-button
84
+               type="warning"
85
+               plain
86
+               icon="Download"
87
+               @click="handleExport"
88
+               v-hasPermi="['system:role:export']"
89
+            >导出</el-button>
90
+         </el-col>
91
+         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
92
+      </el-row>
93
+
94
+      <!-- 表格数据 -->
95
+      <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
96
+         <el-table-column type="selection" width="55" align="center" />
97
+         <el-table-column label="角色编号" prop="roleId" width="120" />
98
+         <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
99
+         <el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
100
+         <el-table-column label="显示顺序" prop="roleSort" width="100" />
101
+         <el-table-column label="状态" align="center" width="100">
102
+            <template #default="scope">
103
+               <el-switch
104
+                  v-model="scope.row.status"
105
+                  active-value="0"
106
+                  inactive-value="1"
107
+                  @change="handleStatusChange(scope.row)"
108
+               ></el-switch>
109
+            </template>
110
+         </el-table-column>
111
+         <el-table-column label="创建时间" align="center" prop="createTime">
112
+            <template #default="scope">
113
+               <span>{{ parseTime(scope.row.createTime) }}</span>
114
+            </template>
115
+         </el-table-column>
116
+         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
117
+            <template #default="scope">
118
+              <el-tooltip content="修改" placement="top" v-if="scope.row.roleId !== 1">
119
+                <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
120
+              </el-tooltip>
121
+              <el-tooltip content="删除" placement="top" v-if="scope.row.roleId !== 1">
122
+                <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
123
+              </el-tooltip>
124
+              <el-tooltip content="数据权限" placement="top" v-if="scope.row.roleId !== 1">
125
+                <el-button link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
126
+              </el-tooltip>
127
+              <el-tooltip content="分配用户" placement="top" v-if="scope.row.roleId !== 1">
128
+                <el-button link type="primary" icon="User" @click="handleAuthUser(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
129
+              </el-tooltip>
130
+            </template>
131
+         </el-table-column>
132
+      </el-table>
133
+
134
+      <pagination
135
+         v-show="total > 0"
136
+         :total="total"
137
+         v-model:page="queryParams.pageNum"
138
+         v-model:limit="queryParams.pageSize"
139
+         @pagination="getList"
140
+      />
141
+
142
+      <!-- 添加或修改角色配置对话框 -->
143
+      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
144
+         <el-form ref="roleRef" :model="form" :rules="rules" label-width="100px">
145
+            <el-form-item label="角色名称" prop="roleName">
146
+               <el-input v-model="form.roleName" placeholder="请输入角色名称" />
147
+            </el-form-item>
148
+            <el-form-item prop="roleKey">
149
+               <template #label>
150
+                  <span>
151
+                     <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasRole('admin')`)" placement="top">
152
+                        <el-icon><question-filled /></el-icon>
153
+                     </el-tooltip>
154
+                     权限字符
155
+                  </span>
156
+               </template>
157
+               <el-input v-model="form.roleKey" placeholder="请输入权限字符" />
158
+            </el-form-item>
159
+            <el-form-item label="角色顺序" prop="roleSort">
160
+               <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
161
+            </el-form-item>
162
+            <el-form-item label="状态">
163
+               <el-radio-group v-model="form.status">
164
+                  <el-radio
165
+                     v-for="dict in sys_normal_disable"
166
+                     :key="dict.value"
167
+                     :value="dict.value"
168
+                  >{{ dict.label }}</el-radio>
169
+               </el-radio-group>
170
+            </el-form-item>
171
+            <el-form-item label="菜单权限">
172
+               <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
173
+               <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
174
+               <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
175
+               <el-tree
176
+                  class="tree-border"
177
+                  :data="menuOptions"
178
+                  show-checkbox
179
+                  ref="menuRef"
180
+                  node-key="id"
181
+                  :check-strictly="!form.menuCheckStrictly"
182
+                  empty-text="加载中,请稍候"
183
+                  :props="{ label: 'label', children: 'children' }"
184
+               ></el-tree>
185
+            </el-form-item>
186
+            <el-form-item label="备注">
187
+               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
188
+            </el-form-item>
189
+         </el-form>
190
+         <template #footer>
191
+            <div class="dialog-footer">
192
+               <el-button type="primary" @click="submitForm">确 定</el-button>
193
+               <el-button @click="cancel">取 消</el-button>
194
+            </div>
195
+         </template>
196
+      </el-dialog>
197
+
198
+      <!-- 分配角色数据权限对话框 -->
199
+      <el-dialog :title="title" v-model="openDataScope" width="500px" append-to-body>
200
+         <el-form :model="form" label-width="80px">
201
+            <el-form-item label="角色名称">
202
+               <el-input v-model="form.roleName" :disabled="true" />
203
+            </el-form-item>
204
+            <el-form-item label="权限字符">
205
+               <el-input v-model="form.roleKey" :disabled="true" />
206
+            </el-form-item>
207
+            <el-form-item label="权限范围">
208
+               <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
209
+                  <el-option
210
+                     v-for="item in dataScopeOptions"
211
+                     :key="item.value"
212
+                     :label="item.label"
213
+                     :value="item.value"
214
+                  ></el-option>
215
+               </el-select>
216
+            </el-form-item>
217
+            <el-form-item label="数据权限" v-show="form.dataScope == 2">
218
+               <el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
219
+               <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
220
+               <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
221
+               <el-tree
222
+                  class="tree-border"
223
+                  :data="deptOptions"
224
+                  show-checkbox
225
+                  default-expand-all
226
+                  ref="deptRef"
227
+                  node-key="id"
228
+                  :check-strictly="!form.deptCheckStrictly"
229
+                  empty-text="加载中,请稍候"
230
+                  :props="{ label: 'label', children: 'children' }"
231
+               ></el-tree>
232
+            </el-form-item>
233
+         </el-form>
234
+         <template #footer>
235
+            <div class="dialog-footer">
236
+               <el-button type="primary" @click="submitDataScope">确 定</el-button>
237
+               <el-button @click="cancelDataScope">取 消</el-button>
238
+            </div>
239
+         </template>
240
+      </el-dialog>
241
+   </div>
242
+</template>
243
+
244
+<script setup name="Role">
245
+import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from "@/api/system/role"
246
+import { roleMenuTreeselect, treeselect as menuTreeselect } from "@/api/system/menu"
247
+
248
+const router = useRouter()
249
+const { proxy } = getCurrentInstance()
250
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
251
+
252
+const roleList = ref([])
253
+const open = ref(false)
254
+const loading = ref(true)
255
+const showSearch = ref(true)
256
+const ids = ref([])
257
+const single = ref(true)
258
+const multiple = ref(true)
259
+const total = ref(0)
260
+const title = ref("")
261
+const dateRange = ref([])
262
+const menuOptions = ref([])
263
+const menuExpand = ref(false)
264
+const menuNodeAll = ref(false)
265
+const deptExpand = ref(true)
266
+const deptNodeAll = ref(false)
267
+const deptOptions = ref([])
268
+const openDataScope = ref(false)
269
+const menuRef = ref(null)
270
+const deptRef = ref(null)
271
+
272
+/** 数据范围选项*/
273
+const dataScopeOptions = ref([
274
+  { value: "1", label: "全部数据权限" },
275
+  { value: "2", label: "自定数据权限" },
276
+  { value: "3", label: "本部门数据权限" },
277
+  { value: "4", label: "本部门及以下数据权限" },
278
+  { value: "5", label: "仅本人数据权限" }
279
+])
280
+
281
+const data = reactive({
282
+  form: {},
283
+  queryParams: {
284
+    pageNum: 1,
285
+    pageSize: 10,
286
+    roleName: undefined,
287
+    roleKey: undefined,
288
+    status: undefined
289
+  },
290
+  rules: {
291
+    roleName: [{ required: true, message: "角色名称不能为空", trigger: "blur" }],
292
+    roleKey: [{ required: true, message: "权限字符不能为空", trigger: "blur" }],
293
+    roleSort: [{ required: true, message: "角色顺序不能为空", trigger: "blur" }]
294
+  },
295
+})
296
+
297
+const { queryParams, form, rules } = toRefs(data)
298
+
299
+/** 查询角色列表 */
300
+function getList() {
301
+  loading.value = true
302
+  listRole(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
303
+    roleList.value = response.rows
304
+    total.value = response.total
305
+    loading.value = false
306
+  })
307
+}
308
+
309
+/** 搜索按钮操作 */
310
+function handleQuery() {
311
+  queryParams.value.pageNum = 1
312
+  getList()
313
+}
314
+
315
+/** 重置按钮操作 */
316
+function resetQuery() {
317
+  dateRange.value = []
318
+  proxy.resetForm("queryRef")
319
+  handleQuery()
320
+}
321
+
322
+/** 删除按钮操作 */
323
+function handleDelete(row) {
324
+  const roleIds = row.roleId || ids.value
325
+  proxy.$modal.confirm('是否确认删除角色编号为"' + roleIds + '"的数据项?').then(function () {
326
+    return delRole(roleIds)
327
+  }).then(() => {
328
+    getList()
329
+    proxy.$modal.msgSuccess("删除成功")
330
+  }).catch(() => {})
331
+}
332
+
333
+/** 导出按钮操作 */
334
+function handleExport() {
335
+  proxy.download("system/role/export", {
336
+    ...queryParams.value,
337
+  }, `role_${new Date().getTime()}.xlsx`)
338
+}
339
+
340
+/** 多选框选中数据 */
341
+function handleSelectionChange(selection) {
342
+  ids.value = selection.map(item => item.roleId)
343
+  single.value = selection.length != 1
344
+  multiple.value = !selection.length
345
+}
346
+
347
+/** 角色状态修改 */
348
+function handleStatusChange(row) {
349
+  let text = row.status === "0" ? "启用" : "停用"
350
+  proxy.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?').then(function () {
351
+    return changeRoleStatus(row.roleId, row.status)
352
+  }).then(() => {
353
+    proxy.$modal.msgSuccess(text + "成功")
354
+  }).catch(function () {
355
+    row.status = row.status === "0" ? "1" : "0"
356
+  })
357
+}
358
+
359
+/** 更多操作 */
360
+function handleCommand(command, row) {
361
+  switch (command) {
362
+    case "handleDataScope":
363
+      handleDataScope(row)
364
+      break
365
+    case "handleAuthUser":
366
+      handleAuthUser(row)
367
+      break
368
+    default:
369
+      break
370
+  }
371
+}
372
+
373
+/** 分配用户 */
374
+function handleAuthUser(row) {
375
+  router.push("/system/role-auth/user/" + row.roleId)
376
+}
377
+
378
+/** 查询菜单树结构 */
379
+function getMenuTreeselect() {
380
+  menuTreeselect().then(response => {
381
+    menuOptions.value = response.data
382
+  })
383
+}
384
+
385
+/** 所有部门节点数据 */
386
+function getDeptAllCheckedKeys() {
387
+  // 目前被选中的部门节点
388
+  let checkedKeys = deptRef.value.getCheckedKeys()
389
+  // 半选中的部门节点
390
+  let halfCheckedKeys = deptRef.value.getHalfCheckedKeys()
391
+  checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
392
+  return checkedKeys
393
+}
394
+
395
+/** 重置新增的表单以及其他数据  */
396
+function reset() {
397
+  if (menuRef.value != undefined) {
398
+    menuRef.value.setCheckedKeys([])
399
+  }
400
+  menuExpand.value = false
401
+  menuNodeAll.value = false
402
+  deptExpand.value = true
403
+  deptNodeAll.value = false
404
+  form.value = {
405
+    roleId: undefined,
406
+    roleName: undefined,
407
+    roleKey: undefined,
408
+    roleSort: 0,
409
+    status: "0",
410
+    menuIds: [],
411
+    deptIds: [],
412
+    menuCheckStrictly: true,
413
+    deptCheckStrictly: true,
414
+    remark: undefined
415
+  }
416
+  proxy.resetForm("roleRef")
417
+}
418
+
419
+/** 添加角色 */
420
+function handleAdd() {
421
+  reset()
422
+  getMenuTreeselect()
423
+  open.value = true
424
+  title.value = "添加角色"
425
+}
426
+
427
+/** 修改角色 */
428
+function handleUpdate(row) {
429
+  reset()
430
+  const roleId = row.roleId || ids.value
431
+  const roleMenu = getRoleMenuTreeselect(roleId)
432
+  getRole(roleId).then(response => {
433
+    form.value = response.data
434
+    form.value.roleSort = Number(form.value.roleSort)
435
+    open.value = true
436
+    nextTick(() => {
437
+      roleMenu.then((res) => {
438
+        let checkedKeys = res.checkedKeys
439
+        checkedKeys.forEach((v) => {
440
+          nextTick(() => {
441
+            menuRef.value.setChecked(v, true, false)
442
+          })
443
+        })
444
+      })
445
+    })
446
+  })
447
+  title.value = "修改角色"
448
+}
449
+
450
+/** 根据角色ID查询菜单树结构 */
451
+function getRoleMenuTreeselect(roleId) {
452
+  return roleMenuTreeselect(roleId).then(response => {
453
+    menuOptions.value = response.menus
454
+    return response
455
+  })
456
+}
457
+
458
+/** 根据角色ID查询部门树结构 */
459
+function getDeptTree(roleId) {
460
+  return deptTreeSelect(roleId).then(response => {
461
+    deptOptions.value = response.depts
462
+    return response
463
+  })
464
+}
465
+
466
+/** 树权限(展开/折叠)*/
467
+function handleCheckedTreeExpand(value, type) {
468
+  if (type == "menu") {
469
+    let treeList = menuOptions.value
470
+    for (let i = 0; i < treeList.length; i++) {
471
+      menuRef.value.store.nodesMap[treeList[i].id].expanded = value
472
+    }
473
+  } else if (type == "dept") {
474
+    let treeList = deptOptions.value
475
+    for (let i = 0; i < treeList.length; i++) {
476
+      deptRef.value.store.nodesMap[treeList[i].id].expanded = value
477
+    }
478
+  }
479
+}
480
+
481
+/** 树权限(全选/全不选) */
482
+function handleCheckedTreeNodeAll(value, type) {
483
+  if (type == "menu") {
484
+    menuRef.value.setCheckedNodes(value ? menuOptions.value : [])
485
+  } else if (type == "dept") {
486
+    deptRef.value.setCheckedNodes(value ? deptOptions.value : [])
487
+  }
488
+}
489
+
490
+/** 树权限(父子联动) */
491
+function handleCheckedTreeConnect(value, type) {
492
+  if (type == "menu") {
493
+    form.value.menuCheckStrictly = value ? true : false
494
+  } else if (type == "dept") {
495
+    form.value.deptCheckStrictly = value ? true : false
496
+  }
497
+}
498
+
499
+/** 所有菜单节点数据 */
500
+function getMenuAllCheckedKeys() {
501
+  // 目前被选中的菜单节点
502
+  let checkedKeys = menuRef.value.getCheckedKeys()
503
+  // 半选中的菜单节点
504
+  let halfCheckedKeys = menuRef.value.getHalfCheckedKeys()
505
+  checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
506
+  return checkedKeys
507
+}
508
+
509
+/** 提交按钮 */
510
+function submitForm() {
511
+  proxy.$refs["roleRef"].validate(valid => {
512
+    if (valid) {
513
+      if (form.value.roleId != undefined) {
514
+        form.value.menuIds = getMenuAllCheckedKeys()
515
+        updateRole(form.value).then(response => {
516
+          proxy.$modal.msgSuccess("修改成功")
517
+          open.value = false
518
+          getList()
519
+        })
520
+      } else {
521
+        form.value.menuIds = getMenuAllCheckedKeys()
522
+        addRole(form.value).then(response => {
523
+          proxy.$modal.msgSuccess("新增成功")
524
+          open.value = false
525
+          getList()
526
+        })
527
+      }
528
+    }
529
+  })
530
+}
531
+
532
+/** 取消按钮 */
533
+function cancel() {
534
+  open.value = false
535
+  reset()
536
+}
537
+
538
+/** 选择角色权限范围触发 */
539
+function dataScopeSelectChange(value) {
540
+  if (value !== "2") {
541
+    deptRef.value.setCheckedKeys([])
542
+  }
543
+}
544
+
545
+/** 分配数据权限操作 */
546
+function handleDataScope(row) {
547
+  reset()
548
+  const deptTreeSelect = getDeptTree(row.roleId)
549
+  getRole(row.roleId).then(response => {
550
+    form.value = response.data
551
+    openDataScope.value = true
552
+    nextTick(() => {
553
+      deptTreeSelect.then(res => {
554
+        nextTick(() => {
555
+          if (deptRef.value) {
556
+            deptRef.value.setCheckedKeys(res.checkedKeys)
557
+          }
558
+        })
559
+      })
560
+    })
561
+  })
562
+  title.value = "分配数据权限"
563
+}
564
+
565
+/** 提交按钮(数据权限) */
566
+function submitDataScope() {
567
+  if (form.value.roleId != undefined) {
568
+    form.value.deptIds = getDeptAllCheckedKeys()
569
+    dataScope(form.value).then(response => {
570
+      proxy.$modal.msgSuccess("修改成功")
571
+      openDataScope.value = false
572
+      getList()
573
+    })
574
+  }
575
+}
576
+
577
+/** 取消按钮(数据权限)*/
578
+function cancelDataScope() {
579
+  openDataScope.value = false
580
+  reset()
581
+}
582
+
583
+getList()
584
+</script>

+ 144 - 0
src/views/system/role/selectUser.vue

@@ -0,0 +1,144 @@
1
+<template>
2
+   <!-- 授权用户 -->
3
+   <el-dialog title="选择用户" v-model="visible" width="800px" top="5vh" append-to-body>
4
+      <el-form :model="queryParams" ref="queryRef" :inline="true">
5
+         <el-form-item label="用户名称" prop="userName">
6
+            <el-input
7
+               v-model="queryParams.userName"
8
+               placeholder="请输入用户名称"
9
+               clearable
10
+               style="width: 180px"
11
+               @keyup.enter="handleQuery"
12
+            />
13
+         </el-form-item>
14
+         <el-form-item label="手机号码" prop="phonenumber">
15
+            <el-input
16
+               v-model="queryParams.phonenumber"
17
+               placeholder="请输入手机号码"
18
+               clearable
19
+               style="width: 180px"
20
+               @keyup.enter="handleQuery"
21
+            />
22
+         </el-form-item>
23
+         <el-form-item>
24
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
25
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
26
+         </el-form-item>
27
+      </el-form>
28
+      <el-row>
29
+         <el-table @row-click="clickRow" ref="refTable" :data="userList" @selection-change="handleSelectionChange" height="260px">
30
+            <el-table-column type="selection" width="55"></el-table-column>
31
+            <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
32
+            <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
33
+            <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
34
+            <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
35
+            <el-table-column label="状态" align="center" prop="status">
36
+               <template #default="scope">
37
+                  <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
38
+               </template>
39
+            </el-table-column>
40
+            <el-table-column label="创建时间" align="center" prop="createTime" width="180">
41
+               <template #default="scope">
42
+                  <span>{{ parseTime(scope.row.createTime) }}</span>
43
+               </template>
44
+            </el-table-column>
45
+         </el-table>
46
+         <pagination
47
+            v-show="total > 0"
48
+            :total="total"
49
+            v-model:page="queryParams.pageNum"
50
+            v-model:limit="queryParams.pageSize"
51
+            @pagination="getList"
52
+         />
53
+      </el-row>
54
+      <template #footer>
55
+         <div class="dialog-footer">
56
+            <el-button type="primary" @click="handleSelectUser">确 定</el-button>
57
+            <el-button @click="visible = false">取 消</el-button>
58
+         </div>
59
+      </template>
60
+   </el-dialog>
61
+</template>
62
+
63
+<script setup name="SelectUser">
64
+import { authUserSelectAll, unallocatedUserList } from "@/api/system/role"
65
+
66
+const props = defineProps({
67
+  roleId: {
68
+    type: [Number, String]
69
+  }
70
+})
71
+
72
+const { proxy } = getCurrentInstance()
73
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
74
+
75
+const userList = ref([])
76
+const visible = ref(false)
77
+const total = ref(0)
78
+const userIds = ref([])
79
+
80
+const queryParams = reactive({
81
+  pageNum: 1,
82
+  pageSize: 10,
83
+  roleId: undefined,
84
+  userName: undefined,
85
+  phonenumber: undefined
86
+})
87
+
88
+// 显示弹框
89
+function show() {
90
+  queryParams.roleId = props.roleId
91
+  getList()
92
+  visible.value = true
93
+}
94
+
95
+/**选择行 */
96
+function clickRow(row) {
97
+  proxy.$refs["refTable"].toggleRowSelection(row)
98
+}
99
+
100
+// 多选框选中数据
101
+function handleSelectionChange(selection) {
102
+  userIds.value = selection.map(item => item.userId)
103
+}
104
+
105
+// 查询表数据
106
+function getList() {
107
+  unallocatedUserList(queryParams).then(res => {
108
+    userList.value = res.rows
109
+    total.value = res.total
110
+  })
111
+}
112
+
113
+/** 搜索按钮操作 */
114
+function handleQuery() {
115
+  queryParams.pageNum = 1
116
+  getList()
117
+}
118
+
119
+/** 重置按钮操作 */
120
+function resetQuery() {
121
+  proxy.resetForm("queryRef")
122
+  handleQuery()
123
+}
124
+
125
+const emit = defineEmits(["ok"])
126
+/** 选择授权用户操作 */
127
+function handleSelectUser() {
128
+  const roleId = queryParams.roleId
129
+  const uIds = userIds.value.join(",")
130
+  if (uIds == "") {
131
+    proxy.$modal.msgError("请选择要分配的用户")
132
+    return
133
+  }
134
+  authUserSelectAll({ roleId: roleId, userIds: uIds }).then(res => {
135
+    proxy.$modal.msgSuccess(res.msg)
136
+    visible.value = false
137
+    emit("ok")
138
+  })
139
+}
140
+
141
+defineExpose({
142
+  show,
143
+})
144
+</script>

+ 276 - 0
src/views/system/sql/index.vue

@@ -0,0 +1,276 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
4
+      <el-form-item label="sql key" prop="sqlKey">
5
+        <el-input
6
+          v-model="queryParams.sqlKey"
7
+          placeholder="请输入sql key"
8
+          clearable
9
+          @keyup.enter="handleQuery"
10
+        />
11
+      </el-form-item>
12
+      <el-form-item>
13
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
14
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
15
+      </el-form-item>
16
+    </el-form>
17
+
18
+    <el-row :gutter="10" class="mb8">
19
+      <el-col :span="1.5">
20
+        <el-button
21
+          type="primary"
22
+          plain
23
+          icon="Plus"
24
+          @click="handleAdd"
25
+          v-hasPermi="['system:sql:add']"
26
+        >新增</el-button>
27
+      </el-col>
28
+      <el-col :span="1.5">
29
+        <el-button
30
+          type="success"
31
+          plain
32
+          icon="Edit"
33
+          :disabled="single"
34
+          @click="handleUpdate"
35
+          v-hasPermi="['system:sql:edit']"
36
+        >修改</el-button>
37
+      </el-col>
38
+      <el-col :span="1.5">
39
+        <el-button
40
+          type="danger"
41
+          plain
42
+          icon="Delete"
43
+          :disabled="multiple"
44
+          @click="handleDelete"
45
+          v-hasPermi="['system:sql:remove']"
46
+        >删除</el-button>
47
+      </el-col>
48
+      <el-col :span="1.5">
49
+        <el-button
50
+          type="warning"
51
+          plain
52
+          icon="Download"
53
+          @click="handleExport"
54
+          v-hasPermi="['system:sql:export']"
55
+        >导出</el-button>
56
+      </el-col>
57
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
58
+    </el-row>
59
+
60
+    <el-table v-loading="loading" :data="sqlList" @selection-change="handleSelectionChange">
61
+      <el-table-column type="selection" width="55" align="center" />
62
+      <!-- <el-table-column label="id" align="center" prop="id" /> -->
63
+      <el-table-column label="sql key" align="center" prop="sqlKey" />
64
+      <!-- <el-table-column label="内容" align="center" prop="sqlContent" /> -->
65
+      <el-table-column label="说明" align="center" prop="description" />
66
+      <el-table-column label="备注" align="center" prop="remark" />
67
+      <!-- <el-table-column label="参数配置" align="center" prop="paramsConfig" /> -->
68
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
69
+        <template #default="scope">
70
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:sql:edit']">修改</el-button>
71
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:sql:remove']">删除</el-button>
72
+          <el-button link type="primary" icon="Search" @click="executeSql(scope.row)" >执行</el-button>
73
+
74
+          
75
+        </template>
76
+      </el-table-column>
77
+    </el-table>
78
+    
79
+    <pagination
80
+      v-show="total>0"
81
+      :total="total"
82
+      v-model:page="queryParams.pageNum"
83
+      v-model:limit="queryParams.pageSize"
84
+      @pagination="getList"
85
+    />
86
+
87
+    <!-- 添加或修改动态sql执行config对话框 -->
88
+    <el-dialog :title="title" v-model="open" width="60%" append-to-body>
89
+      <el-form ref="sqlRef" :model="form" :rules="rules" label-width="80px">
90
+        <el-form-item label="sql key" prop="sqlKey">
91
+          <el-input v-model="form.sqlKey" placeholder="请输入sql key" />
92
+        </el-form-item>
93
+        <el-form-item label="内容" prop="sqlContent">
94
+           <SqlEdit ref="sqlEdit" v-model="form.sqlContent" />
95
+        </el-form-item>
96
+        <el-form-item label="说明" prop="description">
97
+          <el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
98
+        </el-form-item>
99
+        <el-form-item label="备注" prop="remark">
100
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
101
+        </el-form-item>
102
+        <el-form-item label="参数配置" prop="paramsConfig">
103
+          <el-input v-model="form.paramsConfig" type="textarea" placeholder="请输入内容" />
104
+        </el-form-item>
105
+      </el-form>
106
+      <template #footer>
107
+        <div class="dialog-footer">
108
+          <el-button type="primary" @click="submitForm">确 定</el-button>
109
+          <el-button @click="cancel">取 消</el-button>
110
+        </div>
111
+      </template>
112
+    </el-dialog>
113
+  </div>
114
+</template>
115
+
116
+<script setup name="Sql">
117
+import { listSql, getSql, delSql, addSql, updateSql, executeSqlByKeyAndParam } from "@/api/system/sql"
118
+
119
+const { proxy } = getCurrentInstance()
120
+
121
+const sqlList = ref([])
122
+const open = ref(false)
123
+const loading = ref(true)
124
+const showSearch = ref(true)
125
+const ids = ref([])
126
+const single = ref(true)
127
+const multiple = ref(true)
128
+const total = ref(0)
129
+const title = ref("")
130
+
131
+const data = reactive({
132
+  form: {},
133
+  queryParams: {
134
+    pageNum: 1,
135
+    pageSize: 10,
136
+    sqlKey: null,
137
+    sqlContent: null,
138
+    description: null,
139
+    paramsConfig: null
140
+  },
141
+  rules: {
142
+    sqlKey: [
143
+      { required: true, message: "sql key不能为空", trigger: "blur" }
144
+    ],
145
+    sqlContent: [
146
+      { required: true, message: "内容不能为空", trigger: "blur" }
147
+    ],
148
+    createTime: [
149
+      { required: true, message: "创建时间不能为空", trigger: "blur" }
150
+    ],
151
+    updateTime: [
152
+      { required: true, message: "更新时间不能为空", trigger: "blur" }
153
+    ],
154
+  }
155
+})
156
+
157
+const { queryParams, form, rules } = toRefs(data)
158
+
159
+/** 查询动态sql执行config列表 */
160
+function getList() {
161
+  loading.value = true
162
+  listSql(queryParams.value).then(response => {
163
+    sqlList.value = response.rows
164
+    total.value = response.total
165
+    loading.value = false
166
+  })
167
+}
168
+
169
+// 取消按钮
170
+function cancel() {
171
+  open.value = false
172
+  reset()
173
+}
174
+
175
+// 表单重置
176
+function reset() {
177
+  form.value = {
178
+    id: null,
179
+    sqlKey: null,
180
+    sqlContent: null,
181
+    description: null,
182
+    createBy: null,
183
+    createTime: null,
184
+    updateBy: null,
185
+    updateTime: null,
186
+    remark: null,
187
+    paramsConfig: null
188
+  }
189
+  proxy.resetForm("sqlRef")
190
+}
191
+
192
+/** 搜索按钮操作 */
193
+function handleQuery() {
194
+  queryParams.value.pageNum = 1
195
+  getList()
196
+}
197
+
198
+/** 重置按钮操作 */
199
+function resetQuery() {
200
+  proxy.resetForm("queryRef")
201
+  handleQuery()
202
+}
203
+
204
+// 多选框选中数据
205
+function handleSelectionChange(selection) {
206
+  ids.value = selection.map(item => item.id)
207
+  single.value = selection.length != 1
208
+  multiple.value = !selection.length
209
+}
210
+
211
+/** 新增按钮操作 */
212
+function handleAdd() {
213
+  reset()
214
+  open.value = true
215
+  title.value = "添加动态sql执行config"
216
+}
217
+
218
+/** 修改按钮操作 */
219
+function handleUpdate(row) {
220
+  reset()
221
+  const _id = row.id || ids.value
222
+  getSql(_id).then(response => {
223
+    form.value = response.data
224
+    open.value = true
225
+    title.value = "修改动态sql执行config"
226
+  })
227
+}
228
+
229
+/** 提交按钮 */
230
+function submitForm() {
231
+  proxy.$refs["sqlRef"].validate(valid => {
232
+    if (valid) {
233
+      if (form.value.id != null) {
234
+        updateSql(form.value).then(response => {
235
+          proxy.$modal.msgSuccess("修改成功")
236
+          open.value = false
237
+          getList()
238
+        })
239
+      } else {
240
+        addSql(form.value).then(response => {
241
+          proxy.$modal.msgSuccess("新增成功")
242
+          open.value = false
243
+          getList()
244
+        })
245
+      }
246
+    }
247
+  })
248
+}
249
+
250
+/** 删除按钮操作 */
251
+function handleDelete(row) {
252
+  const _ids = row.id || ids.value
253
+  proxy.$modal.confirm('是否确认删除动态sql执行config编号为"' + _ids + '"的数据项?').then(function() {
254
+    return delSql(_ids)
255
+  }).then(() => {
256
+    getList()
257
+    proxy.$modal.msgSuccess("删除成功")
258
+  }).catch(() => {})
259
+}
260
+
261
+/** 导出按钮操作 */
262
+function handleExport() {
263
+  proxy.download('system/sql/export', {
264
+    ...queryParams.value
265
+  }, `sql_${new Date().getTime()}.xlsx`)
266
+}
267
+
268
+/** 执行 */
269
+function executeSql(row) {
270
+  executeSqlByKeyAndParam(row.sqlKey,row.paramsConfig || {}).then(response => {
271
+    proxy.$modal.msgSuccess("执行成功:"+JSON.stringify(response.data) )
272
+  })
273
+}
274
+
275
+getList()
276
+</script>

+ 123 - 0
src/views/system/user/authRole.vue

@@ -0,0 +1,123 @@
1
+<template>
2
+   <div class="app-container">
3
+      <h4 class="form-header h4">基本信息</h4>
4
+      <el-form :model="form" label-width="80px">
5
+         <el-row>
6
+            <el-col :span="8" :offset="2">
7
+               <el-form-item label="用户昵称" prop="nickName">
8
+                  <el-input v-model="form.nickName" disabled />
9
+               </el-form-item>
10
+            </el-col>
11
+            <el-col :span="8" :offset="2">
12
+               <el-form-item label="登录账号" prop="userName">
13
+                  <el-input v-model="form.userName" disabled />
14
+               </el-form-item>
15
+            </el-col>
16
+         </el-row>
17
+      </el-form>
18
+
19
+      <h4 class="form-header h4">角色信息</h4>
20
+      <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
21
+         <el-table-column label="序号" width="55" type="index" align="center">
22
+            <template #default="scope">
23
+               <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
24
+            </template>
25
+         </el-table-column>
26
+         <el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
27
+         <el-table-column label="角色编号" align="center" prop="roleId" />
28
+         <el-table-column label="角色名称" align="center" prop="roleName" />
29
+         <el-table-column label="权限字符" align="center" prop="roleKey" />
30
+         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
31
+            <template #default="scope">
32
+               <span>{{ parseTime(scope.row.createTime) }}</span>
33
+            </template>
34
+         </el-table-column>
35
+      </el-table>
36
+
37
+      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
38
+
39
+      <el-form label-width="100px">
40
+         <div style="text-align: center;margin-left:-120px;margin-top:30px;">
41
+            <el-button type="primary" @click="submitForm()">提交</el-button>
42
+            <el-button @click="close()">返回</el-button>
43
+         </div>
44
+      </el-form>
45
+   </div>
46
+</template>
47
+
48
+<script setup name="AuthRole">
49
+import { getAuthRole, updateAuthRole } from "@/api/system/user"
50
+
51
+const route = useRoute()
52
+const { proxy } = getCurrentInstance()
53
+
54
+const loading = ref(true)
55
+const total = ref(0)
56
+const pageNum = ref(1)
57
+const pageSize = ref(10)
58
+const roleIds = ref([])
59
+const roles = ref([])
60
+const form = ref({
61
+  nickName: undefined,
62
+  userName: undefined,
63
+  userId: undefined
64
+})
65
+
66
+/** 单击选中行数据 */
67
+function clickRow(row) {
68
+  if (checkSelectable(row)) {
69
+    proxy.$refs["roleRef"].toggleRowSelection(row)
70
+  }
71
+}
72
+
73
+/** 多选框选中数据 */
74
+function handleSelectionChange(selection) {
75
+  roleIds.value = selection.map(item => item.roleId)
76
+}
77
+
78
+/** 保存选中的数据编号 */
79
+function getRowKey(row) {
80
+  return row.roleId
81
+}
82
+
83
+// 检查角色状态
84
+function checkSelectable(row) {
85
+  return row.status === "0" ? true : false
86
+}
87
+
88
+/** 关闭按钮 */
89
+function close() {
90
+  const obj = { path: "/system/user" }
91
+  proxy.$tab.closeOpenPage(obj)
92
+}
93
+
94
+/** 提交按钮 */
95
+function submitForm() {
96
+  const userId = form.value.userId
97
+  const rIds = roleIds.value.join(",")
98
+  updateAuthRole({ userId: userId, roleIds: rIds }).then(response => {
99
+    proxy.$modal.msgSuccess("授权成功")
100
+    close()
101
+  })
102
+}
103
+
104
+(() => {
105
+  const userId = route.params && route.params.userId
106
+  if (userId) {
107
+    loading.value = true
108
+    getAuthRole(userId).then(response => {
109
+      form.value = response.user
110
+      roles.value = response.roles
111
+      total.value = roles.value.length
112
+      nextTick(() => {
113
+        roles.value.forEach(row => {
114
+          if (row.flag) {
115
+            proxy.$refs["roleRef"].toggleRowSelection(row)
116
+          }
117
+        })
118
+      })
119
+      loading.value = false
120
+    })
121
+  }
122
+})()
123
+</script>

+ 252 - 0
src/views/system/user/components/UserInfoEdit.vue

@@ -0,0 +1,252 @@
1
+<template>
2
+  <div class="formWrap">
3
+    <el-form :model="form" :rules="rules" ref="userRef" label-width="8em">
4
+      <el-card header="基本身份信息" style="margin-bottom: 10px;">
5
+        <el-row :gutter="16">
6
+          <el-col :span="12">
7
+            <el-form-item label="用户昵称" prop="nickName">
8
+              <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
9
+            </el-form-item>
10
+          </el-col>
11
+          <el-col :span="12">
12
+            <el-form-item label="归属部门" prop="deptId">
13
+              <el-tree-select v-model="form.deptId" :data="enabledDeptOptions"
14
+                :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属部门"
15
+                check-strictly />
16
+            </el-form-item>
17
+          </el-col>
18
+          <el-col :span="12">
19
+            <el-form-item label="手机号码" prop="phonenumber">
20
+              <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
21
+            </el-form-item>
22
+          </el-col>
23
+          <el-col :span="12">
24
+            <el-form-item label="邮箱" prop="email">
25
+              <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
26
+            </el-form-item>
27
+          </el-col>
28
+          <el-col :span="12">
29
+            <el-form-item v-if=" form.userId == undefined " label="用户名称" prop="userName">
30
+              <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
31
+            </el-form-item>
32
+          </el-col>
33
+          <el-col :span="12">
34
+            <el-form-item v-if=" form.userId == undefined " label="用户密码" prop="password">
35
+              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
36
+            </el-form-item>
37
+          </el-col>
38
+          <el-col :span="12">
39
+            <el-form-item label="身份证号" prop="cardNumber">
40
+              <el-input v-model="form.cardNumber" placeholder="请输入身份证号码" maxlength="18" minlength="15" />
41
+            </el-form-item>
42
+          </el-col>
43
+          <el-col :span="12">
44
+            <el-form-item label="用户性别" prop="sex">
45
+              <el-select v-model="form.sex" placeholder="请选择用户性别">
46
+                <el-option v-for=" dict in sys_user_sex " :key="dict.value" :label="dict.label"
47
+                  :value="dict.value"></el-option>
48
+              </el-select>
49
+            </el-form-item>
50
+          </el-col>
51
+          <el-col :span="12">
52
+            <el-form-item label="政治面貌" prop="politicalStatus">
53
+              <el-select v-model="form.politicalStatus" placeholder="请选择政治面貌">
54
+                <el-option v-for=" item in sys_user_political_status " :key="item.value" :label="item.label"
55
+                  :value="item.value" />
56
+              </el-select>
57
+            </el-form-item>
58
+          </el-col>
59
+          <el-col :span="12">
60
+            <el-form-item label="角色" prop="roleIds">
61
+              <el-select v-model="form.roleIds" multiple placeholder="请选择角色" @change="roleChange">
62
+                <el-option v-for=" item in roleOptions " :key="item.roleId" :label="item.roleName"
63
+                  :value="item.roleId"
64
+                  :disabled="item.status == 1"></el-option>
65
+              </el-select>
66
+            </el-form-item>
67
+          </el-col>
68
+        </el-row>
69
+      </el-card>
70
+      <el-card header="状态管理信息" style="margin-bottom: 10px;">
71
+        <el-row :gutter="16">
72
+          <el-col :span="12">
73
+            <el-form-item label="账号状态" prop="status">
74
+              <el-radio-group v-model="form.status">
75
+                <el-radio v-for=" dict in sys_normal_disable " :key="dict.value" :value="dict.value">
76
+                  {{ dict.label }}
77
+                </el-radio>
78
+              </el-radio-group>
79
+            </el-form-item>
80
+          </el-col>
81
+          <el-col :span="12">
82
+            <el-form-item label="培训合规状态" prop="trainingComplianceStatus">
83
+              <el-radio-group v-model="form.trainingComplianceStatus">
84
+                <el-radio v-for=" dict in sys_user_training_compliance_status " :key="dict.value" :value="dict.value">
85
+                  {{ dict.label }}
86
+                </el-radio>
87
+              </el-radio-group>
88
+            </el-form-item>
89
+          </el-col>
90
+        </el-row>
91
+      </el-card>
92
+      <el-card header="资质与岗位信息" style="margin-bottom: 10px;">
93
+        <el-row :gutter="16">
94
+          <el-col :span="12">
95
+            <el-form-item label="资质等级" prop="qualificationLevel"
96
+              :rules="( form.roleIds || [] ).includes( 101 ) || ( form.roleIds || [] ).includes( 102 ) ? [ { required: true, message: '请选择资质等级', trigger: 'blur' } ] : []">
97
+              <el-select v-model="form.qualificationLevel" placeholder="请选择资质等级">
98
+                <el-option v-for=" item in sys_user_qualification_level " :key="item.value" :label="item.label"
99
+                  :value="item.value" />
100
+              </el-select>
101
+            </el-form-item>
102
+          </el-col>
103
+          <el-col :span="12">
104
+            <el-form-item label="岗位类型" prop="postIds"
105
+              :rules="( form.roleIds || [] ).includes( 101 ) || ( form.roleIds || [] ).includes( 102 ) ? [ { required: true, message: '请选择岗位类型', trigger: 'blur' } ] : []">
106
+              <el-cascader v-model="form.postIds" :options="treeData" :props="treeNodeProps" filterable
107
+                style="width: 100%" />
108
+            </el-form-item>
109
+          </el-col>
110
+        </el-row>
111
+      </el-card>
112
+      <el-card header="人员异常状态记录" style="margin-bottom: 10px;">
113
+        <el-row :gutter="16">
114
+          <el-col :span="24">
115
+            <el-form-item label="行政处罚情况" prop="administrativeStatus">
116
+              <el-input v-model="form.administrativeStatus" type="textarea" placeholder="请输入行政处罚情况"></el-input>
117
+            </el-form-item>
118
+          </el-col>
119
+          <el-col :span="24">
120
+            <el-form-item label="身体健康情况" prop="physicalHealthStatus">
121
+              <el-input v-model="form.physicalHealthStatus" type="textarea" placeholder="请输入内容"></el-input>
122
+            </el-form-item>
123
+          </el-col>
124
+        </el-row>
125
+      </el-card>
126
+      <el-card header="应急联络信息" style="margin-bottom: 10px;">
127
+        <el-row :gutter="16">
128
+          <el-col :span="12">
129
+            <el-form-item label="紧急联系人姓名" prop="emergencyContactName">
130
+              <el-input v-model="form.emergencyContactName" type="text" placeholder="请输入紧急联系人姓名"></el-input>
131
+            </el-form-item>
132
+          </el-col>
133
+          <el-col :span="12">
134
+            <el-form-item label="紧急联系人电话" prop="emergencyContactPhone">
135
+              <el-input v-model="form.emergencyContactPhone" type="text" placeholder="请输入紧急联系人电话"
136
+                maxlength="11"></el-input>
137
+            </el-form-item>
138
+          </el-col>
139
+          <el-col :span="12">
140
+            <el-form-item label="紧急联系人关系" prop="emergencyContactRelationship">
141
+              <el-select v-model="form.emergencyContactRelationship" placeholder="请选择联系人关系">
142
+                <el-option v-for=" item in sys_user_emergency_contact_relationship " :key="item.value"
143
+                  :label="item.label"
144
+                  :value="item.value" />
145
+              </el-select>
146
+            </el-form-item>
147
+          </el-col>
148
+        </el-row>
149
+      </el-card>
150
+      <el-card header="其他信息" style="margin-bottom: 10px;">
151
+        <el-row :gutter="16">
152
+          <el-col :span="24">
153
+            <el-form-item label="备注">
154
+              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
155
+            </el-form-item>
156
+          </el-col>
157
+        </el-row>
158
+      </el-card>
159
+    </el-form>
160
+  </div>
161
+</template>
162
+
163
+<script setup>
164
+import { onMounted, reactive, computed } from 'vue';
165
+import { useDict } from '@/utils/dict'
166
+import { listAllTree } from '@/api/system/post'
167
+// 重父页面迁移过来的代码 2处公用选择框数据 选择使用父组件传入
168
+defineProps({
169
+  sys_user_sex: {
170
+    type: Array,
171
+    default: []
172
+  },
173
+  enabledDeptOptions: {
174
+    type: Array,
175
+    default: []
176
+  },
177
+  sys_normal_disable: {
178
+    type: Array,
179
+    default: []
180
+  },
181
+  postOptions: {
182
+    type: Array,
183
+    default: []
184
+  },
185
+  roleOptions: {
186
+    type: Array,
187
+    default: []
188
+  },
189
+})
190
+const userRef = ref(null)
191
+const form = defineModel('form', ref({}))
192
+// 政治面貌、资质等级、紧急联系人关系
193
+const {
194
+  sys_user_political_status,
195
+  sys_user_qualification_level,
196
+  sys_user_emergency_contact_relationship,
197
+  sys_user_training_compliance_status
198
+} = useDict('sys_user_political_status', 'sys_user_qualification_level', 'sys_user_emergency_contact_relationship', 'sys_user_training_compliance_status')
199
+
200
+const rules = computed(() => ({
201
+  userName: [ { required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" } ],
202
+  deptId: [ { required: true, message: '请选择归属部门', trigger: "blur" } ],
203
+  nickName: [ { required: true, message: "用户昵称不能为空", trigger: "blur" } ],
204
+  password: [ { required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" } ],
205
+  email: [ { type: "email", message: "请输入正确的邮箱地址", trigger: [ "blur", "change" ] } ],
206
+  phonenumber: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ],
207
+  emergencyContactPhone: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ],
208
+  roleIds: [ { required: true, message: '请选择角色', trigger: "blur" } ],
209
+  status: [ { required: true, message: '请选择账号状态', trigger: "blur" } ]
210
+}))
211
+
212
+const treeNodeProps = { multiple: true, expandTrigger: 'hover', label: 'postName', value: 'postId', emitPath: false, checkOnClickNode: true }
213
+const treeData = ref([])
214
+onMounted(() => {
215
+  listAllTree().then(res => {
216
+    treeData.value = res.data || []
217
+  })
218
+})
219
+
220
+const roleChange = () => {
221
+  userRef.value && userRef.value.clearValidate('qualificationLevel')
222
+  userRef.value && userRef.value.clearValidate('postIds')
223
+}
224
+
225
+defineExpose({
226
+  submit: () => {
227
+    return userRef.value.validate()
228
+  },
229
+  resetForm: () => {
230
+    return userRef.value && userRef.value.resetFields()
231
+  }
232
+})
233
+
234
+</script>
235
+
236
+<style lang="less" scoped>
237
+.formWrap {
238
+  max-height: 75vh;
239
+  overflow-y: auto;
240
+  overflow-x: hidden;
241
+  padding: 0 15px;
242
+  box-sizing: border-box;
243
+
244
+  :deep(.el-card) {
245
+    .el-card__header {
246
+      font-weight: 600;
247
+      font-size: 16px;
248
+      color: #000;
249
+    }
250
+  }
251
+}
252
+</style>

+ 487 - 0
src/views/system/user/index.vue

@@ -0,0 +1,487 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-row :gutter="20">
4
+      <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
5
+        <!--部门数据-->
6
+        <pane size="16">
7
+          <el-col>
8
+            <div class="head-container">
9
+              <el-input v-model="deptName" placeholder="请输入部门名称" clearable prefix-icon="Search"
10
+                style="margin-bottom: 20px" />
11
+            </div>
12
+            <div class="head-container">
13
+              <el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }"
14
+                :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id"
15
+                highlight-current default-expand-all @node-click="handleNodeClick" />
16
+            </div>
17
+          </el-col>
18
+        </pane>
19
+        <!--用户数据-->
20
+        <pane size="84">
21
+          <el-col>
22
+            <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
23
+              <el-form-item label="用户名称" prop="userName">
24
+                <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px"
25
+                  @keyup.enter="handleQuery" />
26
+              </el-form-item>
27
+              <el-form-item label="手机号码" prop="phonenumber">
28
+                <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px"
29
+                  @keyup.enter="handleQuery" />
30
+              </el-form-item>
31
+              <el-form-item label="状态" prop="status">
32
+                <el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
33
+                  <el-option v-for=" dict in sys_normal_disable " :key="dict.value" :label="dict.label"
34
+                    :value="dict.value" />
35
+                </el-select>
36
+              </el-form-item>
37
+              <el-form-item label="创建时间" style="width: 308px">
38
+                <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-"
39
+                  start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
40
+              </el-form-item>
41
+              <el-form-item>
42
+                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
43
+                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
44
+              </el-form-item>
45
+            </el-form>
46
+
47
+            <el-row :gutter="10" class="mb8">
48
+              <el-col :span="1.5">
49
+                <el-button type="primary" plain icon="Plus" @click="handleAdd"
50
+                  v-hasPermi="[ 'system:user:add' ]">新增</el-button>
51
+              </el-col>
52
+              <el-col :span="1.5">
53
+                <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
54
+                  v-hasPermi="[ 'system:user:edit' ]">修改</el-button>
55
+              </el-col>
56
+              <el-col :span="1.5">
57
+                <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
58
+                  v-hasPermi="[ 'system:user:remove' ]">删除</el-button>
59
+              </el-col>
60
+              <el-col :span="1.5">
61
+                <el-button type="info" plain icon="Upload" @click="handleImport"
62
+                  v-hasPermi="[ 'system:user:import' ]">导入</el-button>
63
+              </el-col>
64
+              <el-col :span="1.5">
65
+                <el-button type="warning" plain icon="Download" @click="handleExport"
66
+                  v-hasPermi="[ 'system:user:export' ]">导出</el-button>
67
+              </el-col>
68
+              <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
69
+            </el-row>
70
+
71
+            <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
72
+              <el-table-column type="selection" width="50" align="center" />
73
+              <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if=" columns[ 0 ].visible " />
74
+              <el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if=" columns[ 1 ].visible "
75
+                :show-overflow-tooltip="true" />
76
+              <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if=" columns[ 2 ].visible "
77
+                :show-overflow-tooltip="true" />
78
+              <el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if=" columns[ 3 ].visible "
79
+                :show-overflow-tooltip="true" />
80
+              <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber"
81
+                v-if=" columns[ 4 ].visible " width="120" />
82
+              <el-table-column label="状态" align="center" key="status" v-if=" columns[ 5 ].visible ">
83
+                <template #default=" scope ">
84
+                  <el-switch
85
+                    v-model="scope.row.status"
86
+                    active-value="0"
87
+                    inactive-value="1"
88
+                    @change="handleStatusChange( scope.row )"></el-switch>
89
+                </template>
90
+              </el-table-column>
91
+              <el-table-column label="创建时间" align="center" prop="createTime" v-if=" columns[ 6 ].visible " width="160">
92
+                <template #default=" scope ">
93
+                  <span>{{ parseTime( scope.row.createTime ) }}</span>
94
+                </template>
95
+              </el-table-column>
96
+              <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
97
+                <template #default=" scope ">
98
+                  <el-tooltip content="修改" placement="top" v-if=" scope.row.userId !== 1 ">
99
+                    <el-button link type="primary" icon="Edit" @click="handleUpdate( scope.row )"
100
+                      v-hasPermi="[ 'system:user:edit' ]"></el-button>
101
+                  </el-tooltip>
102
+                  <el-tooltip content="删除" placement="top" v-if=" scope.row.userId !== 1 ">
103
+                    <el-button link type="primary" icon="Delete" @click="handleDelete( scope.row )"
104
+                      v-hasPermi="[ 'system:user:remove' ]"></el-button>
105
+                  </el-tooltip>
106
+                  <el-tooltip content="重置密码" placement="top" v-if=" scope.row.userId !== 1 ">
107
+                    <el-button link type="primary" icon="Key" @click="handleResetPwd( scope.row )"
108
+                      v-hasPermi="[ 'system:user:resetPwd' ]"></el-button>
109
+                  </el-tooltip>
110
+                  <el-tooltip content="分配角色" placement="top" v-if=" scope.row.userId !== 1 ">
111
+                    <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole( scope.row )"
112
+                      v-hasPermi="[ 'system:user:edit' ]"></el-button>
113
+                  </el-tooltip>
114
+                </template>
115
+              </el-table-column>
116
+            </el-table>
117
+            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
118
+              v-model:limit="queryParams.pageSize"
119
+              @pagination="getList" />
120
+          </el-col>
121
+        </pane>
122
+      </splitpanes>
123
+    </el-row>
124
+
125
+    <!-- 添加或修改用户配置对话框 -->
126
+    <el-dialog :title="title" v-model="open" width="75vw" hei append-to-body>
127
+      <UserInfoEdit ref="userRef" v-model:form="form" :sys_user_sex="sys_user_sex"
128
+        :enabledDeptOptions="enabledDeptOptions"
129
+        :sys_normal_disable="sys_normal_disable" :postOptions="postOptions" :roleOptions="roleOptions" />
130
+      <template #footer>
131
+        <div class="dialog-footer">
132
+          <el-button type="primary" @click="submitForm">确 定</el-button>
133
+          <el-button @click="cancel">取 消</el-button>
134
+        </div>
135
+      </template>
136
+    </el-dialog>
137
+
138
+    <!-- 用户导入对话框 -->
139
+    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
140
+      <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers"
141
+        :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading"
142
+        :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
143
+        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
144
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
145
+        <template #tip>
146
+          <div class="el-upload__tip text-center">
147
+            <div class="el-upload__tip">
148
+              <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
149
+            </div>
150
+            <span>仅允许导入xls、xlsx格式文件。</span>
151
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline"
152
+              @click="importTemplate">下载模板</el-link>
153
+          </div>
154
+        </template>
155
+      </el-upload>
156
+      <template #footer>
157
+        <div class="dialog-footer">
158
+          <el-button type="primary" @click="submitFileForm">确 定</el-button>
159
+          <el-button @click="upload.open = false">取 消</el-button>
160
+        </div>
161
+      </template>
162
+    </el-dialog>
163
+  </div>
164
+</template>
165
+
166
+<script setup name="User">
167
+import { getToken } from "@/utils/auth"
168
+import useAppStore from '@/store/modules/app'
169
+import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
170
+import { Splitpanes, Pane } from "splitpanes"
171
+import "splitpanes/dist/splitpanes.css"
172
+import UserInfoEdit from './components/UserInfoEdit.vue'
173
+
174
+const router = useRouter()
175
+const appStore = useAppStore()
176
+const { proxy } = getCurrentInstance()
177
+const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex")
178
+const userRef = ref(null)
179
+const userList = ref([])
180
+const open = ref(false)
181
+const loading = ref(true)
182
+const showSearch = ref(true)
183
+const ids = ref([])
184
+const single = ref(true)
185
+const multiple = ref(true)
186
+const total = ref(0)
187
+const title = ref("")
188
+const dateRange = ref([])
189
+const deptName = ref("")
190
+const deptOptions = ref(undefined)
191
+const enabledDeptOptions = ref(undefined)
192
+const initPassword = ref(undefined)
193
+const postOptions = ref([])
194
+const roleOptions = ref([])
195
+/*** 用户导入参数 */
196
+const upload = reactive({
197
+  // 是否显示弹出层(用户导入)
198
+  open: false,
199
+  // 弹出层标题(用户导入)
200
+  title: "",
201
+  // 是否禁用上传
202
+  isUploading: false,
203
+  // 是否更新已经存在的用户数据
204
+  updateSupport: 0,
205
+  // 设置上传的请求头部
206
+  headers: { Authorization: "Bearer " + getToken() },
207
+  // 上传的地址
208
+  url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
209
+})
210
+// 列显隐信息
211
+const columns = ref([
212
+  { key: 0, label: `用户编号`, visible: true },
213
+  { key: 1, label: `用户名称`, visible: true },
214
+  { key: 2, label: `用户昵称`, visible: true },
215
+  { key: 3, label: `部门`, visible: true },
216
+  { key: 4, label: `手机号码`, visible: true },
217
+  { key: 5, label: `状态`, visible: true },
218
+  { key: 6, label: `创建时间`, visible: true }
219
+])
220
+
221
+const data = reactive({
222
+  form: {},
223
+  queryParams: {
224
+    pageNum: 1,
225
+    pageSize: 10,
226
+    userName: undefined,
227
+    phonenumber: undefined,
228
+    status: undefined,
229
+    deptId: undefined
230
+  }
231
+})
232
+
233
+const { queryParams, form } = toRefs(data)
234
+
235
+/** 通过条件过滤节点  */
236
+const filterNode = (value, data) => {
237
+  if (!value) return true
238
+  return data.label.indexOf(value) !== -1
239
+}
240
+
241
+/** 根据名称筛选部门树 */
242
+watch(deptName, val => {
243
+  proxy.$refs[ "deptTreeRef" ].filter(val)
244
+})
245
+
246
+/** 查询用户列表 */
247
+function getList () {
248
+  loading.value = true
249
+  listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
250
+    loading.value = false
251
+    userList.value = res.rows
252
+    total.value = res.total
253
+  })
254
+}
255
+
256
+/** 查询部门下拉树结构 */
257
+function getDeptTree () {
258
+  deptTreeSelect().then(response => {
259
+    deptOptions.value = response.data
260
+    enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
261
+  })
262
+}
263
+
264
+/** 过滤禁用的部门 */
265
+function filterDisabledDept (deptList) {
266
+  return deptList.filter(dept => {
267
+    if (dept.disabled) {
268
+      return false
269
+    }
270
+    if (dept.children && dept.children.length) {
271
+      dept.children = filterDisabledDept(dept.children)
272
+    }
273
+    return true
274
+  })
275
+}
276
+
277
+/** 节点单击事件 */
278
+function handleNodeClick (data) {
279
+  queryParams.value.deptId = data.id
280
+  handleQuery()
281
+}
282
+
283
+/** 搜索按钮操作 */
284
+function handleQuery () {
285
+  queryParams.value.pageNum = 1
286
+  getList()
287
+}
288
+
289
+/** 重置按钮操作 */
290
+function resetQuery () {
291
+  dateRange.value = []
292
+  proxy.resetForm("queryRef")
293
+  queryParams.value.deptId = undefined
294
+  proxy.$refs.deptTreeRef.setCurrentKey(null)
295
+  handleQuery()
296
+}
297
+
298
+/** 删除按钮操作 */
299
+function handleDelete (row) {
300
+  const userIds = row.userId || ids.value
301
+  proxy.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?').then(function () {
302
+    return delUser(userIds)
303
+  }).then(() => {
304
+    getList()
305
+    proxy.$modal.msgSuccess("删除成功")
306
+  }).catch(() => { })
307
+}
308
+
309
+/** 导出按钮操作 */
310
+function handleExport () {
311
+  proxy.download("system/user/export", {
312
+    ...queryParams.value,
313
+  }, `user_${new Date().getTime()}.xlsx`)
314
+}
315
+
316
+/** 用户状态修改  */
317
+function handleStatusChange (row) {
318
+  let text = row.status === "0" ? "启用" : "停用"
319
+  proxy.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function () {
320
+    return changeUserStatus(row.userId, row.status)
321
+  }).then(() => {
322
+    proxy.$modal.msgSuccess(text + "成功")
323
+  }).catch(function () {
324
+    row.status = row.status === "0" ? "1" : "0"
325
+  })
326
+}
327
+
328
+/** 更多操作 */
329
+function handleCommand (command, row) {
330
+  switch (command) {
331
+    case "handleResetPwd":
332
+      handleResetPwd(row)
333
+      break
334
+    case "handleAuthRole":
335
+      handleAuthRole(row)
336
+      break
337
+    default:
338
+      break
339
+  }
340
+}
341
+
342
+/** 跳转角色分配 */
343
+function handleAuthRole (row) {
344
+  const userId = row.userId
345
+  router.push("/system/user-auth/role/" + userId)
346
+}
347
+
348
+/** 重置密码按钮操作 */
349
+function handleResetPwd (row) {
350
+  proxy.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
351
+    confirmButtonText: "确定",
352
+    cancelButtonText: "取消",
353
+    closeOnClickModal: false,
354
+    inputPattern: /^.{5,20}$/,
355
+    inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
356
+    inputValidator: (value) => {
357
+      if (/<|>|"|'|\||\\/.test(value)) {
358
+        return "不能包含非法字符:< > \" ' \\\ |"
359
+      }
360
+    },
361
+  }).then(({ value }) => {
362
+    resetUserPwd(row.userId, value).then(response => {
363
+      proxy.$modal.msgSuccess("修改成功,新密码是:" + value)
364
+    })
365
+  }).catch(() => { })
366
+}
367
+
368
+/** 选择条数  */
369
+function handleSelectionChange (selection) {
370
+  ids.value = selection.map(item => item.userId)
371
+  single.value = selection.length != 1
372
+  multiple.value = !selection.length
373
+}
374
+
375
+/** 导入按钮操作 */
376
+function handleImport () {
377
+  upload.title = "用户导入"
378
+  upload.open = true
379
+}
380
+
381
+/** 下载模板操作 */
382
+function importTemplate () {
383
+  proxy.download("system/user/importTemplate", {
384
+  }, `user_template_${new Date().getTime()}.xlsx`)
385
+}
386
+
387
+/**文件上传中处理 */
388
+const handleFileUploadProgress = (event, file, fileList) => {
389
+  upload.isUploading = true
390
+}
391
+
392
+/** 文件上传成功处理 */
393
+const handleFileSuccess = (response, file, fileList) => {
394
+  upload.open = false
395
+  upload.isUploading = false
396
+  proxy.$refs[ "uploadRef" ].handleRemove(file)
397
+  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
398
+  getList()
399
+}
400
+
401
+/** 提交上传文件 */
402
+function submitFileForm () {
403
+  proxy.$refs[ "uploadRef" ].submit()
404
+}
405
+
406
+/** 重置操作表单 */
407
+function reset () {
408
+  form.value = {
409
+    userId: undefined,
410
+    deptId: undefined,
411
+    userName: undefined,
412
+    nickName: undefined,
413
+    password: undefined,
414
+    phonenumber: undefined,
415
+    email: undefined,
416
+    sex: undefined,
417
+    status: "0",
418
+    remark: undefined,
419
+    postIds: [],
420
+    roleIds: []
421
+  }
422
+  proxy.$refs[ "userRef" ] && proxy.$refs[ "userRef" ].resetForm()
423
+}
424
+
425
+/** 取消按钮 */
426
+function cancel () {
427
+  reset()
428
+  open.value = false
429
+  
430
+}
431
+
432
+/** 新增按钮操作 */
433
+function handleAdd () {
434
+  open.value = true
435
+  getUser().then(response => {
436
+    reset()
437
+    postOptions.value = response.posts
438
+    roleOptions.value = response.roles
439
+    title.value = "添加用户"
440
+    form.value.password = initPassword.value
441
+  })
442
+}
443
+
444
+/** 修改按钮操作 */
445
+function handleUpdate (row) {
446
+  
447
+  const userId = row.userId || ids.value
448
+  open.value = true
449
+  getUser(userId).then(response => {
450
+    reset()
451
+    form.value = response.data
452
+    postOptions.value = response.posts
453
+    roleOptions.value = response.roles
454
+    form.value.postIds = response.postIds || []
455
+    form.value.roleIds = response.roleIds || []
456
+    title.value = "修改用户"
457
+    form.password = ""
458
+  })
459
+}
460
+
461
+/** 提交按钮 */
462
+function submitForm () {
463
+  proxy.$refs[ "userRef" ].submit().then(() => {
464
+    if (form.value.userId != undefined) {
465
+      updateUser(form.value).then(response => {
466
+        proxy.$modal.msgSuccess("修改成功")
467
+        open.value = false
468
+        getList()
469
+      })
470
+    } else {
471
+      addUser(form.value).then(response => {
472
+        proxy.$modal.msgSuccess("新增成功")
473
+        open.value = false
474
+        getList()
475
+      })
476
+    }
477
+  }).catch(() => {})
478
+}
479
+
480
+onMounted(() => {
481
+  getDeptTree()
482
+  getList()
483
+  proxy.getConfigKey("sys.user.initPassword").then(response => {
484
+    initPassword.value = response.msg
485
+  })
486
+})
487
+</script>

+ 94 - 0
src/views/system/user/profile/index.vue

@@ -0,0 +1,94 @@
1
+<template>
2
+   <div class="app-container">
3
+      <el-row :gutter="20">
4
+         <el-col :span="6" :xs="24">
5
+            <el-card class="box-card">
6
+               <template v-slot:header>
7
+                 <div class="clearfix">
8
+                   <span>个人信息</span>
9
+                 </div>
10
+               </template>
11
+               <div>
12
+                  <div class="text-center">
13
+                     <userAvatar />
14
+                  </div>
15
+                  <ul class="list-group list-group-striped">
16
+                     <li class="list-group-item">
17
+                        <svg-icon icon-class="user" />用户名称
18
+                        <div class="pull-right">{{ state.user.userName }}</div>
19
+                     </li>
20
+                     <li class="list-group-item">
21
+                        <svg-icon icon-class="phone" />手机号码
22
+                        <div class="pull-right">{{ state.user.phonenumber }}</div>
23
+                     </li>
24
+                     <li class="list-group-item">
25
+                        <svg-icon icon-class="email" />用户邮箱
26
+                        <div class="pull-right">{{ state.user.email }}</div>
27
+                     </li>
28
+                     <li class="list-group-item">
29
+                        <svg-icon icon-class="tree" />所属部门
30
+                        <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
31
+                     </li>
32
+                     <li class="list-group-item">
33
+                        <svg-icon icon-class="peoples" />所属角色
34
+                        <div class="pull-right">{{ state.roleGroup }}</div>
35
+                     </li>
36
+                     <li class="list-group-item">
37
+                        <svg-icon icon-class="date" />创建日期
38
+                        <div class="pull-right">{{ state.user.createTime }}</div>
39
+                     </li>
40
+                  </ul>
41
+               </div>
42
+            </el-card>
43
+         </el-col>
44
+         <el-col :span="18" :xs="24">
45
+            <el-card>
46
+               <template v-slot:header>
47
+                 <div class="clearfix">
48
+                   <span>基本资料</span>
49
+                 </div>
50
+               </template>
51
+               <el-tabs v-model="selectedTab">
52
+                  <el-tab-pane label="基本资料" name="userinfo">
53
+                     <userInfo :user="state.user" />
54
+                  </el-tab-pane>
55
+                  <el-tab-pane label="修改密码" name="resetPwd">
56
+                     <resetPwd />
57
+                  </el-tab-pane>
58
+               </el-tabs>
59
+            </el-card>
60
+         </el-col>
61
+      </el-row>
62
+   </div>
63
+</template>
64
+
65
+<script setup name="Profile">
66
+import userAvatar from "./userAvatar"
67
+import userInfo from "./userInfo"
68
+import resetPwd from "./resetPwd"
69
+import { getUserProfile } from "@/api/system/user"
70
+
71
+const route = useRoute()
72
+const selectedTab = ref("userinfo")
73
+const state = reactive({
74
+  user: {},
75
+  roleGroup: {},
76
+  postGroup: {}
77
+})
78
+
79
+function getUser() {
80
+  getUserProfile().then(response => {
81
+    state.user = response.data
82
+    state.roleGroup = response.roleGroup
83
+    state.postGroup = response.postGroup
84
+  })
85
+}
86
+
87
+onMounted(() => {
88
+  const activeTab = route.params && route.params.activeTab
89
+  if (activeTab) {
90
+    selectedTab.value = activeTab
91
+  }
92
+  getUser()
93
+})
94
+</script>

+ 59 - 0
src/views/system/user/profile/resetPwd.vue

@@ -0,0 +1,59 @@
1
+<template>
2
+   <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
3
+      <el-form-item label="旧密码" prop="oldPassword">
4
+         <el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
5
+      </el-form-item>
6
+      <el-form-item label="新密码" prop="newPassword">
7
+         <el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
8
+      </el-form-item>
9
+      <el-form-item label="确认密码" prop="confirmPassword">
10
+         <el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
11
+      </el-form-item>
12
+      <el-form-item>
13
+      <el-button type="primary" @click="submit">保存</el-button>
14
+      <el-button type="danger" @click="close">关闭</el-button>
15
+      </el-form-item>
16
+   </el-form>
17
+</template>
18
+
19
+<script setup>
20
+import { updateUserPwd } from "@/api/system/user"
21
+
22
+const { proxy } = getCurrentInstance()
23
+
24
+const user = reactive({
25
+  oldPassword: undefined,
26
+  newPassword: undefined,
27
+  confirmPassword: undefined
28
+})
29
+
30
+const equalToPassword = (rule, value, callback) => {
31
+  if (user.newPassword !== value) {
32
+    callback(new Error("两次输入的密码不一致"))
33
+  } else {
34
+    callback()
35
+  }
36
+}
37
+
38
+const rules = ref({
39
+  oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
40
+  newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
41
+  confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
42
+})
43
+
44
+/** 提交按钮 */
45
+function submit() {
46
+  proxy.$refs.pwdRef.validate(valid => {
47
+    if (valid) {
48
+      updateUserPwd(user.oldPassword, user.newPassword).then(response => {
49
+        proxy.$modal.msgSuccess("修改成功")
50
+      })
51
+    }
52
+  })
53
+}
54
+
55
+/** 关闭按钮 */
56
+function close() {
57
+  proxy.$tab.closePage()
58
+}
59
+</script>

+ 180 - 0
src/views/system/user/profile/userAvatar.vue

@@ -0,0 +1,180 @@
1
+<template>
2
+  <div class="user-info-head" @click="editCropper()">
3
+    <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
4
+    <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
5
+      <el-row>
6
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
7
+          <vue-cropper
8
+            ref="cropper"
9
+            :img="options.img"
10
+            :info="true"
11
+            :autoCrop="options.autoCrop"
12
+            :autoCropWidth="options.autoCropWidth"
13
+            :autoCropHeight="options.autoCropHeight"
14
+            :fixedBox="options.fixedBox"
15
+            :outputType="options.outputType"
16
+            @realTime="realTime"
17
+            v-if="visible"
18
+          />
19
+        </el-col>
20
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
21
+          <div class="avatar-upload-preview">
22
+            <img :src="options.previews.url" :style="options.previews.img" />
23
+          </div>
24
+        </el-col>
25
+      </el-row>
26
+      <br />
27
+      <el-row>
28
+        <el-col :lg="2" :md="2">
29
+          <el-upload
30
+            action="#"
31
+            :http-request="requestUpload"
32
+            :show-file-list="false"
33
+            :before-upload="beforeUpload"
34
+          >
35
+            <el-button>
36
+              选择
37
+              <el-icon class="el-icon--right"><Upload /></el-icon>
38
+            </el-button>
39
+          </el-upload>
40
+        </el-col>
41
+        <el-col :lg="{ span: 1, offset: 2 }" :md="2">
42
+          <el-button icon="Plus" @click="changeScale(1)"></el-button>
43
+        </el-col>
44
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
45
+          <el-button icon="Minus" @click="changeScale(-1)"></el-button>
46
+        </el-col>
47
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
48
+          <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
49
+        </el-col>
50
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
51
+          <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
52
+        </el-col>
53
+        <el-col :lg="{ span: 2, offset: 6 }" :md="2">
54
+          <el-button type="primary" @click="uploadImg()">提 交</el-button>
55
+        </el-col>
56
+      </el-row>
57
+    </el-dialog>
58
+  </div>
59
+</template>
60
+
61
+<script setup>
62
+import "vue-cropper/dist/index.css"
63
+import { VueCropper } from "vue-cropper"
64
+import { uploadAvatar } from "@/api/system/user"
65
+import useUserStore from "@/store/modules/user"
66
+
67
+const userStore = useUserStore()
68
+const { proxy } = getCurrentInstance()
69
+
70
+const open = ref(false)
71
+const visible = ref(false)
72
+const title = ref("修改头像")
73
+
74
+//图片裁剪数据
75
+const options = reactive({
76
+  img: userStore.avatar,     // 裁剪图片的地址
77
+  autoCrop: true,            // 是否默认生成截图框
78
+  autoCropWidth: 200,        // 默认生成截图框宽度
79
+  autoCropHeight: 200,       // 默认生成截图框高度
80
+  fixedBox: true,            // 固定截图框大小 不允许改变
81
+  outputType: "png",         // 默认生成截图为PNG格式
82
+  filename: 'avatar',        // 文件名称
83
+  previews: {}               //预览数据
84
+})
85
+
86
+/** 编辑头像 */
87
+function editCropper() {
88
+  open.value = true
89
+}
90
+
91
+/** 打开弹出层结束时的回调 */
92
+function modalOpened() {
93
+  visible.value = true
94
+}
95
+
96
+/** 覆盖默认上传行为 */
97
+function requestUpload() {}
98
+
99
+/** 向左旋转 */
100
+function rotateLeft() {
101
+  proxy.$refs.cropper.rotateLeft()
102
+}
103
+
104
+/** 向右旋转 */
105
+function rotateRight() {
106
+  proxy.$refs.cropper.rotateRight()
107
+}
108
+
109
+/** 图片缩放 */
110
+function changeScale(num) {
111
+  num = num || 1
112
+  proxy.$refs.cropper.changeScale(num)
113
+}
114
+
115
+/** 上传预处理 */
116
+function beforeUpload(file) {
117
+  if (file.type.indexOf("image/") == -1) {
118
+    proxy.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。")
119
+  } else {
120
+    const reader = new FileReader()
121
+    reader.readAsDataURL(file)
122
+    reader.onload = () => {
123
+      options.img = reader.result
124
+      options.filename = file.name
125
+    }
126
+  }
127
+}
128
+
129
+/** 上传图片 */
130
+function uploadImg() {
131
+  proxy.$refs.cropper.getCropBlob(data => {
132
+    let formData = new FormData()
133
+    formData.append("avatarfile", data, options.filename)
134
+    uploadAvatar(formData).then(response => {
135
+      open.value = false
136
+      options.img = import.meta.env.VITE_APP_BASE_API + response.imgUrl
137
+      userStore.avatar = options.img
138
+      proxy.$modal.msgSuccess("修改成功")
139
+      visible.value = false
140
+    })
141
+  })
142
+}
143
+
144
+/** 实时预览 */
145
+function realTime(data) {
146
+  options.previews = data
147
+}
148
+
149
+/** 关闭窗口 */
150
+function closeDialog() {
151
+  options.img = userStore.avatar
152
+  options.visible = false
153
+}
154
+</script>
155
+
156
+<style lang='scss' scoped>
157
+.user-info-head {
158
+  position: relative;
159
+  display: inline-block;
160
+  height: 120px;
161
+}
162
+
163
+.user-info-head:hover:after {
164
+  content: "+";
165
+  position: absolute;
166
+  left: 0;
167
+  right: 0;
168
+  top: 0;
169
+  bottom: 0;
170
+  color: #eee;
171
+  background: rgba(0, 0, 0, 0.5);
172
+  font-size: 24px;
173
+  font-style: normal;
174
+  -webkit-font-smoothing: antialiased;
175
+  -moz-osx-font-smoothing: grayscale;
176
+  cursor: pointer;
177
+  line-height: 110px;
178
+  border-radius: 50%;
179
+}
180
+</style>

+ 67 - 0
src/views/system/user/profile/userInfo.vue

@@ -0,0 +1,67 @@
1
+<template>
2
+   <el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
3
+      <el-form-item label="用户昵称" prop="nickName">
4
+         <el-input v-model="form.nickName" maxlength="30" />
5
+      </el-form-item>
6
+      <el-form-item label="手机号码" prop="phonenumber">
7
+         <el-input v-model="form.phonenumber" maxlength="11" />
8
+      </el-form-item>
9
+      <el-form-item label="邮箱" prop="email">
10
+         <el-input v-model="form.email" maxlength="50" />
11
+      </el-form-item>
12
+      <el-form-item label="性别">
13
+         <el-radio-group v-model="form.sex">
14
+            <el-radio value="0">男</el-radio>
15
+            <el-radio value="1">女</el-radio>
16
+         </el-radio-group>
17
+      </el-form-item>
18
+      <el-form-item>
19
+      <el-button type="primary" @click="submit">保存</el-button>
20
+      <el-button type="danger" @click="close">关闭</el-button>
21
+      </el-form-item>
22
+   </el-form>
23
+</template>
24
+
25
+<script setup>
26
+import { updateUserProfile } from "@/api/system/user"
27
+
28
+const props = defineProps({
29
+  user: {
30
+    type: Object
31
+  }
32
+})
33
+
34
+const { proxy } = getCurrentInstance()
35
+
36
+const form = ref({})
37
+const rules = ref({
38
+  nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
39
+  email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
40
+  phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
41
+})
42
+
43
+/** 提交按钮 */
44
+function submit() {
45
+  proxy.$refs.userRef.validate(valid => {
46
+    if (valid) {
47
+      updateUserProfile(form.value).then(response => {
48
+        proxy.$modal.msgSuccess("修改成功")
49
+        props.user.phonenumber = form.value.phonenumber
50
+        props.user.email = form.value.email
51
+      })
52
+    }
53
+  })
54
+}
55
+
56
+/** 关闭按钮 */
57
+function close() {
58
+  proxy.$tab.closePage()
59
+}
60
+
61
+// 回显当前登录用户信息
62
+watch(() => props.user, user => {
63
+  if (user) {
64
+    form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
65
+  }
66
+},{ immediate: true })
67
+</script>

+ 71 - 0
src/views/tool/build/CodeTypeDialog.vue

@@ -0,0 +1,71 @@
1
+<template>
2
+  <el-dialog v-model="open" width="500px" title="选择生成类型" @open="onOpen" @close="onClose">
3
+    <el-form ref="codeTypeForm" :model="formData" :rules="rules" label-width="100px">
4
+      <el-form-item label="生成类型" prop="type">
5
+        <el-radio-group v-model="formData.type">
6
+          <el-radio-button v-for="(item, index) in typeOptions" :key="index" :label="item.value">
7
+            {{ item.label }}
8
+          </el-radio-button>
9
+        </el-radio-group>
10
+      </el-form-item>
11
+      <el-form-item v-if="showFileName" label="文件名" prop="fileName">
12
+        <el-input v-model="formData.fileName" placeholder="请输入文件名" clearable />
13
+      </el-form-item>
14
+    </el-form>
15
+
16
+    <template #footer>
17
+      <el-button @click="onClose">取消</el-button>
18
+      <el-button type="primary" @click="handelConfirm">确定</el-button>
19
+    </template>
20
+  </el-dialog>
21
+</template>
22
+
23
+<script setup>
24
+const open = defineModel()
25
+const props = defineProps({
26
+  showFileName: Boolean
27
+})
28
+const emit = defineEmits(['confirm'])
29
+const formData = ref({
30
+  fileName: undefined,
31
+  type: 'file'
32
+})
33
+const codeTypeForm = ref()
34
+const rules = {
35
+  fileName: [{
36
+    required: true,
37
+    message: '请输入文件名',
38
+    trigger: 'blur'
39
+  }],
40
+  type: [{
41
+    required: true,
42
+    message: '生成类型不能为空',
43
+    trigger: 'change'
44
+  }]
45
+}
46
+const typeOptions = ref([
47
+  {
48
+    label: '页面',
49
+    value: 'file'
50
+  },
51
+  {
52
+    label: '弹窗',
53
+    value: 'dialog'
54
+  }
55
+])
56
+function onOpen() {
57
+  if (props.showFileName) {
58
+    formData.value.fileName = `${+new Date()}.vue`
59
+  }
60
+}
61
+function onClose() {
62
+  open.value = false
63
+}
64
+function handelConfirm() {
65
+  codeTypeForm.value.validate(valid => {
66
+    if (!valid) return
67
+    emit('confirm', { ...formData.value })
68
+    onClose()
69
+  })
70
+}
71
+</script>

+ 68 - 0
src/views/tool/build/DraggableItem.vue

@@ -0,0 +1,68 @@
1
+<template>
2
+  <el-col :span="element.span" :class="className" @click.stop="activeItem(element)">
3
+    <el-form-item :label="element.label" :label-width="element.labelWidth ? element.labelWidth + 'px' : null"
4
+      :required="element.required" v-if="element.layout === 'colFormItem'">
5
+      <render :key="element.tag" :conf="element" v-model="element.defaultValue" />
6
+    </el-form-item>
7
+    <el-row :gutter="element.gutter" :class="element.class" @click.stop="activeItem(element)" v-else>
8
+      <span class="component-name"> {{ element.componentName }} </span>
9
+      <draggable group="componentsGroup" :animation="340" :list="element.children" class="drag-wrapper" item-key="label"
10
+        ref="draggableItemRef" :component-data="getComponentData()">
11
+        <template #item="scoped">
12
+          <draggable-item :key="scoped.element.renderKey" :drawing-list="element.children" :element="scoped.element"
13
+            :index="index" :active-id="activeId" :form-conf="formConf" @activeItem="activeItem(scoped.element)"
14
+            @copyItem="copyItem(scoped.element, element.children)"
15
+            @deleteItem="deleteItem(scoped.index, element.children)" />
16
+        </template>
17
+      </draggable>
18
+    </el-row>
19
+    <span class="drawing-item-copy" title="复制" @click.stop="copyItem(element)">
20
+      <el-icon><CopyDocument /></el-icon>
21
+    </span>
22
+    <span class="drawing-item-delete" title="删除" @click.stop="deleteItem(index)">
23
+      <el-icon><Delete /></el-icon>
24
+    </span>
25
+  </el-col>
26
+</template>
27
+<script setup name="DraggableItem">
28
+import draggable from "vuedraggable/dist/vuedraggable.common"
29
+import render from '@/utils/generator/render'
30
+
31
+const props = defineProps({
32
+  element: Object,
33
+  index: Number,
34
+  drawingList: Array,
35
+  activeId: {
36
+    type: [String, Number]
37
+  },
38
+  formConf: Object
39
+})
40
+const className = ref('')
41
+const draggableItemRef = ref(null)
42
+const emits = defineEmits(['activeItem', 'copyItem', 'deleteItem'])
43
+
44
+function activeItem(item) {
45
+  emits('activeItem', item)
46
+}
47
+function copyItem(item, parent) {
48
+  emits('copyItem', item, parent ?? props.drawingList)
49
+}
50
+function deleteItem(item, parent) {
51
+  emits('deleteItem', item, parent ?? props.drawingList)
52
+}
53
+
54
+function getComponentData() {
55
+  return {
56
+    gutter: props.element.gutter,
57
+    justify: props.element.justify,
58
+    align: props.element.align
59
+  }
60
+}
61
+
62
+watch(() => props.activeId, (val) => {
63
+  className.value = (props.element.layout === 'rowFormItem' ? 'drawing-row-item' : 'drawing-item') + (val === props.element.formId ? ' active-from-item' : '')
64
+  if (props.formConf.unFocusedComponentBorder) {
65
+    className.value += ' unfocus-bordered'
66
+  }
67
+}, { immediate: true })
68
+</script>

+ 115 - 0
src/views/tool/build/IconsDialog.vue

@@ -0,0 +1,115 @@
1
+<template>
2
+  <div class="icon-dialog">
3
+    <el-dialog v-model="value" width="980px" :close-on-click-modal="false" :modal-append-to-body="false" @open="onOpen"
4
+      @close="onClose">
5
+      <template #header="{ close, titleId, titleClass }">
6
+        选择图标
7
+        <el-input v-model="key" size="small" :style="{ width: '260px' }" placeholder="请输入图标名称" prefix-icon="Search"
8
+          clearable />
9
+      </template>
10
+      <ul class="icon-ul">
11
+        <li v-for="icon in iconList" :key="icon" :class="active === icon ? 'active-item' : ''" @click="onSelect(icon)">
12
+          <div>
13
+            <el-icon :size="30">
14
+              <component :is="icon" />
15
+            </el-icon>
16
+            <div>{{ icon }}</div>
17
+          </div>
18
+        </li>
19
+      </ul>
20
+    </el-dialog>
21
+  </div>
22
+</template>
23
+<script setup>
24
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
25
+import { watch } from 'vue'
26
+
27
+const iconList = ref([])
28
+const originList = []
29
+const key = ref('')
30
+const active = ref('')
31
+const emit = defineEmits(['select'])
32
+const value = defineModel()
33
+for (const [key] of Object.entries(ElementPlusIconsVue)) {
34
+  iconList.value.push(key)
35
+  originList.push(key)
36
+}
37
+
38
+function onOpen() { }
39
+function onClose() { }
40
+function onSelect(icon) {
41
+  active.value = icon
42
+  emit('select', icon)
43
+  value.value = false
44
+}
45
+
46
+watch(key, (val) => {
47
+  if (val) {
48
+    iconList.value = originList.filter(name => name.indexOf(val) > -1)
49
+  } else {
50
+    iconList.value = originList
51
+  }
52
+})
53
+</script>
54
+<style lang="scss" scoped>
55
+.icon-ul {
56
+  margin: 0;
57
+  padding: 0;
58
+  font-size: 0;
59
+
60
+  li {
61
+    list-style-type: none;
62
+    text-align: center;
63
+    font-size: 14px;
64
+    display: inline-flex;
65
+    width: 16.66%;
66
+    box-sizing: border-box;
67
+    height: 108px;
68
+    padding: 6px 6px 6px 6px;
69
+    cursor: pointer;
70
+    overflow: hidden;
71
+    align-items: center;
72
+    justify-content: center;
73
+
74
+    &:hover {
75
+      background: #f2f2f2;
76
+    }
77
+
78
+    &.active-item {
79
+      background: #e1f3fb;
80
+      color: #7a6df0
81
+    }
82
+
83
+    i {
84
+      font-size: 30px;
85
+      line-height: 50px;
86
+      margin-bottom: 10px;
87
+    }
88
+  }
89
+}
90
+
91
+.icon-dialog {
92
+  :deep() {
93
+    .el-dialog {
94
+      border-radius: 8px;
95
+      margin-bottom: 0;
96
+      margin-top: 4vh !important;
97
+      display: flex;
98
+      flex-direction: column;
99
+      max-height: 92vh;
100
+      overflow: hidden;
101
+      box-sizing: border-box;
102
+
103
+      .el-dialog__header {
104
+        padding-top: 14px;
105
+      }
106
+
107
+      .el-dialog__body {
108
+        margin: 0 20px 20px 20px;
109
+        padding: 0;
110
+        overflow: auto;
111
+      }
112
+    }
113
+  }
114
+}
115
+</style>

+ 906 - 0
src/views/tool/build/RightPanel.vue

@@ -0,0 +1,906 @@
1
+<template>
2
+  <div class="right-board">
3
+    <el-tabs v-model="currentTab" stretch class="center-tabs">
4
+      <el-tab-pane label="组件属性" name="field" />
5
+      <el-tab-pane label="表单属性" name="form" />
6
+    </el-tabs>
7
+    <div class="field-box">
8
+      <a class="document-link" target="_blank" :href="documentLink" title="查看组件文档">
9
+        <el-icon>
10
+          <Link />
11
+        </el-icon>
12
+      </a>
13
+      <el-scrollbar class="right-scrollbar">
14
+        <!-- 组件属性 -->
15
+        <el-form v-show="currentTab === 'field' && showField" size="default" label-width="90px" label-position="top"
16
+          style="">
17
+          <el-form-item v-if="activeData.changeTag" label="组件类型">
18
+            <el-select v-model="activeData.tagIcon" placeholder="请选择组件类型" :style="{ width: '100%' }" @change="tagChange">
19
+              <el-option-group v-for="group in tagList" :key="group.label" :label="group.label">
20
+                <el-option v-for="item in group.options" :key="item.label" :label="item.label" :value="item.tagIcon">
21
+                  <svg-icon class="node-icon" :icon-class="item.tagIcon" style="margin-right: 10px;" />
22
+                  <span> {{ item.label }}</span>
23
+                </el-option>
24
+              </el-option-group>
25
+            </el-select>
26
+          </el-form-item>
27
+          <el-form-item v-if="activeData.vModel !== undefined" label="字段名">
28
+            <el-input v-model="activeData.vModel" placeholder="请输入字段名(v-model)" />
29
+          </el-form-item>
30
+          <el-form-item v-if="activeData.componentName !== undefined" label="组件名">
31
+            {{ activeData.componentName }}
32
+          </el-form-item>
33
+          <el-form-item v-if="activeData.label !== undefined" label="标题">
34
+            <el-input v-model="activeData.label" placeholder="请输入标题" />
35
+          </el-form-item>
36
+          <el-form-item v-if="activeData.placeholder !== undefined" label="占位提示">
37
+            <el-input v-model="activeData.placeholder" placeholder="请输入占位提示" />
38
+          </el-form-item>
39
+          <el-form-item v-if="activeData['start-placeholder'] !== undefined" label="开始占位">
40
+            <el-input v-model="activeData['start-placeholder']" placeholder="请输入占位提示" />
41
+          </el-form-item>
42
+          <el-form-item v-if="activeData['end-placeholder'] !== undefined" label="结束占位">
43
+            <el-input v-model="activeData['end-placeholder']" placeholder="请输入占位提示" />
44
+          </el-form-item>
45
+          <el-form-item v-if="activeData.span !== undefined" label="表单栅格">
46
+            <el-slider v-model="activeData.span" :max="24" :min="1" :marks="{ 12: '' }" @change="spanChange" />
47
+          </el-form-item>
48
+          <el-form-item v-if="activeData.layout === 'rowFormItem'" label="栅格间隔">
49
+            <el-input-number v-model="activeData.gutter" :min="0" placeholder="栅格间隔" />
50
+          </el-form-item>
51
+
52
+          <el-form-item v-if="activeData.justify !== undefined" label="水平排列">
53
+            <el-select v-model="activeData.justify" placeholder="请选择水平排列" :style="{ width: '100%' }">
54
+              <el-option v-for="(item, index) in justifyOptions" :key="index" :label="item.label" :value="item.value" />
55
+            </el-select>
56
+          </el-form-item>
57
+          <el-form-item v-if="activeData.align !== undefined" label="垂直排列">
58
+            <el-radio-group v-model="activeData.align">
59
+              <el-radio-button label="top" />
60
+              <el-radio-button label="middle" />
61
+              <el-radio-button label="bottom" />
62
+            </el-radio-group>
63
+          </el-form-item>
64
+          <el-form-item v-if="activeData.labelWidth !== undefined" label="标签宽度">
65
+            <el-input v-model.number="activeData.labelWidth" type="number" placeholder="请输入标签宽度" />
66
+          </el-form-item>
67
+          <el-form-item v-if="activeData.style && activeData.style.width !== undefined" label="组件宽度">
68
+            <el-input v-model="activeData.style.width" placeholder="请输入组件宽度" clearable />
69
+          </el-form-item>
70
+          <el-form-item v-if="activeData.vModel !== undefined" label="默认值">
71
+            <el-input :value="setDefaultValue(activeData.defaultValue)" placeholder="请输入默认值"
72
+              @input="onDefaultValueInput" />
73
+          </el-form-item>
74
+          <el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="至少应选">
75
+            <el-input-number :value="activeData.min" :min="0" placeholder="至少应选"
76
+              @input="$set(activeData, 'min', $event ? $event : undefined)" />
77
+          </el-form-item>
78
+          <el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="最多可选">
79
+            <el-input-number :value="activeData.max" :min="0" placeholder="最多可选"
80
+              @input="$set(activeData, 'max', $event ? $event : undefined)" />
81
+          </el-form-item>
82
+          <el-form-item v-if="activeData.prepend !== undefined" label="前缀">
83
+            <el-input v-model="activeData.prepend" placeholder="请输入前缀" />
84
+          </el-form-item>
85
+          <el-form-item v-if="activeData.append !== undefined" label="后缀">
86
+            <el-input v-model="activeData.append" placeholder="请输入后缀" />
87
+          </el-form-item>
88
+          <el-form-item v-if="activeData['prefix-icon'] !== undefined" label="前图标">
89
+            <el-input v-model="activeData['prefix-icon']" placeholder="请输入前图标名称">
90
+              <template #append>
91
+                <el-button icon="Pointer" @click="openIconsDialog('prefix-icon')">
92
+                  选择
93
+                </el-button>
94
+              </template>
95
+            </el-input>
96
+          </el-form-item>
97
+          <el-form-item v-if="activeData['suffix-icon'] !== undefined" label="后图标">
98
+            <el-input v-model="activeData['suffix-icon']" placeholder="请输入后图标名称">
99
+              <template #append>
100
+                <el-button icon="Pointer" @click="openIconsDialog('suffix-icon')">
101
+                  选择
102
+                </el-button>
103
+              </template>
104
+            </el-input>
105
+          </el-form-item>
106
+          <el-form-item v-if="activeData.tag === 'el-cascader'" label="选项分隔符">
107
+            <el-input v-model="activeData.separator" placeholder="请输入选项分隔符" />
108
+          </el-form-item>
109
+          <el-form-item v-if="activeData.autosize !== undefined" label="最小行数">
110
+            <el-input-number v-model="activeData.autosize.minRows" :min="1" placeholder="最小行数" />
111
+          </el-form-item>
112
+          <el-form-item v-if="activeData.autosize !== undefined" label="最大行数">
113
+            <el-input-number v-model="activeData.autosize.maxRows" :min="1" placeholder="最大行数" />
114
+          </el-form-item>
115
+          <el-form-item v-if="activeData.min !== undefined" label="最小值">
116
+            <el-input-number v-model="activeData.min" placeholder="最小值" />
117
+          </el-form-item>
118
+          <el-form-item v-if="activeData.max !== undefined" label="最大值">
119
+            <el-input-number v-model="activeData.max" placeholder="最大值" />
120
+          </el-form-item>
121
+          <el-form-item v-if="activeData.step !== undefined" label="步长">
122
+            <el-input-number v-model="activeData.step" placeholder="步数" />
123
+          </el-form-item>
124
+          <el-form-item v-if="activeData.tag === 'el-input-number'" label="精度">
125
+            <el-input-number v-model="activeData.precision" :min="0" placeholder="精度" />
126
+          </el-form-item>
127
+          <el-form-item v-if="activeData.tag === 'el-input-number'" label="按钮位置">
128
+            <el-radio-group v-model="activeData['controls-position']">
129
+              <el-radio-button label="">
130
+                默认
131
+              </el-radio-button>
132
+              <el-radio-button label="right">
133
+                右侧
134
+              </el-radio-button>
135
+            </el-radio-group>
136
+          </el-form-item>
137
+          <el-form-item v-if="activeData.maxlength !== undefined" label="最多输入">
138
+            <el-input v-model="activeData.maxlength" placeholder="请输入字符长度">
139
+              <template slot="append">
140
+                个字符
141
+              </template>
142
+            </el-input>
143
+          </el-form-item>
144
+          <el-form-item v-if="activeData['active-text'] !== undefined" label="开启提示">
145
+            <el-input v-model="activeData['active-text']" placeholder="请输入开启提示" />
146
+          </el-form-item>
147
+          <el-form-item v-if="activeData['inactive-text'] !== undefined" label="关闭提示">
148
+            <el-input v-model="activeData['inactive-text']" placeholder="请输入关闭提示" />
149
+          </el-form-item>
150
+          <el-form-item v-if="activeData['active-value'] !== undefined" label="开启值">
151
+            <el-input :value="setDefaultValue(activeData['active-value'])" placeholder="请输入开启值"
152
+              @input="onSwitchValueInput($event, 'active-value')" />
153
+          </el-form-item>
154
+          <el-form-item v-if="activeData['inactive-value'] !== undefined" label="关闭值">
155
+            <el-input :value="setDefaultValue(activeData['inactive-value'])" placeholder="请输入关闭值"
156
+              @input="onSwitchValueInput($event, 'inactive-value')" />
157
+          </el-form-item>
158
+          <el-form-item v-if="activeData.type !== undefined && 'el-date-picker' === activeData.tag" label="时间类型">
159
+            <el-select v-model="activeData.type" placeholder="请选择时间类型" :style="{ width: '100%' }"
160
+              @change="dateTypeChange">
161
+              <el-option v-for="(item, index) in dateOptions" :key="index" :label="item.label" :value="item.value" />
162
+            </el-select>
163
+          </el-form-item>
164
+          <el-form-item v-if="activeData.name !== undefined" label="文件字段名">
165
+            <el-input v-model="activeData.name" placeholder="请输入上传文件字段名" />
166
+          </el-form-item>
167
+          <el-form-item v-if="activeData.accept !== undefined" label="文件类型">
168
+            <el-select v-model="activeData.accept" placeholder="请选择文件类型" :style="{ width: '100%' }" clearable>
169
+              <el-option label="图片" value="image/*" />
170
+              <el-option label="视频" value="video/*" />
171
+              <el-option label="音频" value="audio/*" />
172
+              <el-option label="excel" value=".xls,.xlsx" />
173
+              <el-option label="word" value=".doc,.docx" />
174
+              <el-option label="pdf" value=".pdf" />
175
+              <el-option label="txt" value=".txt" />
176
+            </el-select>
177
+          </el-form-item>
178
+          <el-form-item v-if="activeData.fileSize !== undefined" label="文件大小">
179
+            <el-input v-model.number="activeData.fileSize" placeholder="请输入文件大小">
180
+              <el-select slot="append" v-model="activeData.sizeUnit" :style="{ width: '66px' }">
181
+                <el-option label="KB" value="KB" />
182
+                <el-option label="MB" value="MB" />
183
+                <el-option label="GB" value="GB" />
184
+              </el-select>
185
+            </el-input>
186
+          </el-form-item>
187
+          <el-form-item v-if="activeData.action !== undefined" label="上传地址">
188
+            <el-input v-model="activeData.action" placeholder="请输入上传地址" clearable />
189
+          </el-form-item>
190
+          <el-form-item v-if="activeData['list-type'] !== undefined" label="列表类型">
191
+            <el-radio-group v-model="activeData['list-type']" size="small">
192
+              <el-radio-button label="text">
193
+                text
194
+              </el-radio-button>
195
+              <el-radio-button label="picture">
196
+                picture
197
+              </el-radio-button>
198
+              <el-radio-button label="picture-card">
199
+                picture-card
200
+              </el-radio-button>
201
+            </el-radio-group>
202
+          </el-form-item>
203
+          <el-form-item v-if="activeData.buttonText !== undefined" v-show="'picture-card' !== activeData['list-type']"
204
+            label="按钮文字">
205
+            <el-input v-model="activeData.buttonText" placeholder="请输入按钮文字" />
206
+          </el-form-item>
207
+          <el-form-item v-if="activeData['range-separator'] !== undefined" label="分隔符">
208
+            <el-input v-model="activeData['range-separator']" placeholder="请输入分隔符" />
209
+          </el-form-item>
210
+          <el-form-item v-if="activeData['picker-options'] !== undefined" label="时间段">
211
+            <el-input v-model="activeData['picker-options'].selectableRange" placeholder="请输入时间段" />
212
+          </el-form-item>
213
+          <el-form-item v-if="activeData.format !== undefined" label="时间格式">
214
+            <el-input :value="activeData.format" placeholder="请输入时间格式" @input="setTimeValue($event)" />
215
+          </el-form-item>
216
+          <template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.tag) > -1">
217
+            <el-divider>选项</el-divider>
218
+            <draggable :list="activeData.options" :animation="340" group="selectItem" handle=".option-drag"
219
+              item-key="label">
220
+              <template #item="{ element, index }">
221
+                <div :key="index" class="select-item">
222
+                  <div class="select-line-icon option-drag">
223
+                    <i class="el-icon-s-operation" />
224
+                  </div>
225
+                  <el-input v-model="element.label" placeholder="选项名" size="small" />
226
+                  <el-input placeholder="选项值" size="small" :value="element.value"
227
+                    @input="setOptionValue(element, $event)" />
228
+                  <div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
229
+                    <el-icon>
230
+                      <Remove />
231
+                    </el-icon>
232
+                  </div>
233
+                </div>
234
+              </template>
235
+            </draggable>
236
+            <div>
237
+              <el-button icon="CirclePlus" style="margin-left: 8px; margin-top: 10px;" text bg type="primary"
238
+                @click="addSelectItem">
239
+                添加选项
240
+              </el-button>
241
+            </div>
242
+            <el-divider />
243
+          </template>
244
+
245
+          <template v-if="['el-cascader'].indexOf(activeData.tag) > -1">
246
+            <el-divider>选项</el-divider>
247
+            <el-form-item label="数据类型">
248
+              <el-radio-group v-model="activeData.dataType" size="small">
249
+                <el-radio-button label="dynamic">
250
+                  动态数据
251
+                </el-radio-button>
252
+                <el-radio-button label="static">
253
+                  静态数据
254
+                </el-radio-button>
255
+              </el-radio-group>
256
+            </el-form-item>
257
+
258
+            <template v-if="activeData.dataType === 'dynamic'">
259
+              <el-form-item label="标签键名">
260
+                <el-input v-model="activeData.labelKey" placeholder="请输入标签键名" />
261
+              </el-form-item>
262
+              <el-form-item label="值键名">
263
+                <el-input v-model="activeData.valueKey" placeholder="请输入值键名" />
264
+              </el-form-item>
265
+              <el-form-item label="子级键名">
266
+                <el-input v-model="activeData.childrenKey" placeholder="请输入子级键名" />
267
+              </el-form-item>
268
+            </template>
269
+
270
+            <el-tree v-if="activeData.dataType === 'static'" draggable :data="activeData.options" node-key="id"
271
+              :expand-on-click-node="false" :render-content="renderContent" />
272
+            <div v-if="activeData.dataType === 'static'">
273
+              <el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
274
+                @click="addTreeItem">
275
+                添加父级
276
+              </el-button>
277
+            </div>
278
+            <el-divider />
279
+          </template>
280
+
281
+          <el-form-item v-if="activeData.optionType !== undefined" label="选项样式">
282
+            <el-radio-group v-model="activeData.optionType">
283
+              <el-radio-button label="default">
284
+                默认
285
+              </el-radio-button>
286
+              <el-radio-button label="button">
287
+                按钮
288
+              </el-radio-button>
289
+            </el-radio-group>
290
+          </el-form-item>
291
+          <el-form-item v-if="activeData['active-color'] !== undefined" label="开启颜色">
292
+            <el-color-picker v-model="activeData['active-color']" />
293
+          </el-form-item>
294
+          <el-form-item v-if="activeData['inactive-color'] !== undefined" label="关闭颜色">
295
+            <el-color-picker v-model="activeData['inactive-color']" />
296
+          </el-form-item>
297
+
298
+          <el-form-item v-if="activeData['allow-half'] !== undefined" label="允许半选">
299
+            <el-switch v-model="activeData['allow-half']" />
300
+          </el-form-item>
301
+          <el-form-item v-if="activeData['show-text'] !== undefined" label="辅助文字">
302
+            <el-switch v-model="activeData['show-text']" @change="rateTextChange" />
303
+          </el-form-item>
304
+          <el-form-item v-if="activeData['show-score'] !== undefined" label="显示分数">
305
+            <el-switch v-model="activeData['show-score']" @change="rateScoreChange" />
306
+          </el-form-item>
307
+          <el-form-item v-if="activeData['show-stops'] !== undefined" label="显示间断点">
308
+            <el-switch v-model="activeData['show-stops']" />
309
+          </el-form-item>
310
+          <el-form-item v-if="activeData.range !== undefined" label="范围选择">
311
+            <el-switch v-model="activeData.range" @change="rangeChange" />
312
+          </el-form-item>
313
+          <el-form-item v-if="activeData.border !== undefined && activeData.optionType === 'default'" label="是否带边框">
314
+            <el-switch v-model="activeData.border" />
315
+          </el-form-item>
316
+          <el-form-item v-if="activeData.tag === 'el-color-picker'" label="颜色格式">
317
+            <el-select v-model="activeData['color-format']" placeholder="请选择颜色格式" :style="{ width: '100%' }"
318
+              @change="colorFormatChange">
319
+              <el-option v-for="(item, index) in colorFormatOptions" :key="index" :label="item.label"
320
+                :value="item.value" />
321
+            </el-select>
322
+          </el-form-item>
323
+          <el-form-item v-if="activeData.size !== undefined &&
324
+            (activeData.optionType === 'button' ||
325
+              activeData.border ||
326
+              activeData.tag === 'el-color-picker')" label="选项尺寸">
327
+            <el-radio-group v-model="activeData.size">
328
+              <el-radio-button label="large">
329
+                较大
330
+              </el-radio-button>
331
+              <el-radio-button label="default">
332
+                默认
333
+              </el-radio-button>
334
+              <el-radio-button label="small">
335
+                较小
336
+              </el-radio-button>
337
+            </el-radio-group>
338
+          </el-form-item>
339
+          <el-form-item v-if="activeData['show-word-limit'] !== undefined" label="输入统计">
340
+            <el-switch v-model="activeData['show-word-limit']" />
341
+          </el-form-item>
342
+          <el-form-item v-if="activeData.tag === 'el-input-number'" label="严格步数">
343
+            <el-switch v-model="activeData['step-strictly']" />
344
+          </el-form-item>
345
+          <el-form-item v-if="activeData.tag === 'el-cascader'" label="是否多选">
346
+            <el-switch v-model="activeData.props.props.multiple" />
347
+          </el-form-item>
348
+          <el-form-item v-if="activeData.tag === 'el-cascader'" label="展示全路径">
349
+            <el-switch v-model="activeData['show-all-levels']" />
350
+          </el-form-item>
351
+          <el-form-item v-if="activeData.tag === 'el-cascader'" label="可否筛选">
352
+            <el-switch v-model="activeData.filterable" />
353
+          </el-form-item>
354
+          <el-form-item v-if="activeData.clearable !== undefined" label="能否清空">
355
+            <el-switch v-model="activeData.clearable" />
356
+          </el-form-item>
357
+          <el-form-item v-if="activeData.showTip !== undefined" label="显示提示">
358
+            <el-switch v-model="activeData.showTip" />
359
+          </el-form-item>
360
+          <el-form-item v-if="activeData.multiple !== undefined" label="多选文件">
361
+            <el-switch v-model="activeData.multiple" />
362
+          </el-form-item>
363
+          <el-form-item v-if="activeData['auto-upload'] !== undefined" label="自动上传">
364
+            <el-switch v-model="activeData['auto-upload']" />
365
+          </el-form-item>
366
+          <el-form-item v-if="activeData.readonly !== undefined" label="是否只读">
367
+            <el-switch v-model="activeData.readonly" />
368
+          </el-form-item>
369
+          <el-form-item v-if="activeData.disabled !== undefined" label="是否禁用">
370
+            <el-switch v-model="activeData.disabled" />
371
+          </el-form-item>
372
+          <el-form-item v-if="activeData.tag === 'el-select'" label="是否可搜索">
373
+            <el-switch v-model="activeData.filterable" />
374
+          </el-form-item>
375
+          <el-form-item v-if="activeData.tag === 'el-select'" label="是否多选">
376
+            <el-switch v-model="activeData.multiple" @change="multipleChange" />
377
+          </el-form-item>
378
+          <el-form-item v-if="activeData.required !== undefined" label="是否必填">
379
+            <el-switch v-model="activeData.required" />
380
+          </el-form-item>
381
+
382
+          <template v-if="activeData.layoutTree">
383
+            <el-divider>布局结构树</el-divider>
384
+            <el-tree :data="[activeData]" :props="layoutTreeProps" node-key="renderKey" default-expand-all draggable>
385
+              <template #default="{ node, data }">
386
+                <span class="node-label">
387
+                  <svg-icon class="node-icon" :icon-class="data.tagIcon" style="margin-right: 5px;" />
388
+                  {{ node.label }}
389
+                </span>
390
+              </template>
391
+            </el-tree>
392
+          </template>
393
+
394
+          <template v-if="activeData.layout === 'colFormItem'">
395
+            <el-divider>正则校验</el-divider>
396
+            <div v-for="(item, index) in activeData.regList" :key="index" class="reg-item">
397
+              <span class="close-btn" @click="activeData.regList.splice(index, 1)">
398
+                <el-icon>
399
+                  <Close />
400
+                </el-icon>
401
+              </span>
402
+              <el-form-item label="表达式">
403
+                <el-input v-model="item.pattern" placeholder="请输入正则" />
404
+              </el-form-item>
405
+              <el-form-item label="错误提示" style="margin-bottom:0">
406
+                <el-input v-model="item.message" placeholder="请输入错误提示" />
407
+              </el-form-item>
408
+            </div>
409
+            <div>
410
+              <el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
411
+                @click="addReg">
412
+                添加规则
413
+              </el-button>
414
+            </div>
415
+          </template>
416
+        </el-form>
417
+        <!-- 表单属性 -->
418
+        <el-form v-show="currentTab === 'form'" label-width="90px" label-position="top">
419
+          <el-form-item label="表单名">
420
+            <el-input v-model="formConf.formRef" placeholder="请输入表单名(ref)" />
421
+          </el-form-item>
422
+          <el-form-item label="表单模型">
423
+            <el-input v-model="formConf.formModel" placeholder="请输入数据模型" />
424
+          </el-form-item>
425
+          <el-form-item label="校验模型">
426
+            <el-input v-model="formConf.formRules" placeholder="请输入校验模型" />
427
+          </el-form-item>
428
+          <el-form-item label="表单尺寸">
429
+            <el-radio-group v-model="formConf.size">
430
+              <el-radio-button label="large" value="较大" />
431
+              <el-radio-button label="default" value="默认" />
432
+              <el-radio-button label="small" value="较小" />
433
+            </el-radio-group>
434
+          </el-form-item>
435
+          <el-form-item label="标签对齐">
436
+            <el-radio-group v-model="formConf.labelPosition">
437
+              <el-radio-button label="left" value="左对齐" />
438
+              <el-radio-button label="right" value="右对齐" />
439
+              <el-radio-button label="top" value="顶部对齐" />
440
+            </el-radio-group>
441
+          </el-form-item>
442
+          <el-form-item label="标签宽度">
443
+            <el-input-number v-model="formConf.labelWidth" placeholder="标签宽度" />
444
+          </el-form-item>
445
+          <el-form-item label="栅格间隔">
446
+            <el-input-number v-model="formConf.gutter" :min="0" placeholder="栅格间隔" />
447
+          </el-form-item>
448
+          <el-form-item label="禁用表单">
449
+            <el-switch v-model="formConf.disabled" />
450
+          </el-form-item>
451
+          <el-form-item label="表单按钮">
452
+            <el-switch v-model="formConf.formBtns" />
453
+          </el-form-item>
454
+          <el-form-item label="显示未选中组件边框">
455
+            <el-switch v-model="formConf.unFocusedComponentBorder" />
456
+          </el-form-item>
457
+        </el-form>
458
+      </el-scrollbar>
459
+    </div>
460
+    <icons-dialog v-model="iconsVisible" :current="activeData[currentIconModel]" @select="setIcon" />
461
+    <treeNode-dialog v-model="dialogVisible" @commit="addNode" />
462
+
463
+  </div>
464
+</template>
465
+
466
+<script setup>
467
+import draggable from "vuedraggable/dist/vuedraggable.common"
468
+import { isNumberStr } from '@/utils/index'
469
+import IconsDialog from './IconsDialog'
470
+import TreeNodeDialog from './TreeNodeDialog'
471
+import { inputComponents, selectComponents } from '@/utils/generator/config'
472
+
473
+const { proxy } = getCurrentInstance()
474
+const dateTimeFormat = {
475
+  date: 'YYYY-MM-DD',
476
+  week: 'YYYY 第 ww 周',
477
+  month: 'YYYY-MM',
478
+  year: 'YYYY',
479
+  datetime: 'YYYY-MM-DD HH:mm:ss',
480
+  daterange: 'YYYY-MM-DD',
481
+  monthrange: 'YYYY-MM',
482
+  datetimerange: 'YYYY-MM-DD HH:mm:ss'
483
+}
484
+const props = defineProps({
485
+  showField: Boolean,
486
+  activeData: Object,
487
+  formConf: Object
488
+})
489
+
490
+const data = reactive({
491
+  currentTab: 'field',
492
+  currentNode: null,
493
+  dialogVisible: false,
494
+  iconsVisible: false,
495
+  currentIconModel: null,
496
+  dateTypeOptions: [
497
+    {
498
+      label: '日(date)',
499
+      value: 'date'
500
+    },
501
+    {
502
+      label: '周(week)',
503
+      value: 'week'
504
+    },
505
+    {
506
+      label: '月(month)',
507
+      value: 'month'
508
+    },
509
+    {
510
+      label: '年(year)',
511
+      value: 'year'
512
+    },
513
+    {
514
+      label: '日期时间(datetime)',
515
+      value: 'datetime'
516
+    }
517
+  ],
518
+  dateRangeTypeOptions: [
519
+    {
520
+      label: '日期范围(daterange)',
521
+      value: 'daterange'
522
+    },
523
+    {
524
+      label: '月范围(monthrange)',
525
+      value: 'monthrange'
526
+    },
527
+    {
528
+      label: '日期时间范围(datetimerange)',
529
+      value: 'datetimerange'
530
+    }
531
+  ],
532
+  colorFormatOptions: [
533
+    {
534
+      label: 'hex',
535
+      value: 'hex'
536
+    },
537
+    {
538
+      label: 'rgb',
539
+      value: 'rgb'
540
+    },
541
+    {
542
+      label: 'rgba',
543
+      value: 'rgba'
544
+    },
545
+    {
546
+      label: 'hsv',
547
+      value: 'hsv'
548
+    },
549
+    {
550
+      label: 'hsl',
551
+      value: 'hsl'
552
+    }
553
+  ],
554
+  justifyOptions: [
555
+    {
556
+      label: 'start',
557
+      value: 'start'
558
+    },
559
+    {
560
+      label: 'end',
561
+      value: 'end'
562
+    },
563
+    {
564
+      label: 'center',
565
+      value: 'center'
566
+    },
567
+    {
568
+      label: 'space-around',
569
+      value: 'space-around'
570
+    },
571
+    {
572
+      label: 'space-between',
573
+      value: 'space-between'
574
+    }
575
+  ],
576
+  layoutTreeProps: {
577
+    label(data, node) {
578
+      return data.componentName || `${data.label}: ${data.vModel}`
579
+    }
580
+  }
581
+})
582
+
583
+const { currentTab, currentNode, dialogVisible, iconsVisible, currentIconModel, dateTypeOptions, dateRangeTypeOptions, colorFormatOptions, justifyOptions, layoutTreeProps } = toRefs(data)
584
+
585
+const documentLink = computed(() => props.activeData.document || 'https://element-plus.org/zh-CN/guide/installation')
586
+
587
+const dateOptions = computed(() => {
588
+  if (props.activeData.type !== undefined && props.activeData.tag === 'el-date-picker') {
589
+    if (props.activeData['start-placeholder'] === undefined) {
590
+      return dateTypeOptions.value
591
+    }
592
+    return dateRangeTypeOptions.value
593
+  }
594
+  return []
595
+})
596
+
597
+const tagList = ref([
598
+  {
599
+    label: '输入型组件',
600
+    options: inputComponents
601
+  },
602
+  {
603
+    label: '选择型组件',
604
+    options: selectComponents
605
+  }
606
+])
607
+
608
+const emit = defineEmits(['tag-change'])
609
+
610
+function addReg() {
611
+  props.activeData.regList.push({
612
+    pattern: '',
613
+    message: ''
614
+  })
615
+}
616
+function addSelectItem() {
617
+  props.activeData.options.push({
618
+    label: '',
619
+    value: ''
620
+  })
621
+}
622
+
623
+function addTreeItem() {
624
+  ++proxy.idGlobal
625
+  dialogVisible.value = true
626
+  currentNode.value = props.activeData.options
627
+}
628
+
629
+function renderContent(h, { node, data, store }) {
630
+  return h('div', {
631
+    class: "custom-tree-node"
632
+  }, [
633
+    h('span', node.label),
634
+    h('span', {
635
+      class: "node-operation"
636
+    }, [
637
+      h(resolveComponent('el-link'), {
638
+        type: "primary",
639
+        icon: "Plus",
640
+        underline: false,
641
+        onClick: () => {
642
+          append(data)
643
+
644
+        }
645
+      }),
646
+      h(resolveComponent('el-link'), {
647
+        type: "danger",
648
+        icon: "Delete",
649
+        underline: false,
650
+        style: "margin-left: 5px;",
651
+        onClick: () => {
652
+          remove(node, data)
653
+        }
654
+      })
655
+    ])
656
+  ])
657
+}
658
+function append(data) {
659
+  if (!data.children) {
660
+    data.children = []
661
+  }
662
+  dialogVisible.value = true
663
+  currentNode.value = data.children
664
+}
665
+function remove(node, data) {
666
+  const { parent } = node
667
+  const children = parent.data.children || parent.data
668
+  const index = children.findIndex(d => d.id === data.id)
669
+  children.splice(index, 1)
670
+}
671
+function addNode(data) {
672
+  currentNode.value.push(data)
673
+}
674
+
675
+function setOptionValue(item, val) {
676
+  item.value = isNumberStr(val) ? +val : val
677
+}
678
+function setDefaultValue(val) {
679
+  if (Array.isArray(val)) {
680
+    return val.join(',')
681
+  }
682
+  if (['string', 'number'].indexOf(val) > -1) {
683
+    return val
684
+  }
685
+  if (typeof val === 'boolean') {
686
+    return `${val}`
687
+  }
688
+  return val
689
+}
690
+
691
+function onDefaultValueInput(str) {
692
+  if (Array.isArray(props.activeData.defaultValue)) {
693
+    // 数组
694
+    props.activeData.defaultValue = str.split(',').map(val => (isNumberStr(val) ? +val : val))
695
+  } else if (['true', 'false'].indexOf(str) > -1) {
696
+    // 布尔
697
+    props.activeData.defaultValue = JSON.parse(str)
698
+  } else {
699
+    // 字符串和数字
700
+    props.activeData.defaultValue = isNumberStr(str) ? +str : str
701
+  }
702
+}
703
+
704
+function onSwitchValueInput(val, name) {
705
+  if (['true', 'false'].indexOf(val) > -1) {
706
+    props.activeData[name] = JSON.parse(val)
707
+  } else {
708
+    props.activeData[name] = isNumberStr(val) ? +val : val
709
+  }
710
+}
711
+
712
+function setTimeValue(val, type) {
713
+  const valueFormat = type === 'week' ? dateTimeFormat.date : val
714
+  props.activeData.defaultValue = null
715
+  props.activeData['value-format'] = valueFormat
716
+  props.activeData.format = val
717
+}
718
+
719
+function spanChange(val) {
720
+  props.formConf.span = val
721
+}
722
+
723
+function multipleChange(val) {
724
+  props.activeData.defaultValue = val ? [] : ''
725
+}
726
+
727
+function dateTypeChange(val) {
728
+  setTimeValue(dateTimeFormat[val], val)
729
+}
730
+
731
+function rangeChange(val) {
732
+  props.activeData.defaultValue = val ? [props.activeData.min, props.activeData.max] : props.activeData.min
733
+}
734
+
735
+function rateTextChange(val) {
736
+  if (val) props.activeData['show-score'] = false
737
+}
738
+
739
+function rateScoreChange(val) {
740
+  if (val) props.activeData['show-text'] = false
741
+}
742
+
743
+function colorFormatChange(val) {
744
+  props.activeData.defaultValue = null
745
+  props.activeData['show-alpha'] = val.indexOf('a') > -1
746
+  props.activeData.renderKey = +new Date() // 更新renderKey,重新渲染该组件
747
+}
748
+
749
+function openIconsDialog(model) {
750
+  iconsVisible.value = true
751
+  currentIconModel.value = model
752
+}
753
+
754
+function setIcon(val) {
755
+  props.activeData[currentIconModel.value] = val
756
+}
757
+
758
+function tagChange(tagIcon) {
759
+  let target = inputComponents.find(item => item.tagIcon === tagIcon)
760
+  if (!target) target = selectComponents.find(item => item.tagIcon === tagIcon)
761
+  emit('tag-change', target)
762
+}
763
+</script>
764
+
765
+<style lang="scss" scoped>
766
+.right-board {
767
+  width: 350px;
768
+  position: absolute;
769
+  right: 0;
770
+  top: 0;
771
+  padding-top: 3px;
772
+
773
+  &:deep() {
774
+    .el-tabs__header {
775
+      margin: 0;
776
+    }
777
+
778
+    .el-input-group__append .el-button {
779
+      display: inline-flex;
780
+    }
781
+  }
782
+
783
+  .field-box {
784
+    position: relative;
785
+    height: calc(100vh - 50px - 40px - 42px);
786
+    box-sizing: border-box;
787
+    overflow: hidden;
788
+  }
789
+
790
+  .el-scrollbar {
791
+    height: 100%;
792
+
793
+    &:deep() {
794
+      .el-scrollbar__view {
795
+        padding: 30px 20px;
796
+      }
797
+
798
+    }
799
+  }
800
+}
801
+
802
+.reg-item {
803
+  padding: 12px 6px;
804
+  background: var(--el-border-color-extra-light);
805
+  position: relative;
806
+  border-radius: 4px;
807
+
808
+  .close-btn {
809
+    position: absolute;
810
+    right: -6px;
811
+    top: -6px;
812
+    display: flex;
813
+    align-items: center;
814
+    justify-content: center;
815
+    width: 16px;
816
+    height: 16px;
817
+    line-height: 16px;
818
+    background: rgba(0, 0, 0, .2);
819
+    border-radius: 50%;
820
+    color: #fff;
821
+    z-index: 1;
822
+    cursor: pointer;
823
+    font-size: 12px;
824
+  }
825
+}
826
+
827
+.select-item {
828
+  display: flex;
829
+  border: 1px dashed #fff;
830
+  box-sizing: border-box;
831
+
832
+  & .close-btn {
833
+    cursor: pointer;
834
+    color: #f56c6c;
835
+  }
836
+
837
+  & .el-input+.el-input {
838
+    margin-left: 4px;
839
+  }
840
+}
841
+
842
+.select-item+.select-item {
843
+  margin-top: 4px;
844
+}
845
+
846
+.select-item.sortable-chosen {
847
+  border: 1px dashed #409eff;
848
+}
849
+
850
+.select-line-icon {
851
+  line-height: 32px;
852
+  font-size: 22px;
853
+  padding: 0 4px;
854
+  color: #777;
855
+}
856
+
857
+.option-drag {
858
+  cursor: move;
859
+}
860
+
861
+.time-range {
862
+  .el-date-editor {
863
+    width: 227px;
864
+  }
865
+
866
+  :deep() {
867
+    .el-icon-time {
868
+      display: none;
869
+    }
870
+  }
871
+}
872
+
873
+.document-link {
874
+  position: absolute;
875
+  display: flex;
876
+  width: 26px;
877
+  height: 26px;
878
+  top: 0;
879
+  left: 0;
880
+  cursor: pointer;
881
+  background: #409eff;
882
+  z-index: 1;
883
+  border-radius: 0 0 6px 0;
884
+  justify-content: center;
885
+  align-items: center;
886
+  color: #fff;
887
+  font-size: 18px;
888
+}
889
+
890
+.node-label {
891
+  font-size: 14px;
892
+}
893
+
894
+.node-icon {
895
+  color: #bebfc3;
896
+}
897
+
898
+.custom-tree-node {
899
+  flex: 1;
900
+  display: flex;
901
+  align-items: center;
902
+  justify-content: space-between;
903
+  font-size: 14px;
904
+  padding-right: 8px;
905
+}
906
+</style>

+ 93 - 0
src/views/tool/build/TreeNodeDialog.vue

@@ -0,0 +1,93 @@
1
+<template>
2
+  <div>
3
+    <el-dialog title="添加选项" v-model="open" width="800px" :close-on-click-modal="false" :modal-append-to-body="false"
4
+      @open="onOpen" @close="onClose">
5
+      <el-form ref="treeNodeForm" :model="formData" :rules="rules" label-width="100px">
6
+        <el-col :span="24">
7
+          <el-form-item label="选项名" prop="label">
8
+            <el-input v-model="formData.label" placeholder="请输入选项名" clearable />
9
+          </el-form-item>
10
+        </el-col>
11
+        <el-col :span="24">
12
+          <el-form-item label="选项值" prop="value">
13
+            <el-input v-model="formData.value" placeholder="请输入选项值" clearable>
14
+              <template #append>
15
+                <el-select v-model="dataType" :style="{ width: '100px' }">
16
+                  <el-option v-for="(item, index) in dataTypeOptions" :key="index" :label="item.label" :value="item.value"
17
+                    :disabled="item.disabled" />
18
+                </el-select>
19
+              </template>
20
+
21
+            </el-input>
22
+          </el-form-item>
23
+        </el-col>
24
+      </el-form>
25
+      <template #footer>
26
+        <div class="dialog-footer">
27
+          <el-button type="primary" @click="handelConfirm">确 定</el-button>
28
+          <el-button @click="onClose">取 消</el-button>
29
+        </div>
30
+      </template>
31
+    </el-dialog>
32
+  </div>
33
+</template>
34
+<script setup>
35
+const open = defineModel()
36
+const emit = defineEmits(['confirm'])
37
+const formData = ref({
38
+  label: undefined,
39
+  value: undefined
40
+})
41
+const rules = {
42
+  label: [
43
+    {
44
+      required: true,
45
+      message: '请输入选项名',
46
+      trigger: 'blur'
47
+    }
48
+  ],
49
+  value: [
50
+    {
51
+      required: true,
52
+      message: '请输入选项值',
53
+      trigger: 'blur'
54
+    }
55
+  ]
56
+}
57
+const dataType = ref('string')
58
+const dataTypeOptions = ref([
59
+  {
60
+    label: '字符串',
61
+    value: 'string'
62
+  },
63
+  {
64
+    label: '数字',
65
+    value: 'number'
66
+  }
67
+])
68
+const id = ref(100)
69
+const treeNodeForm = ref()
70
+
71
+function onOpen() {
72
+  formData.value = {
73
+    label: undefined,
74
+    value: undefined
75
+  }
76
+}
77
+
78
+function onClose() {
79
+  open.value = false
80
+}
81
+
82
+function handelConfirm() {
83
+  treeNodeForm.value.validate(valid => {
84
+    if (!valid) return
85
+    if (dataType.value === 'number') {
86
+      formData.value.value = parseFloat(formData.value.value)
87
+    }
88
+    formData.value.id = id.value++
89
+    emit('commit', formData.value)
90
+    onClose()
91
+  })
92
+}
93
+</script>

+ 653 - 0
src/views/tool/build/index.vue

@@ -0,0 +1,653 @@
1
+<template>
2
+  <div class="container">
3
+    <div class="left-board">
4
+      <div class="logo-wrapper">
5
+        <div class="logo">
6
+          <img :src="logo" alt="logo"> Form Generator
7
+        </div>
8
+      </div>
9
+      <el-scrollbar class="left-scrollbar">
10
+        <div class="components-list">
11
+          <div class="components-title">
12
+            <svg-icon icon-class="component" />输入型组件
13
+          </div>
14
+          <draggable class="components-draggable" :list="inputComponents"
15
+            :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
16
+            draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
17
+            <template #item="{ element, index }">
18
+              <div :key="index" class="components-item" @click="addComponent(element)">
19
+                <div class="components-body">
20
+                  <svg-icon :icon-class="element.tagIcon" />
21
+                  {{ element.label }}
22
+                </div>
23
+              </div>
24
+            </template>
25
+          </draggable>
26
+          <div class="components-title">
27
+            <svg-icon icon-class="component" />选择型组件
28
+          </div>
29
+          <draggable class="components-draggable" :list="selectComponents"
30
+            :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
31
+            draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
32
+            <template #item="{ element, index }">
33
+              <div :key="index" class="components-item" @click="addComponent(element)">
34
+                <div class="components-body">
35
+                  <svg-icon :icon-class="element.tagIcon" />
36
+                  {{ element.label }}
37
+                </div>
38
+              </div>
39
+            </template>
40
+          </draggable>
41
+          <div class="components-title">
42
+            <svg-icon icon-class="component" /> 布局型组件
43
+          </div>
44
+          <draggable class="components-draggable" :list="layoutComponents"
45
+            :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
46
+            draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
47
+            <template #item="{ element, index }">
48
+              <div :key="index" class="components-item" @click="addComponent(element)">
49
+                <div class="components-body">
50
+                  <svg-icon :icon-class="element.tagIcon" />
51
+                  {{ element.label }}
52
+                </div>
53
+              </div>
54
+            </template>
55
+          </draggable>
56
+        </div>
57
+      </el-scrollbar>
58
+    </div>
59
+    <div class="center-board">
60
+      <div class="action-bar">
61
+        <el-button icon="Download" type="primary" text @click="download">
62
+          导出vue文件
63
+        </el-button>
64
+        <el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copy">
65
+          复制代码
66
+        </el-button>
67
+        <el-button class="delete-btn" icon="Delete" text @click="empty" type="danger">
68
+          清空
69
+        </el-button>
70
+      </div>
71
+      <el-scrollbar class="center-scrollbar">
72
+        <el-row class="center-board-row" :gutter="formConf.gutter">
73
+          <el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled"
74
+            :label-width="formConf.labelWidth + 'px'">
75
+            <draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"
76
+              item-key="label">
77
+              <template #item="{ element, index }">
78
+                <draggable-item :key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index"
79
+                  :active-id="activeId" :form-conf="formConf" @activeItem="activeFormItem" @copyItem="drawingItemCopy"
80
+                  @deleteItem="drawingItemDelete" />
81
+              </template>
82
+            </draggable>
83
+            <div v-show="!drawingList.length" class="empty-info">
84
+              从左侧拖入或点选组件进行表单设计
85
+            </div>
86
+          </el-form>
87
+        </el-row>
88
+      </el-scrollbar>
89
+    </div>
90
+    <right-panel :active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length"
91
+      @tag-change="tagChange" />
92
+
93
+    <code-type-dialog v-model="dialogVisible" title="选择生成类型" :showFileName="showFileName" @confirm="generate" />
94
+    <input id="copyNode" type="hidden">
95
+  </div>
96
+</template>
97
+
98
+<script setup>
99
+import draggable from "vuedraggable/dist/vuedraggable.common"
100
+import ClipboardJS from 'clipboard'
101
+import beautifier from 'js-beautify'
102
+import logo from '@/assets/logo/logo.png'
103
+import { inputComponents, selectComponents, layoutComponents, formConf as formConfData } from '@/utils/generator/config'
104
+import { beautifierConf } from '@/utils/index'
105
+import drawingDefalut from '@/utils/generator/drawingDefalut'
106
+import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
107
+import { makeUpJs } from '@/utils/generator/js'
108
+import { makeUpCss } from '@/utils/generator/css'
109
+import Download from '@/plugins/download'
110
+import { ElNotification } from 'element-plus'
111
+import DraggableItem from './DraggableItem'
112
+import RightPanel from './RightPanel'
113
+import CodeTypeDialog from './CodeTypeDialog'
114
+import { onMounted, watch } from 'vue'
115
+
116
+const drawingList = ref(drawingDefalut)
117
+const { proxy } = getCurrentInstance()
118
+const dialogVisible = ref(false)
119
+const showFileName = ref(false)
120
+const operationType = ref('')
121
+const idGlobal = ref(100)
122
+const activeData = ref(drawingDefalut[0])
123
+const activeId = ref(drawingDefalut[0].formId)
124
+const generateConf = ref(null)
125
+const formData = ref({})
126
+const formConf = ref(formConfData)
127
+let oldActiveId
128
+let tempActiveData
129
+
130
+function activeFormItem(element) {
131
+  activeData.value = element
132
+  activeId.value = element.formId
133
+}
134
+function copy() {
135
+  dialogVisible.value = true
136
+  showFileName.value = false
137
+  operationType.value = 'copy'
138
+}
139
+function download() {
140
+  dialogVisible.value = true
141
+  showFileName.value = true
142
+  operationType.value = 'download'
143
+}
144
+function empty() {
145
+  proxy.$modal.confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then(() => {
146
+      idGlobal.value = 100
147
+      drawingList.value = []
148
+    }
149
+  )
150
+}
151
+
152
+function onEnd(obj, a) {
153
+  if (obj.from !== obj.to) {
154
+    activeData.value = tempActiveData
155
+    activeId.value = idGlobal.value
156
+  }
157
+}
158
+
159
+function addComponent(item) {
160
+  const clone = cloneComponent(item)
161
+  drawingList.value.push(clone)
162
+  activeFormItem(clone)
163
+}
164
+
165
+function cloneComponent(origin) {
166
+  const clone = JSON.parse(JSON.stringify(origin))
167
+  clone.formId = ++idGlobal.value
168
+  clone.span = formConf.value.span
169
+  clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
170
+  if (!clone.layout) clone.layout = 'colFormItem'
171
+  if (clone.layout === 'colFormItem') {
172
+    clone.vModel = `field${idGlobal.value}`
173
+    clone.placeholder !== undefined && (clone.placeholder += clone.label)
174
+    tempActiveData = clone
175
+  } else if (clone.layout === 'rowFormItem') {
176
+    delete clone.label
177
+    clone.componentName = `row${idGlobal.value}`
178
+    clone.gutter = formConf.value.gutter
179
+    tempActiveData = clone
180
+  }
181
+  return tempActiveData
182
+}
183
+
184
+function drawingItemCopy(item, parent) {
185
+  let clone = JSON.parse(JSON.stringify(item))
186
+  clone = createIdAndKey(clone)
187
+  parent.push(clone)
188
+  activeFormItem(clone)
189
+}
190
+
191
+
192
+function createIdAndKey(item) {
193
+  item.formId = ++idGlobal.value
194
+  item.renderKey = +new Date()
195
+  if (item.layout === 'colFormItem') {
196
+    item.vModel = `field${idGlobal.value}`
197
+  } else if (item.layout === 'rowFormItem') {
198
+    item.componentName = `row${idGlobal.value}`
199
+  }
200
+  if (Array.isArray(item.children)) {
201
+    item.children = item.children.map(childItem => createIdAndKey(childItem))
202
+  }
203
+  return item
204
+}
205
+
206
+function drawingItemDelete(index, parent) {
207
+  parent.splice(index, 1)
208
+  nextTick(() => {
209
+    const len = drawingList.value.length
210
+    if (len) {
211
+      activeFormItem(drawingList.value[len - 1])
212
+    }
213
+  })
214
+}
215
+
216
+function tagChange(newTag) {
217
+  newTag = cloneComponent(newTag)
218
+  newTag.vModel = activeData.value.vModel
219
+  newTag.formId = activeId.value
220
+  newTag.span = activeData.value.span
221
+  delete activeData.value.tag
222
+  delete activeData.value.tagIcon
223
+  delete activeData.value.document
224
+  Object.keys(newTag).forEach(key => {
225
+    if (activeData.value[key] !== undefined
226
+      && typeof activeData.value[key] === typeof newTag[key]) {
227
+      newTag[key] = activeData.value[key]
228
+    }
229
+  })
230
+  activeData.value = newTag
231
+  updateDrawingList(newTag, drawingList.value)
232
+}
233
+
234
+
235
+function updateDrawingList(newTag, list) {
236
+  const index = list.findIndex(item => item.formId === activeId.value)
237
+  if (index > -1) {
238
+    list.splice(index, 1, newTag)
239
+  } else {
240
+    list.forEach(item => {
241
+      if (Array.isArray(item.children)) updateDrawingList(newTag, item.children)
242
+    })
243
+  }
244
+}
245
+function generate(data) {
246
+  generateConf.value = data
247
+  nextTick(() => {
248
+    switch (operationType.value) {
249
+      case 'copy':
250
+        execCopy(data)
251
+        break
252
+      case 'download':
253
+        execDownload(data)
254
+        break
255
+      default:
256
+        break
257
+    }
258
+  })
259
+}
260
+
261
+function execDownload(data) {
262
+  const codeStr = generateCode()
263
+  const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
264
+  Download.saveAs(blob, data.fileName)
265
+}
266
+
267
+function execCopy(data) {
268
+  document.getElementById('copyNode').click()
269
+}
270
+function AssembleFormData() {
271
+  formData.value = { fields: JSON.parse(JSON.stringify(drawingList.value)), ...formConf.value }
272
+}
273
+function generateCode() {
274
+  const { type } = generateConf.value
275
+  AssembleFormData()
276
+  const script = vueScript(makeUpJs(formData.value, type))
277
+  const html = vueTemplate(makeUpHtml(formData.value, type))
278
+  const css = cssStyle(makeUpCss(formData.value))
279
+  return beautifier.html(html + script + css, beautifierConf.html)
280
+}
281
+watch(() => activeData.value.label, (val, oldVal) => {
282
+  if (
283
+    activeData.value.placeholder === undefined
284
+    || !activeData.value.tag
285
+    || oldActiveId !== activeId.value
286
+  ) {
287
+    return
288
+  }
289
+  activeData.value.placeholder = activeData.value.placeholder.replace(oldVal, '') + val
290
+})
291
+watch(activeId, (val) => {
292
+  oldActiveId = val
293
+}, { immediate: true })
294
+
295
+onMounted(() => {
296
+  const clipboard = new ClipboardJS('#copyNode', {
297
+    text: trigger => {
298
+      const codeStr = generateCode()
299
+      ElNotification({ title: '成功', message: '代码已复制到剪切板,可粘贴。', type: 'success' })
300
+      return codeStr
301
+    }
302
+  })
303
+  clipboard.on('error', e => {
304
+    proxy.$modal.msgError('代码复制失败')
305
+  })
306
+})
307
+</script>
308
+
309
+<style lang='scss'>
310
+$lighterBlue: #409EFF;
311
+
312
+.container {
313
+  position: relative;
314
+  width: 100%;
315
+  background-color: var(--el-bg-color-overlay);
316
+  height: calc(100vh - 50px - 40px);
317
+  overflow: hidden;
318
+
319
+  .left-board {
320
+    width: 260px;
321
+    position: absolute;
322
+    left: 0;
323
+    top: 0;
324
+    height: calc(100vh - 50px - 40px);
325
+
326
+    .logo-wrapper {
327
+      position: relative;
328
+      height: 42px;
329
+      border-bottom: 1px solid var(--el-border-color-extra-light);
330
+      box-sizing: border-box;
331
+
332
+      .logo {
333
+        position: absolute;
334
+        left: 12px;
335
+        top: 6px;
336
+        line-height: 30px;
337
+        color: #00afff;
338
+        font-weight: 600;
339
+        font-size: 17px;
340
+        white-space: nowrap;
341
+
342
+        >img {
343
+          width: 30px;
344
+          height: 30px;
345
+          vertical-align: top;
346
+        }
347
+
348
+        .github {
349
+          display: inline-block;
350
+          vertical-align: sub;
351
+          margin-left: 15px;
352
+
353
+          >img {
354
+            height: 22px;
355
+          }
356
+        }
357
+      }
358
+    }
359
+
360
+    .left-scrollbar {
361
+      .el-scrollbar__wrap {
362
+        box-sizing: border-box;
363
+        overflow-x: hidden !important;
364
+        margin-bottom: 0 !important;
365
+
366
+        .components-list {
367
+          padding: 8px;
368
+          box-sizing: border-box;
369
+          height: 100%;
370
+
371
+          .components-title {
372
+            font-size: 14px;
373
+            // color: #222;
374
+            margin: 6px 2px;
375
+
376
+            .svg-icon {
377
+              // color: #666;
378
+              font-size: 18px;
379
+              margin-right: 5px;
380
+            }
381
+          }
382
+
383
+          .components-draggable {
384
+            padding-bottom: 20px;
385
+
386
+            .components-item {
387
+              display: inline-block;
388
+              width: 48%;
389
+              margin: 1%;
390
+              transition: transform 0ms !important;
391
+
392
+              .components-body {
393
+                padding: 8px 10px;
394
+                background: var(--el-border-color-extra-light);
395
+                font-size: 12px;
396
+                cursor: move;
397
+                border: 1px dashed var(--el-border-color-extra-light);
398
+                border-radius: 3px;
399
+
400
+                .svg-icon {
401
+                  // color: #777;
402
+                  font-size: 15px;
403
+                  margin-right: 5px;
404
+                }
405
+
406
+                &:hover {
407
+                  border: 1px dashed #787be8;
408
+                  color: #787be8;
409
+
410
+                  .svg-icon {
411
+                    color: #787be8;
412
+                  }
413
+                }
414
+              }
415
+            }
416
+          }
417
+
418
+
419
+        }
420
+      }
421
+    }
422
+  }
423
+
424
+  .center-board {
425
+    height: calc(100vh - 50px - 40px);
426
+    width: auto;
427
+    margin: 0 350px 0 260px;
428
+    box-sizing: border-box;
429
+
430
+    .action-bar {
431
+      position: relative;
432
+      height: 42px;
433
+      padding: 0 15px;
434
+      box-sizing: border-box;
435
+      ;
436
+      border: 1px solid var(--el-border-color-extra-light);
437
+      border-top: none;
438
+      border-left: none;
439
+      display: flex;
440
+      align-items: center;
441
+      justify-content: flex-end;
442
+
443
+      u .delete-btn {
444
+        color: #F56C6C;
445
+      }
446
+    }
447
+
448
+    .center-scrollbar {
449
+      height: calc(100vh - 50px - 40px - 42px);
450
+      overflow: hidden;
451
+      border-left: 1px solid var(--el-border-color-extra-light);
452
+      border-right: 1px solid var(--el-border-color-extra-light);
453
+      box-sizing: border-box;
454
+
455
+      .el-scrollbar__view {
456
+        overflow-x: hidden;
457
+      }
458
+
459
+      .center-board-row {
460
+        padding: 12px 12px 15px 12px;
461
+        box-sizing: border-box;
462
+
463
+        &>.el-form {
464
+          // 69 = 12+15+42
465
+          height: calc(100vh - 50px - 40px - 69px);
466
+          flex: 1;
467
+
468
+          .drawing-board {
469
+            height: 100%;
470
+            position: relative;
471
+
472
+            .components-body {
473
+              padding: 0;
474
+              margin: 0;
475
+              font-size: 0;
476
+            }
477
+
478
+            .sortable-ghost {
479
+              position: relative;
480
+              display: block;
481
+              overflow: hidden;
482
+
483
+              &::before {
484
+                content: " ";
485
+                position: absolute;
486
+                left: 0;
487
+                right: 0;
488
+                top: 0;
489
+                height: 3px;
490
+                background: rgb(89, 89, 223);
491
+                z-index: 2;
492
+              }
493
+            }
494
+
495
+            .components-item.sortable-ghost {
496
+              width: 100%;
497
+              height: 60px;
498
+              background: var(--el-border-color-extra-light);
499
+            }
500
+
501
+            .active-from-item {
502
+              &>.el-form-item {
503
+                background: var(--el-border-color-extra-light);
504
+                border-radius: 6px;
505
+              }
506
+
507
+              &>.drawing-item-copy,
508
+              &>.drawing-item-delete {
509
+                display: initial;
510
+              }
511
+
512
+              &>.component-name {
513
+                color: $lighterBlue;
514
+              }
515
+
516
+              .el-input__wrapper {
517
+                box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;
518
+              }
519
+            }
520
+
521
+            .el-form-item {
522
+              margin-bottom: 15px;
523
+            }
524
+          }
525
+
526
+          .drawing-item {
527
+            position: relative;
528
+            cursor: move;
529
+
530
+            &.unfocus-bordered:not(.activeFromItem)>div:first-child {
531
+              border: 1px dashed #ccc;
532
+            }
533
+
534
+            .el-form-item {
535
+              padding: 12px 10px;
536
+            }
537
+          }
538
+
539
+          .drawing-row-item {
540
+            position: relative;
541
+            cursor: move;
542
+            box-sizing: border-box;
543
+            border: 1px dashed #ccc;
544
+            border-radius: 3px;
545
+            padding: 0 2px;
546
+            margin-bottom: 15px;
547
+
548
+            .drawing-row-item {
549
+              margin-bottom: 2px;
550
+            }
551
+
552
+            .el-col {
553
+              margin-top: 22px;
554
+            }
555
+
556
+            .el-form-item {
557
+              margin-bottom: 0;
558
+            }
559
+
560
+            .drag-wrapper {
561
+              min-height: 80px;
562
+              flex: 1;
563
+              display: flex;
564
+              flex-wrap: wrap;
565
+            }
566
+
567
+            &.active-from-item {
568
+              border: 1px dashed $lighterBlue;
569
+            }
570
+
571
+            .component-name {
572
+              position: absolute;
573
+              top: 0;
574
+              left: 0;
575
+              font-size: 12px;
576
+              color: #bbb;
577
+              display: inline-block;
578
+              padding: 0 6px;
579
+            }
580
+          }
581
+
582
+          .drawing-item,
583
+          .drawing-row-item {
584
+            &:hover {
585
+              &>.el-form-item {
586
+                background: var(--el-border-color-extra-light);
587
+                border-radius: 6px;
588
+              }
589
+
590
+              &>.drawing-item-copy,
591
+              &>.drawing-item-delete {
592
+                display: initial;
593
+              }
594
+            }
595
+
596
+            &>.drawing-item-copy,
597
+            &>.drawing-item-delete {
598
+              display: none;
599
+              position: absolute;
600
+              top: -10px;
601
+              width: 22px;
602
+              height: 22px;
603
+              line-height: 22px;
604
+              text-align: center;
605
+              border-radius: 50%;
606
+              font-size: 12px;
607
+              border: 1px solid;
608
+              cursor: pointer;
609
+              z-index: 1;
610
+            }
611
+
612
+            &>.drawing-item-copy {
613
+              right: 56px;
614
+              border-color: $lighterBlue;
615
+              color: $lighterBlue;
616
+              background: #fff;
617
+
618
+              &:hover {
619
+                background: $lighterBlue;
620
+                color: #fff;
621
+              }
622
+            }
623
+
624
+            &>.drawing-item-delete {
625
+              right: 24px;
626
+              border-color: #F56C6C;
627
+              color: #F56C6C;
628
+              background: #fff;
629
+
630
+              &:hover {
631
+                background: #F56C6C;
632
+                color: #fff;
633
+              }
634
+            }
635
+          }
636
+
637
+          .empty-info {
638
+            position: absolute;
639
+            top: 46%;
640
+            left: 0;
641
+            right: 0;
642
+            text-align: center;
643
+            font-size: 18px;
644
+            color: #ccb1ea;
645
+            letter-spacing: 4px;
646
+          }
647
+
648
+        }
649
+      }
650
+    }
651
+  }
652
+}
653
+</style>

+ 48 - 0
src/views/tool/gen/basicInfoForm.vue

@@ -0,0 +1,48 @@
1
+<template>
2
+  <el-form ref="basicInfoForm" :model="info" :rules="rules" label-width="150px">
3
+    <el-row>
4
+      <el-col :span="12">
5
+        <el-form-item label="表名称" prop="tableName">
6
+          <el-input placeholder="请输入仓库名称" v-model="info.tableName" />
7
+        </el-form-item>
8
+      </el-col>
9
+      <el-col :span="12">
10
+        <el-form-item label="表描述" prop="tableComment">
11
+          <el-input placeholder="请输入" v-model="info.tableComment" />
12
+        </el-form-item>
13
+      </el-col>
14
+      <el-col :span="12">
15
+        <el-form-item label="实体类名称" prop="className">
16
+          <el-input placeholder="请输入" v-model="info.className" />
17
+        </el-form-item>
18
+      </el-col>
19
+      <el-col :span="12">
20
+        <el-form-item label="作者" prop="functionAuthor">
21
+          <el-input placeholder="请输入" v-model="info.functionAuthor" />
22
+        </el-form-item>
23
+      </el-col>
24
+      <el-col :span="24">
25
+        <el-form-item label="备注" prop="remark">
26
+          <el-input type="textarea" :rows="3" v-model="info.remark"></el-input>
27
+        </el-form-item>
28
+      </el-col>
29
+    </el-row>
30
+  </el-form>
31
+</template>
32
+
33
+<script setup>
34
+defineProps({
35
+  info: {
36
+    type: Object,
37
+    default: null
38
+  }
39
+})
40
+
41
+// 表单校验
42
+const rules = ref({
43
+  tableName: [{ required: true, message: "请输入表名称", trigger: "blur" }],
44
+  tableComment: [{ required: true, message: "请输入表描述", trigger: "blur" }],
45
+  className: [{ required: true, message: "请输入实体类名称", trigger: "blur" }],
46
+  functionAuthor: [{ required: true, message: "请输入作者", trigger: "blur" }]
47
+})
48
+</script>

+ 0 - 0
src/views/tool/gen/createTable.vue


部分文件因为文件数量过多而无法显示