Преглед на файлове

移动端-课程管理,人员小节,课程小节实现

liuzhuo преди 2 дни
родител
ревизия
891dc6ac86

+ 346
- 0
src/components/AttachmentS3Required.vue Целия файл

@@ -0,0 +1,346 @@
1
+<template>
2
+	<div style="padding: 5px;flex: 1">
3
+		<div>
4
+			<van-uploader :disabled="readonly" v-model="fileListTemp"
5
+				accept=".txt,.pdf,.doc,.docx,.xls,.xlsx,.wps,.et,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/*"
6
+				:after-read="uploadFile" upload-icon="plus" :show-upload="plusIcon" :max-count="1" />
7
+		</div>
8
+
9
+		<van-row v-for="item in tableData" :key="item.id" class="table-row">
10
+			<van-col :span="15">
11
+				<div style="display: flex; align-items: center; padding: 10px;">
12
+					<div :class="getFileIconClass(item.fileType) + ' file-icon'">
13
+						<van-icon :name="getFileIcon(item.fileType)" />
14
+					</div>
15
+					<div class="file-name">{{ item.fileName }}</div>
16
+				</div>
17
+			</van-col>
18
+			<van-col :span="9">
19
+				<div class="action-buttons">
20
+					<van-button size="small" plain type="primary" circle @click="handlePreview(item)">
21
+						<van-icon name="eye-o" />
22
+					</van-button>
23
+					<van-button size="small" plain type="success" circle @click="handleDownload(item)">
24
+						<van-icon name="down" />
25
+					</van-button>
26
+					<van-button size="small" plain type="danger" circle @click="handleDelete(item)">
27
+						<van-icon name="delete-o" />
28
+					</van-button>
29
+				</div>
30
+			</van-col>
31
+		</van-row>
32
+		<van-dialog v-model:show="showDialogVisible" title="删除文件" show-cancel-button confirm-button-color="#ee0124"
33
+			message="确定删除该文件吗?" @confirm="onDelete" />
34
+	</div>
35
+</template>
36
+
37
+
38
+<script setup>
39
+	import {
40
+		ref,
41
+		onMounted,
42
+		getCurrentInstance,
43
+		watch
44
+	} from 'vue'
45
+	const {
46
+		proxy
47
+	} = getCurrentInstance()
48
+
49
+	const props = defineProps({
50
+		fId: {
51
+			type: String,
52
+			default: () => ''
53
+		},
54
+		value1: {
55
+			type: String,
56
+			default: () => null
57
+		},
58
+		value2: {
59
+			type: String,
60
+			default: () => null
61
+		},
62
+		value3: {
63
+			type: String,
64
+			default: () => null
65
+		},
66
+		value4: {
67
+			type: String,
68
+			default: () => null
69
+		},
70
+		value5: {
71
+			type: String,
72
+			default: () => null
73
+		},
74
+		selectButton: {
75
+			type: Boolean,
76
+			default: () => true
77
+		},
78
+		deleteButton: {
79
+			type: Boolean,
80
+			default: () => true
81
+		},
82
+		readonly: {
83
+			type: Boolean,
84
+			default: () => false
85
+		},
86
+		plusIcon: {
87
+			type: Boolean,
88
+			default: () => true
89
+		},
90
+		isFile: {
91
+			type: Boolean,
92
+			default: () => false
93
+		}
94
+	})
95
+
96
+	const initImage = 'image/*'
97
+	const initFile =
98
+		'.txt,.pdf,.doc,.docx,.xls,.xlsx,.wps,.et,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/*'
99
+
100
+	onMounted(() => {
101
+		getTableData()
102
+	})
103
+
104
+	const tableData = ref([])
105
+	const getTableData = () => {
106
+		//携带自定义参数
107
+		var url = 'framework/Common/queryFileWithValues'
108
+		var param = {
109
+			fId: props.fId, //必填
110
+			value1: props.value1, //非必填
111
+			value2: props.value2, //非必填
112
+			value3: props.value3, //非必填
113
+			value4: props.value4, //非必填
114
+			value5: props.value5, //非必填
115
+		}
116
+		proxy.$axios.get(url, param).then(response => {
117
+			if (response.data.code === 0) {
118
+				tableData.value = response.data.data
119
+			} else {
120
+				showFailToast('失败!' + response.data.msg)
121
+			}
122
+		})
123
+	}
124
+	const headers = ref({
125
+		token: localStorage.getItem('token'),
126
+		userId: localStorage.getItem('userId')
127
+	})
128
+	const fileListTemp = ref([])
129
+	const bucket = ref(import.meta.env.VITE_BUCKET)
130
+
131
+	/* 转成blob */
132
+	import {
133
+		atob
134
+	} from 'js-base64';
135
+
136
+	function base64ToBlob(base64) {
137
+		const contentType = base64.substring(base64.indexOf(':') + 1, base64.indexOf(';'))
138
+		const byteCharacters = atob(base64);
139
+		const byteArrays = [];
140
+
141
+		for (let i = 0; i < byteCharacters.length; i++) {
142
+			byteArrays.push(byteCharacters.charCodeAt(i));
143
+		}
144
+
145
+		const byteArray = new Uint8Array(byteArrays);
146
+		return new Blob([byteArray], {
147
+			type: contentType
148
+		});
149
+	}
150
+	watch(props, (newVal, oldVal) => {
151
+		getTableData()
152
+
153
+	})
154
+	const uploadFile = (file) => {
155
+		const formData = new FormData()
156
+		formData.append('fId', props.fId);
157
+		formData.append('bucket', bucket.value);
158
+		formData.append('value1', props.value1 || '');
159
+		formData.append('value2', props.value2 || '');
160
+		formData.append('value3', props.value3 || '');
161
+		formData.append('value4', props.value4 || '');
162
+		formData.append('value5', props.value5 || '');
163
+		formData.append('file', file.file);
164
+		formData.append('fileName', file.file.name);
165
+		var url = 'framework/Common/uploadFileS3'
166
+		proxy.$axios.post(url, formData, 'multipart/form-data').then(response => {
167
+			if (response.data.code === 0 || response.data.code === '0') {
168
+				showSuccessToast('上传成功')
169
+				getTableData()
170
+			} else {
171
+				showFailToast('上传失败:' + (response.data.msg || '未知错误'))
172
+			}
173
+		}).catch(error => {
174
+			showFailToast('上传失败:' + (error.message || '网络错误'))
175
+		});
176
+	}
177
+
178
+	/* 在线预览 */
179
+	import {
180
+		Base64
181
+	} from "js-base64"
182
+	import {
183
+		showFailToast,
184
+		showSuccessToast
185
+	} from 'vant';
186
+
187
+	import tools from "../tools" 
188
+	const handlePreview = (row) => { 
189
+		var baseUrl = ''
190
+		if (tools.tool.isImage(row.fileType)) {
191
+			baseUrl = import.meta.env.VITE_BASE_API
192
+		} else {
193
+			baseUrl = import.meta.env.VITE_PREVIEW_BASE_API
194
+		}
195
+		var originUrl = baseUrl + 'framework/Common/downloadFileS3?bucket=' + bucket
196
+			.value +
197
+			'&id=' + row.id
198
+		var previewUrl = originUrl + '&fullfilename=' + Date.now() + row.fileName
199
+		var url = import.meta.env.VITE_PREVIEW_API + 'onlinePreview?url=' + encodeURIComponent(Base64.encode(
200
+			previewUrl)) + '&officePreviewType=pdf'
201
+		window.open(url);
202
+	}
203
+
204
+	/* 在线下载 */
205
+	const handleDownload = (row) => {
206
+		location.href = import.meta.env.VITE_BASE_API + '/framework/Common/downloadFileS3?bucket=' + bucket.value +
207
+			'&id=' + row.id
208
+	}
209
+
210
+	/* 在线删除 */
211
+	const showDialogVisible = ref(false)
212
+	const deleteInfo = ref()
213
+	const handleDelete = (row) => {
214
+		showDialogVisible.value = true
215
+		deleteInfo.value = row
216
+	}
217
+	const onDelete = () => {
218
+		let url = 'framework/Common/removeFile'
219
+		let param = {
220
+			id: deleteInfo.value.id
221
+		}
222
+		proxy.$axios.post(url, param).then(response => {
223
+			if (response.data.code === 0) {
224
+				showSuccessToast('删除成功!')
225
+				getTableData()
226
+			} else {
227
+				showFailToast('删除失败;' + response.data.msg)
228
+			}
229
+		});
230
+	}
231
+
232
+	// 获取文件图标类名
233
+	const getFileIconClass = (fileType) => {
234
+		if (fileType === 'pdf') return 'pdf-icon';
235
+		if (fileType === 'image' || fileType === 'jpg' || fileType === 'png' || fileType === 'gif')
236
+			return 'image-icon';
237
+		if (fileType === 'doc' || fileType === 'docx') return 'doc-icon';
238
+		if (fileType === 'xls' || fileType === 'xlsx') return 'excel-icon';
239
+		if (fileType === 'txt') return 'text-icon';
240
+		return 'other-icon';
241
+	}
242
+
243
+	// 获取文件图标
244
+	const getFileIcon = (fileType) => {
245
+		if (fileType === 'pdf') return 'description';
246
+		if (fileType === 'image' || fileType === 'jpg' || fileType === 'png' || fileType === 'gif') return 'photo-o';
247
+		if (fileType === 'doc' || fileType === 'docx') return 'notes-o';
248
+		if (fileType === 'xls' || fileType === 'xlsx') return 'chart-trending-o';
249
+		if (fileType === 'txt') return 'document';
250
+		return 'description';
251
+	}
252
+
253
+	const convertBytesToSize = (bytes) => {
254
+		const kb = 1024;
255
+		const mb = kb * 1024;
256
+
257
+		if (bytes < kb) return bytes + ' B';
258
+		else if (bytes < mb) return (bytes / kb).toFixed(2) + ' KB';
259
+		else return (bytes / mb).toFixed(2) + ' MB';
260
+	}
261
+
262
+	// 获取文件数量,用于验证
263
+	const getFileCount = () => {
264
+		return tableData.value.length
265
+	}
266
+
267
+	// 验证是否至少有一个文件
268
+	const validate = () => {
269
+		return tableData.value.length > 0
270
+	}
271
+
272
+	// 刷新文件列表
273
+	const refreshFileList = () => {
274
+		getTableData()
275
+	}
276
+
277
+	// 暴露方法供父组件调用
278
+	defineExpose({
279
+		getFileCount,
280
+		validate,
281
+		refreshFileList
282
+	})
283
+</script>
284
+
285
+<style scoped>
286
+	* {
287
+		box-sizing: border-box;
288
+		margin: 0;
289
+		padding: 0;
290
+	}
291
+
292
+	body {
293
+		font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
294
+		background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
295
+		color: #333;
296
+		line-height: 1.6;
297
+		min-height: 100vh;
298
+	}
299
+
300
+	.header h1 {
301
+		font-size: 2.5rem;
302
+		color: #1989fa;
303
+		margin-bottom: 10px;
304
+		text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
305
+	}
306
+
307
+	.header p {
308
+		font-size: 1.1rem;
309
+		color: #666;
310
+		max-width: 600px;
311
+		margin: 0 auto;
312
+	}
313
+
314
+	.section-title i {
315
+		margin-right: 10px;
316
+		font-size: 1.6rem;
317
+	}
318
+
319
+	.table-row {
320
+		background-color: white;
321
+		border-radius: 8px;
322
+		margin-bottom: 8px;
323
+		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
324
+		transition: transform 0.2s ease, box-shadow 0.2s ease;
325
+		align-items: center;
326
+	}
327
+
328
+	.table-row:hover {
329
+		transform: translateX(5px);
330
+		box-shadow: 0 5px 15px rgba(25, 137, 250, 0.15);
331
+	}
332
+
333
+	.file-name {
334
+		font-weight: 500;
335
+		white-space: nowrap;
336
+		overflow: hidden;
337
+		text-overflow: ellipsis;
338
+	}
339
+
340
+	.action-buttons {
341
+		display: flex;
342
+		gap: 10px;
343
+		justify-content: flex-start;
344
+	}
345
+</style>
346
+

+ 626
- 0
src/components/OrganizationalWithLeafUserForCourse.vue Целия файл

@@ -0,0 +1,626 @@
1
+<template>
2
+  <van-popup :close-on-click-overlay="false" v-model:show="deptCascadeVisible" position="bottom">
3
+    <div class="content-container">
4
+      <div class="action-buttons-container">
5
+        <div style="color:#dc3545;" @click="close">取消</div>
6
+        <div>部门选择</div>
7
+        <div style="color: #007bff" @click="switchPeople">下一步</div>
8
+      </div>
9
+      <div class="cascade-container">
10
+        <van-cascader
11
+          v-model="cascadeValue"
12
+          title="请选择组织"
13
+          :options="options"
14
+          :swipeable="true"
15
+          :columns-num="3"
16
+          @change="SelectedDeptChange"
17
+          :field-names="{
18
+            text: 'text',
19
+            value: 'value',
20
+            children: 'children'
21
+          }"
22
+        />
23
+      </div>
24
+    </div>
25
+  </van-popup>
26
+  <van-popup :close-on-click-overlay="false" v-model:show="dialogVisible" position="bottom">
27
+
28
+    <div class="action-buttons-container">
29
+      <div style="color: #ffc107" @click="switchDept">返回</div>
30
+      <div>人员选择</div>
31
+      <div v-if="multiple" style="color: #28a745" @click="handleSelect">确认</div>
32
+    </div>
33
+
34
+    <div class="content-container">
35
+      <div class="search-container">
36
+        <div class="search-label">人员查询</div>
37
+        <div class="search-row">
38
+          <van-field v-model="userName" class="search-field" placeholder="请输入姓名"></van-field>
39
+          <div class="action-buttons">
40
+            <van-button icon="Search" type="primary" @click="getAllTableData">查询</van-button>
41
+          </div>
42
+        </div>
43
+      </div>
44
+
45
+      <div class="table-container">
46
+        <div class="table-label">人员选择</div>
47
+        <div class="table">
48
+          <div class="table-header">
49
+            <div class="table-column">
50
+              <div class="th" style="flex: 0 0 40px;"></div>
51
+              <div class="th">工号</div>
52
+              <div class="th">姓名</div>
53
+              <div v-if="!multiple" class="th">操作</div>
54
+            </div>
55
+          </div>
56
+          <div class="table-body">
57
+            <!-- 多选模式 -->
58
+            <van-checkbox-group v-if="multiple" v-model="checked" @change="onChange">
59
+              <van-checkbox v-for="item in tableData" shape="square" :name="item.user">
60
+                <div class="employee-row">
61
+                  <div class="employee-id">{{ item.user.userCode }}</div>
62
+                  <div class="employee-name">{{ item.user.userDesc }}</div>
63
+                </div>
64
+              </van-checkbox>
65
+            </van-checkbox-group>
66
+            <!-- 单选模式 -->
67
+            <div v-else class="employee-list">
68
+              <div
69
+                v-for="item in tableData"
70
+                :key="item.user.id"
71
+                class="employee-item"
72
+              >
73
+                <div class="employee-row">
74
+                  <div class="employee-id">{{ item.user.userCode }}</div>
75
+                  <div class="employee-name">{{ item.user.userDesc }}</div>
76
+                  <div class="employee-name">
77
+                    <van-button style="margin: 10px"
78
+                                icon="Select"
79
+                                type="success"
80
+                                size="small"
81
+                                plain
82
+                                @click="handleSelectSingle(item)"
83
+                    >
84
+                      选择
85
+                    </van-button>
86
+                  </div>
87
+                </div>
88
+              </div>
89
+            </div>
90
+          </div>
91
+        </div>
92
+      </div>
93
+    </div>
94
+  </van-popup>
95
+</template>
96
+
97
+
98
+<script setup>
99
+import { ref, getCurrentInstance, nextTick } from 'vue';
100
+import { showFailToast } from 'vant';
101
+
102
+const {
103
+  proxy
104
+} = getCurrentInstance();
105
+
106
+const props = defineProps({
107
+  multiple: {
108
+    type: Boolean,
109
+    default: false
110
+  },
111
+  mainKey: {
112
+    type: String,
113
+    default: ''
114
+  }
115
+});
116
+
117
+const getUserCodeList = (mainKey) => {
118
+  if (!mainKey) return [];
119
+  const json = sessionStorage.getItem(mainKey);
120
+  try {
121
+    return json ? JSON.parse(json) : [];
122
+  } catch (error) {
123
+    return [];
124
+  }
125
+};
126
+
127
+/* 组织树 */
128
+const deptCascadeVisible = ref(false)
129
+const switchPeople = () => {
130
+  // 多选模式:初始化缓存数据
131
+  if (props.multiple && props.mainKey) {
132
+    selectedUsers.value = getUserCodeList(props.mainKey);
133
+  }
134
+  deptCascadeVisible.value = false
135
+  dialogVisible.value = true
136
+}
137
+
138
+/* 人员列表 */
139
+const dialogVisible = ref(false);
140
+const switchDept = () => {
141
+  if (props.multiple && props.mainKey) {
142
+    // 多选模式:删除上次已选数据
143
+    for (let item of lastCheckedUsers.value) {
144
+      selectedUsers.value = selectedUsers.value.filter(user => user.userCode !== item.userCode);
145
+    }
146
+
147
+    // 切换其他部门前将数据加入缓存
148
+    for (let item of checkedUsers.value) {
149
+      selectedUsers.value.push(item);
150
+    }
151
+    sessionStorage.setItem(props.mainKey, JSON.stringify(selectedUsers.value));
152
+  }
153
+  dialogVisible.value = false
154
+  deptCascadeVisible.value = true
155
+}
156
+
157
+/* 读取用户所在部门信息 */
158
+const deptJson = localStorage.getItem('dept');
159
+const deptInformation = ref([]);
160
+try {
161
+  deptInformation.value = deptJson ? JSON.parse(deptJson) : [];
162
+} catch (error) {
163
+  deptInformation.value = [];
164
+}
165
+const deptCode = deptInformation.value[0]?.deptCode || '';
166
+const rawSuperDeptCode = deptCode.length > 2 ? deptCode.slice(0, -2) : '';
167
+
168
+/* 读取用户角色信息 */
169
+const roleJson = localStorage.getItem('role');
170
+const roleInformation = ref([]);
171
+try {
172
+  roleInformation.value = roleJson ? JSON.parse(roleJson) : [];
173
+} catch (error) {
174
+  roleInformation.value = [];
175
+}
176
+const roleCodes = roleInformation.value
177
+  .filter(item => item.roleCode && item.roleCode.includes('SX0'))
178
+  .map(item => item.roleCode);
179
+
180
+const hasRole = (codes) => {
181
+  if (!roleCodes.length) return false;
182
+  return roleCodes.some(code => codes.some(target => code.includes(target)));
183
+};
184
+
185
+const normalizeSuperDept = (code) => {
186
+  if (!code) return '';
187
+  if (code === 'D11011301') return 'D110113';
188
+  if (code === 'D1101') return 'D11';
189
+  return code;
190
+};
191
+
192
+/* 打开和关闭弹出框 */
193
+const currentDeptCode = ref('');
194
+const open = async () => {
195
+  // 多选模式:初始化缓存数据
196
+  if (props.multiple && props.mainKey) {
197
+    selectedUsers.value = getUserCodeList(props.mainKey);
198
+  }
199
+
200
+  // 始终使用 'D' 查询所有组织,确保显示完整组织树
201
+  // currentDeptCode 仅用于默认选择,不影响查询
202
+  const userDeptCode = deptInformation.value[0]?.deptCode || 'D';
203
+  currentDeptCode.value = userDeptCode;
204
+
205
+  // 先显示弹窗,再加载数据
206
+  deptCascadeVisible.value = true;
207
+  
208
+  // 重置并加载数据
209
+  options.value = [];
210
+  cascadeValue.value = '';
211
+  
212
+  // getSafeDataTree 内部会强制使用 'D' 查询所有组织
213
+  await getSafeDataTree('D');
214
+};
215
+const close = () => {
216
+  deptCascadeVisible.value = false
217
+  dialogVisible.value = false;
218
+};
219
+defineExpose({
220
+  open,
221
+  close
222
+});
223
+
224
+/* 条件查询该部门下的所有人员信息 */
225
+const currentPage = ref(1);
226
+const pageSize = ref(10);
227
+const userName = ref('');
228
+const tableData = ref([]);
229
+
230
+/* 多选模式相关 */
231
+const selectedUsers = ref([]);
232
+const lastCheckedUsers = ref([]);
233
+const checkedUsers = ref([]);
234
+const checked = ref([]);
235
+
236
+const currentDeptId = ref('');
237
+/* 获取表格信息 */
238
+const getTableData = async () => {
239
+  var url = 'framework/SysUserDept/queryByDeptUserNamePage';
240
+  var param = {
241
+    page: currentPage.value,
242
+    rows: pageSize.value,
243
+    deptId: currentDeptId.value,
244
+    userName: userName.value
245
+  };
246
+  await proxy.$axios.get(url, param).then(response => {
247
+    if (response.data.code === 0) {
248
+      tableData.value = response.data.data.records;
249
+      pageSize.value = response.data.data.total;
250
+
251
+      // 多选模式:处理缓存数据
252
+      if (props.multiple && props.mainKey) {
253
+        selectedUsers.value = getUserCodeList(props.mainKey);
254
+        lastCheckedUsers.value = [];
255
+        checked.value = [];
256
+        for (let item of tableData.value) {
257
+          if (selectedUsers.value.find(content => content.userCode === item.user.userCode)) {
258
+            checked.value.push(item.user);
259
+            lastCheckedUsers.value.push(item.user);
260
+          }
261
+        }
262
+      }
263
+    } else {
264
+      showFailToast('操作失败!' + response.data.msg);
265
+    }
266
+  });
267
+};
268
+
269
+const getAllTableData = async () => {
270
+  await getTableData();
271
+  await getTableData();
272
+};
273
+
274
+const SelectedDeptChange = (selectInfo) => {
275
+  currentDeptInfo.value = findNodeWithBacktrack(dataTree.value[0], selectInfo.value);
276
+  currentDeptId.value = currentDeptInfo.value.id;
277
+  getAllTableData();
278
+};
279
+
280
+/* 选中数据返回主页面 */
281
+const emit = defineEmits(['receiveFromChild']);
282
+
283
+// 单选模式:选择单个人员
284
+const handleSelectSingle = (item) => {
285
+  emit('receiveFromChild', item);
286
+  close();
287
+};
288
+
289
+// 多选模式:确认选择
290
+const handleSelect = () => {
291
+  if (!props.multiple) return;
292
+  
293
+  // 确保 selectedUsers 已初始化
294
+  if (props.mainKey && selectedUsers.value.length === 0) {
295
+    selectedUsers.value = getUserCodeList(props.mainKey);
296
+  }
297
+  
298
+  // 删除上次已选数据
299
+  for (let item of lastCheckedUsers.value) {
300
+    selectedUsers.value = selectedUsers.value.filter(user => user.userCode !== item.userCode);
301
+  }
302
+
303
+  // 将当前选中的数据加入缓存
304
+  for (let item of checkedUsers.value) {
305
+    // 避免重复添加
306
+    if (!selectedUsers.value.find(user => user.userCode === item.userCode)) {
307
+      selectedUsers.value.push(item);
308
+    }
309
+  }
310
+  
311
+  if (props.mainKey) {
312
+    sessionStorage.setItem(props.mainKey, JSON.stringify(selectedUsers.value));
313
+  }
314
+
315
+  emit('receiveFromChild', selectedUsers.value);
316
+  close();
317
+};
318
+
319
+const onChange = (value) => {
320
+  if (props.multiple) {
321
+    checkedUsers.value = value;
322
+  }
323
+};
324
+
325
+// 转换部门数据为 cascade 格式
326
+function convertToCascadeFormat(data) {
327
+  if (!data || !Array.isArray(data)) return [];
328
+  return data.map(item => ({
329
+    text: item.deptName,
330
+    value: item.deptCode,
331
+    children: item.children && item.children.length > 0 ? convertToCascadeFormat(item.children) : []
332
+  }));
333
+}
334
+
335
+// 叶子节点去掉children
336
+const processTreeNodes  = (node) => {
337
+  // 如果节点没有子节点,删除children属性并返回
338
+  if (!node.children || node.children.length === 0) {
339
+    if (node.children) delete node.children;
340
+    return;
341
+  }
342
+
343
+  // 递归处理所有子节点
344
+  node.children.forEach(child => processTreeNodes(child));
345
+
346
+  // 检查当前节点是否应该保留children属性
347
+  const hasChildrenWithChildren = node.children.some(child =>
348
+    child.children && child.children.length > 0
349
+  );
350
+
351
+  // 如果所有子节点都没有子节点(都是叶子节点),删除它们的children属性
352
+  if (!hasChildrenWithChildren) {
353
+    node.children.forEach(child => {
354
+      if (child.children) delete child.children;
355
+    });
356
+  }
357
+}
358
+
359
+const dataTree = ref([]);
360
+const options = ref([]);
361
+const cascadeValue = ref();
362
+const currentDeptInfo = ref({});
363
+const getSafeDataTree = async (deptCode) => {
364
+  // 强制使用 'D' 查询所有组织,确保显示完整组织树
365
+  const finalDeptCode = 'D';
366
+  
367
+  let url = 'sgsafe/SafeDepartment/queryDeptTreeWithLeafByDeptCode';
368
+  let param = {
369
+    deptCode: finalDeptCode
370
+  };
371
+  
372
+  await proxy.$axios.get(url, param).then(res => {
373
+    if (!res || !res.data || !res.data.data) {
374
+      showFailToast('获取组织架构数据失败');
375
+      return;
376
+    }
377
+    
378
+    dataTree.value = res.data.data;
379
+    
380
+    // 不过滤部门树,直接使用原始数据,显示完整组织架构
381
+    options.value = convertToCascadeFormat(dataTree.value);
382
+
383
+    options.value.forEach(child => {
384
+      processTreeNodes(child)
385
+    })
386
+
387
+    /* 组件默认选择用户当前部门,如果查询的是所有组织('D'),则使用用户实际部门代码 */
388
+    const userDeptCode = deptInformation.value[0]?.deptCode || 'D';
389
+    const defaultDeptCode = finalDeptCode === 'D' ? userDeptCode : currentDeptCode.value;
390
+    cascadeValue.value = defaultDeptCode;
391
+
392
+    /* 默认返回当前部门所有信息 */
393
+    if (dataTree.value && dataTree.value.length > 0) {
394
+      currentDeptInfo.value = findNodeWithBacktrack(dataTree.value[0], defaultDeptCode);
395
+      if (currentDeptInfo.value && currentDeptInfo.value.id) {
396
+        currentDeptId.value = currentDeptInfo.value.id;
397
+        getAllTableData();
398
+      } else {
399
+        // 如果找不到默认部门,则选择根节点
400
+        if (dataTree.value[0]) {
401
+          currentDeptInfo.value = dataTree.value[0];
402
+          currentDeptId.value = dataTree.value[0].id;
403
+          cascadeValue.value = dataTree.value[0].deptCode;
404
+          getAllTableData();
405
+        }
406
+      }
407
+    }
408
+  }).catch(error => {
409
+    showFailToast('获取组织架构数据出错');
410
+  });
411
+};
412
+
413
+/* 根据deptCode查出部门信息 */
414
+function findNodeWithBacktrack(node, targetDeptCode) {
415
+  if (node.deptCode === targetDeptCode) {
416
+    return node;
417
+  }
418
+  if (node.children && node.children.length > 0) {
419
+    for (const child of node.children) {
420
+      const found = findNodeWithBacktrack(child, targetDeptCode);
421
+      if (found && found.deptCode) {
422
+        return found;
423
+      }
424
+    }
425
+  }
426
+  return {};
427
+}
428
+
429
+</script>
430
+
431
+<style scoped>
432
+
433
+:deep(.van-checkbox__label) {
434
+  flex: 1;
435
+}
436
+
437
+* {
438
+  margin: 0;
439
+  padding: 0;
440
+  box-sizing: border-box;
441
+  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
442
+}
443
+
444
+body {
445
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
446
+  min-height: 100vh;
447
+  padding: 20px;
448
+  display: flex;
449
+  justify-content: center;
450
+  align-items: flex-start;
451
+}
452
+
453
+.content-container {
454
+  height: 100%;
455
+  display: flex;
456
+  flex-direction: column;
457
+  background: #f8fafd;
458
+}
459
+
460
+.search-container {
461
+  background: #f8fafd;
462
+  border-radius: 12px;
463
+  padding: 15px;
464
+  margin-bottom: 20px;
465
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
466
+}
467
+
468
+.search-label {
469
+  font-size: 14px;
470
+  color: #5a7eb5;
471
+  font-weight: 500;
472
+  margin-bottom: 8px;
473
+  display: flex;
474
+  align-items: center;
475
+}
476
+
477
+.search-label::before {
478
+  content: "";
479
+  display: inline-block;
480
+  width: 4px;
481
+  height: 16px;
482
+  background: #4a90e2;
483
+  border-radius: 2px;
484
+  margin-right: 8px;
485
+}
486
+
487
+.search-row {
488
+  display: flex;
489
+  align-items: center;
490
+  flex-wrap: wrap;
491
+  gap: 12px;
492
+  padding: 10px;
493
+  background: white;
494
+  border-radius: 8px;
495
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
496
+}
497
+
498
+.search-field {
499
+  flex: 1;
500
+  min-width: 250px;
501
+}
502
+
503
+.action-buttons {
504
+  display: flex;
505
+  gap: 12px;
506
+}
507
+
508
+.table-container {
509
+  background: #f8fafd;
510
+  border-radius: 12px;
511
+  padding: 15px;
512
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
513
+}
514
+
515
+.table-label {
516
+  font-size: 14px;
517
+  color: #5a7eb5;
518
+  font-weight: 500;
519
+  margin-bottom: 8px;
520
+  display: flex;
521
+  align-items: center;
522
+}
523
+
524
+.table-label::before {
525
+  content: "";
526
+  display: inline-block;
527
+  width: 4px;
528
+  height: 16px;
529
+  background: #4a90e2;
530
+  border-radius: 2px;
531
+  margin-right: 8px;
532
+}
533
+
534
+.table {
535
+  width: 100%;
536
+  border-collapse: separate;
537
+  border-spacing: 0;
538
+  background: white;
539
+  border-radius: 8px;
540
+  overflow: hidden;
541
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
542
+}
543
+
544
+.table-header {
545
+  background: linear-gradient(120deg, #f0f7ff, #e6f1ff);
546
+}
547
+
548
+.table-column {
549
+  display: flex;
550
+  width: 100%;
551
+  border-bottom: 1px solid #e0eeff;
552
+}
553
+
554
+.th {
555
+  padding: 16px 15px;
556
+  font-weight: 600;
557
+  color: #3a6ab7;
558
+  flex: 1;
559
+}
560
+
561
+.table-body .table-column {
562
+  transition: background-color 0.2s;
563
+  border-bottom: 1px solid #f0f7ff;
564
+}
565
+
566
+.table-body .tr:last-child {
567
+  border-bottom: none;
568
+}
569
+
570
+.table-body .tr:hover {
571
+  background-color: #f8fbff;
572
+}
573
+
574
+.employee-row {
575
+  display: flex;
576
+  width: 100%;
577
+}
578
+
579
+.employee-id, .employee-name {
580
+  flex: 1;
581
+  padding: 0 15px;
582
+  display: flex;
583
+  align-items: center;
584
+}
585
+
586
+.employee-row {
587
+  transition: all 0.3s ease;
588
+}
589
+
590
+.van-checkbox {
591
+  width: 100%;
592
+  padding: 12px 15px;
593
+}
594
+
595
+.van-checkbox__label {
596
+  width: 100%;
597
+}
598
+
599
+.action-buttons-container {
600
+  display: flex;
601
+  justify-content: space-between;
602
+  align-items: center;
603
+  padding: 15px 20px;
604
+  background: white;
605
+  border-bottom: 1px solid #e8e8e8;
606
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
607
+  z-index: 10;
608
+}
609
+
610
+.cascade-container {
611
+  flex: 1;
612
+  overflow: hidden;
613
+  background: rgb(255, 255, 255);
614
+  height: calc(100vh - 200px);
615
+  min-height: 400px;
616
+}
617
+
618
+.cascade-container :deep(.van-cascader) {
619
+  height: 100%;
620
+}
621
+
622
+.cascade-container :deep(.van-cascader__content) {
623
+  height: calc(100% - 44px);
624
+}
625
+
626
+</style>

+ 21
- 0
src/router/index.ts Целия файл

@@ -659,6 +659,27 @@ const router = createRouter({
659 659
 			name: '安全费用预算编辑',
660 660
 			component: () => import('@/view/moneySafe/safeMoneyBudgetList.vue')
661 661
 		},
662
+		{
663
+			path: '/courseManagement',
664
+			name: '课程管理',
665
+			component: () => import('@/view/dati/courseManagement/courseManagement.vue')
666
+		},
667
+		{
668
+			path: '/courseManagementList',
669
+			name: '课程管理编辑',
670
+			component: () => import('@/view/dati/courseManagement/courseManagementList.vue')
671
+		},
672
+		{
673
+			path: '/courseAddPeo',
674
+			name: '课程添加人员',
675
+			component: () => import('@/view/dati/courseManagement/addPeo.vue')
676
+		},
677
+		{
678
+			path: '/section',
679
+			name: '课程添加小节',
680
+			component: () => import('@/view/dati/courseManagement/section.vue')
681
+		},
682
+
662 683
 	]
663 684
 })
664 685
 

+ 4
- 0
src/view/Home2.vue Целия файл

@@ -132,6 +132,10 @@
132 132
           <img src="../../public/images/zyqk.png" width="45rpx" />
133 133
           <span class="vanicon_text">考试任务</span>
134 134
         </van-grid-item>
135
+        <van-grid-item to="/courseManagement" v-if="showCheckTake">
136
+          <img src="../../public/images/zyqk.png" width="45rpx" />
137
+          <span class="vanicon_text">课程管理</span>
138
+        </van-grid-item>
135 139
       </van-grid>
136 140
     </div>
137 141
     <div class="card">

+ 362
- 0
src/view/dati/courseManagement/addPeo.vue Целия файл

@@ -0,0 +1,362 @@
1
+<template>
2
+  <div class="h5-container">
3
+    <van-nav-bar title="添加人员" @click-left="onClickLeft">
4
+      <template #right>
5
+        <van-icon name="add" size="25" color="#000" @click="handleDepartmentLeaderNameOne" />
6
+      </template>
7
+    </van-nav-bar>
8
+
9
+    <!-- 项目列表 -->
10
+    <van-pull-refresh v-model="isRefreshing" success-text="刷新成功" @refresh="onRefresh">
11
+      <van-list v-model:loading="isLoading" :finished="isFinished" finished-text="没有更多了" offset="200" @load="onLoad">
12
+        <div v-for="(item, idx) in resultData" :key="item.id || item.userId || idx">
13
+          <van-swipe-cell title-style="color: #007aff" style="height: 80px;" :ref="el => getSwipeCellRef(el, idx)">
14
+            <template #default>
15
+              <div class="swipe-cell-default">
16
+                <van-cell style="height: 100%; display: flex; align-items: center;">
17
+                  <template #title>
18
+                    <div class="cell-title">
19
+                      {{ item.userName }}
20
+                    </div>
21
+                  </template>
22
+                  <template #label>
23
+                    <div>工号:{{ item.userCode }}</div>
24
+                  </template>
25
+                </van-cell>
26
+                <div class="swipe-cell-default-icon">
27
+                  <van-icon v-if="openStatus[idx]" name="arrow-double-left" @click.stop="openSwipe(idx)" />
28
+                  <van-icon v-else name="arrow-double-right" @click.stop="closeSwipe(idx)" />
29
+                </div>
30
+              </div>
31
+            </template>
32
+
33
+            <template #right>
34
+              <van-button square class="delete-button" text="删除" @click="deleteRowUser(item)" />
35
+            </template>
36
+          </van-swipe-cell>
37
+        </div>
38
+      </van-list>
39
+    </van-pull-refresh>
40
+
41
+    <!-- 人员选择组件 -->
42
+    <OrganizationalWithLeafUserForCourse
43
+        ref="PopupDepartmentLeaderNameRef2"
44
+        :multiple="false"
45
+        @receiveFromChild="getDepartmentLeaderNameOne"
46
+    />
47
+  </div>
48
+</template>
49
+
50
+<script setup>
51
+import { ref, onMounted, getCurrentInstance } from 'vue';
52
+import { showFailToast, showSuccessToast, showToast, showConfirmDialog } from 'vant';
53
+import OrganizationalWithLeafUserForCourse from '@/components/OrganizationalWithLeafUserForCourse.vue';
54
+import { useRoute, useRouter } from 'vue-router';
55
+
56
+const { proxy } = getCurrentInstance();
57
+const router = useRouter();
58
+const route = useRoute();
59
+
60
+const onClickLeft = () => {
61
+  history.back();
62
+};
63
+
64
+const guid = () => {
65
+  function S4() {
66
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
67
+  }
68
+  return (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4());
69
+};
70
+
71
+const leaderKey = guid();
72
+const ClassId = ref('');
73
+const planInfo = ref({});
74
+
75
+if (route.query.data) {
76
+  planInfo.value = JSON.parse(route.query.data);
77
+  ClassId.value = planInfo.value.id;
78
+  sessionStorage.setItem('id', ClassId.value);
79
+}
80
+
81
+if (!planInfo.value || !ClassId.value) {
82
+  showToast({
83
+    type: 'error',
84
+    message: '请退出重试'
85
+  });
86
+}
87
+
88
+const PopupDepartmentLeaderNameRef2 = ref();
89
+const handleDepartmentLeaderNameOne = () => {
90
+  if (PopupDepartmentLeaderNameRef2.value) {
91
+    PopupDepartmentLeaderNameRef2.value.open();
92
+  }
93
+};
94
+
95
+
96
+const getDepartmentLeaderNameOne = (item) => {
97
+  // 检查 item 和 item.user 是否存在
98
+  if (!item || !item.user) {
99
+    showFailToast('用户数据无效');
100
+    return;
101
+  }
102
+
103
+  // 获取当前已有的用户列表(从 resultData 中提取,格式为 userCode-userName)
104
+  const currentUsers = resultData.value
105
+    .filter(user => user && user.userCode)
106
+    .map(user => `${user.userCode}-${user.userName || user.userDesc || ''}`);
107
+  
108
+  // 检查用户是否已存在
109
+  const newUserStr = `${item.user.userCode}-${item.user.userDesc || ''}`;
110
+  if (currentUsers.includes(newUserStr)) {
111
+    showFailToast('该用户已添加,请勿重复添加');
112
+    return;
113
+  }
114
+
115
+  // 构建新的用户列表(包含已有用户和新用户),格式为 userCode-userName
116
+  const allUsers = [...currentUsers, newUserStr];
117
+  userNameArr.value = allUsers.join(',');
118
+
119
+  // 保存用户
120
+  usersave();
121
+};
122
+
123
+
124
+const userNameArr = ref([]);
125
+const getDepartmentLeaderName = (userArray) => {
126
+  // 处理用户对象数组,提取 userCode
127
+  if (userArray && userArray.length > 0) {
128
+    if (typeof userArray[0] === 'string') {
129
+      // 如果是字符串数组,直接使用
130
+      userNameArr.value = userArray.join(',');
131
+    } else {
132
+      // 如果是对象数组,提取 userCode
133
+      userNameArr.value = userArray.map(user => user.userCode).join(',');
134
+    }
135
+  } else {
136
+    userNameArr.value = '';
137
+  }
138
+  usersave();
139
+};
140
+
141
+const usersave = () => {
142
+  const classId = sessionStorage.getItem('id') || ClassId.value;
143
+  
144
+  if (!classId) {
145
+    showFailToast('课程ID不存在,请重新进入页面');
146
+    return;
147
+  }
148
+
149
+  if (!userNameArr.value || userNameArr.value.trim() === '') {
150
+    showFailToast('用户列表为空,无法保存');
151
+    return;
152
+  }
153
+
154
+  const url = '/sgsafe/Class/userSave';
155
+  const param = {
156
+    json: userNameArr.value,
157
+    classId: classId
158
+  };
159
+
160
+  proxy.$axios.post(url, param).then(response => {
161
+    if (response.data.code == '0' || response.data.code === 0) {
162
+      showSuccessToast('保存成功');
163
+      // 重新获取用户列表
164
+      getClassUserData();
165
+    } else {
166
+      // 根据错误信息显示更友好的提示
167
+      const errorMsg = response.data.msg || '未知错误';
168
+      if (errorMsg.includes('未配置小节') || errorMsg.includes('小节')) {
169
+        showFailToast('该课程未配置小节,请先添加小节后再添加人员');
170
+      } else {
171
+        showFailToast('操作失败:' + errorMsg);
172
+      }
173
+    }
174
+  }).catch(error => {
175
+    showFailToast('保存失败:' + (error.message || '网络错误'));
176
+  });
177
+};
178
+
179
+const isRefreshing = ref(false);
180
+const isLoading = ref(false);
181
+const isFinished = ref(false);
182
+const currentPage = ref(1);
183
+const pageSize = ref(10);
184
+const totalRows = ref(0);
185
+const resultData = ref([]);
186
+const tableData = ref([]);
187
+
188
+const getClassUserData = async () => {
189
+  const classId = sessionStorage.getItem('id') || ClassId.value;
190
+  if (!classId) {
191
+    return;
192
+  }
193
+
194
+  const url = '/sgsafe/Class/queryUserById';
195
+  const param = {
196
+    params: classId
197
+  };
198
+
199
+  try {
200
+    const response = await proxy.$axios.get(url, param);
201
+    if (response.data.code == '0') {
202
+      const userList = response.data.data || [];
203
+      tableData.value = userList;
204
+      totalRows.value = userList.length;
205
+      
206
+      // 直接更新 resultData,确保数据能立即显示
207
+      resultData.value = [...userList];
208
+      openStatus.value = new Array(resultData.value.length).fill(true);
209
+      isFinished.value = true;
210
+      isLoading.value = false;
211
+    } else {
212
+      showToast({
213
+        type: 'error',
214
+        message: '操作失败!' + (response.data.msg || '未知错误')
215
+      });
216
+    }
217
+  } catch (error) {
218
+    showToast({
219
+      type: 'error',
220
+      message: '获取数据失败:' + (error.message || '网络错误')
221
+    });
222
+  }
223
+};
224
+
225
+const onRefresh = () => {
226
+  basicReset();
227
+  onLoad();
228
+};
229
+
230
+const onLoad = async () => {
231
+  if (isRefreshing.value) {
232
+    resultData.value = [];
233
+    currentPage.value = 1;
234
+    isRefreshing.value = false;
235
+  }
236
+
237
+  try {
238
+    await getClassUserData();
239
+    // getClassUserData 已经直接更新了 resultData,这里只需要标记完成
240
+    isLoading.value = false;
241
+  } catch (error) {
242
+    isFinished.value = true;
243
+    isLoading.value = false;
244
+  }
245
+};
246
+
247
+const basicReset = () => {
248
+  isFinished.value = false;
249
+  isLoading.value = true;
250
+  currentPage.value = 1;
251
+  resultData.value = [];
252
+};
253
+
254
+const deleteRowUser = (row) => {
255
+  showConfirmDialog({
256
+    title: '提示',
257
+    message: '确定要删除吗?'
258
+  }).then(() => {
259
+    const qiyongUser = ref({});
260
+    qiyongUser.value = { ...row };
261
+    qiyongUser.value.cancelFlag = '1';
262
+    const url = '/sgsafe/Class/delectUser';
263
+    const param = {
264
+      json: JSON.stringify(qiyongUser.value)
265
+    };
266
+    proxy.$axios.post(url, param).then(response => {
267
+      if (response.data.code == '0') {
268
+        showSuccessToast('删除成功');
269
+        getClassUserData();
270
+      } else {
271
+        showToast({
272
+          type: 'error',
273
+          message: '操作失败!' + response.data.msg
274
+        });
275
+      }
276
+    });
277
+  }).catch(() => {
278
+  });
279
+};
280
+
281
+const openStatus = ref([]);
282
+const swipeCellRefs = ref([]);
283
+const getSwipeCellRef = (el, index) => {
284
+  if (el) {
285
+    swipeCellRefs.value[index] = el;
286
+  }
287
+};
288
+
289
+const openSwipe = (idx) => {
290
+  openStatus.value = new Array(resultData.value.length).fill(true);
291
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
292
+    openStatus.value[idx] = false;
293
+    swipeCellRefs.value[idx].open('right');
294
+  }
295
+  document.addEventListener('click', handleDocumentClick);
296
+};
297
+
298
+const handleDocumentClick = (event) => {
299
+  openStatus.value = new Array(resultData.value.length).fill(true);
300
+};
301
+
302
+const closeSwipe = (idx) => {
303
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
304
+    openStatus.value[idx] = true;
305
+    swipeCellRefs.value[idx].close();
306
+  }
307
+};
308
+
309
+onMounted(() => {
310
+  // 确保 ClassId 已设置
311
+  const classId = sessionStorage.getItem('id') || ClassId.value;
312
+  if (classId) {
313
+    if (!ClassId.value) {
314
+      ClassId.value = classId;
315
+    }
316
+    getClassUserData();
317
+  }
318
+});
319
+</script>
320
+
321
+<style scoped>
322
+.h5-container {
323
+  width: 100%;
324
+  padding: 5px;
325
+  box-sizing: border-box;
326
+}
327
+
328
+.cell-title {
329
+  display: -webkit-box;
330
+  -webkit-box-orient: vertical;
331
+  -webkit-line-clamp: 2;
332
+  overflow: hidden;
333
+  text-overflow: ellipsis;
334
+  line-height: 1.5;
335
+  max-height: calc(1.5em * 2);
336
+  font-size: 16px;
337
+  font-weight: bold;
338
+  color: #333;
339
+}
340
+
341
+.swipe-cell-default {
342
+  display: flex;
343
+  background-color: #ffffff;
344
+  justify-content: center;
345
+  align-items: center;
346
+}
347
+
348
+.swipe-cell-default-icon {
349
+  width: 60px;
350
+  display: flex;
351
+  justify-content: center;
352
+}
353
+
354
+.delete-button {
355
+  height: 100%;
356
+  border: none;
357
+  color: #ff0000;
358
+  background-image: url('@/assets/img/del.png');
359
+  background-size: auto 100%;
360
+  background-repeat: no-repeat;
361
+}
362
+</style>

+ 357
- 0
src/view/dati/courseManagement/courseManagement.vue Целия файл

@@ -0,0 +1,357 @@
1
+<template>
2
+  <div class="h5-container">
3
+    <van-nav-bar title="课程管理" @click-left="onClickLeft" @click-right="handAdd">
4
+      <template #right>
5
+        <van-icon name="add" size="25" color="#000" />
6
+      </template>
7
+    </van-nav-bar>
8
+    <van-search v-model="query.name" show-action placeholder="请输入课程名称" @search="onRefresh"
9
+      @cancel="handdelect" />
10
+
11
+    <!-- 年份选择 -->
12
+    <van-field
13
+      v-model="selectedYearText"
14
+      readonly
15
+      is-link
16
+      label="年份"
17
+      placeholder="请选择年份"
18
+      @click="showYearPicker = true"
19
+    />
20
+
21
+    <!-- 项目列表 -->
22
+    <van-pull-refresh v-model="isRefreshing" success-text="刷新成功" @refresh="onRefresh">
23
+      <van-list v-model:loading="isLoading" :finished="isFinished" finished-text="没有更多了" offset="200" @load="onLoad">
24
+        <div v-for="(item, idx) in resultData" :key="item.id">
25
+          <van-swipe-cell title-style="color: #007aff" style="height: 80px;" :ref="el => getSwipeCellRef(el, idx)">
26
+            <template #default>
27
+              <div class="swipe-cell-default">
28
+                <van-cell style="height: 100%; display: flex; align-items: flex-start;" @click="queryInfo(item)">
29
+                  <template #title>
30
+                    <div class="cell-title">
31
+                      {{ item.projectName }}
32
+                    </div>
33
+                  </template>
34
+                  <template #label>
35
+                    <div>培训学时:{{ item.trainHours }}</div>
36
+                    <div>培训类别:{{ item.projectType }}</div>
37
+                  </template>
38
+                </van-cell>
39
+                <div class="swipe-cell-default-icon">
40
+                  <van-icon v-if="openStatus[idx]" name="arrow-double-left" @click.stop="openSwipe(idx)" />
41
+                  <van-icon v-else name="arrow-double-right" @click.stop="closeSwipe(idx)" />
42
+                </div>
43
+              </div>
44
+            </template>
45
+
46
+            <template #right>
47
+              <van-button square class="edit-button" text="编辑" @click="openTrain(item)" />
48
+              <van-button square class="delete-button" text="删除" @click="handleDelete(item)" />
49
+              <van-button square class="submit-button" text="添加人员" @click="openClassName(item)" />
50
+              <van-button square class="submit-button" text="添加小节" @click="jumpsection(item)" />
51
+            </template>
52
+          </van-swipe-cell>
53
+        </div>
54
+
55
+      </van-list>
56
+    </van-pull-refresh>
57
+
58
+    <!-- 删除确认弹窗 -->
59
+    <van-dialog v-model:show="deleteDialogVisible" show-cancel-button @confirm="confirmDelete">
60
+      <template #title>
61
+        <div>删除确认</div>
62
+      </template>
63
+      <div style="padding: 30px;">确定要删除该课程吗?</div>
64
+    </van-dialog>
65
+
66
+    <!-- 年份选择器 -->
67
+    <van-popup v-model:show="showYearPicker" round position="bottom">
68
+      <van-picker
69
+        :columns="yearOptions"
70
+        @confirm="onYearConfirm"
71
+        @cancel="showYearPicker = false"
72
+      />
73
+    </van-popup>
74
+  </div>
75
+</template>
76
+
77
+<script setup>
78
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, toRaw } from 'vue';
79
+import { Dialog, showDialog, showSuccessToast, showToast, Toast, showConfirmDialog } from 'vant';
80
+import { useRouter } from 'vue-router';
81
+
82
+const { proxy } = getCurrentInstance();
83
+const router = useRouter();
84
+
85
+const onClickLeft = () => {
86
+  history.back();
87
+};
88
+
89
+const searchShow = ref(false);
90
+const query = ref({
91
+  year: '',
92
+  name: ''
93
+});
94
+
95
+const isRefreshing = ref(false);
96
+const isLoading = ref(false);
97
+const isFinished = ref(false);
98
+const currentPage = ref(1);
99
+const pageSize = ref(10);
100
+const totalRows = ref(0);
101
+const resultData = ref([]);
102
+const tableData = ref([]);
103
+const deleteDialogVisible = ref(false);
104
+const deleteId = ref('');
105
+
106
+const currentYear = ref(new Date().getFullYear());
107
+const years = Array.from({ length: 5 }, (_, i) => {
108
+  const year = new Date().getFullYear();
109
+  return year - 4 + i;
110
+});
111
+
112
+const yearOptions = years.map(year => ({
113
+  text: String(year),
114
+  value: String(year)
115
+}));
116
+
117
+const selectedYearText = ref(String(currentYear.value));
118
+const showYearPicker = ref(false);
119
+
120
+const onYearConfirm = ({ selectedOptions }) => {
121
+  query.value.year = selectedOptions[0].value;
122
+  selectedYearText.value = selectedOptions[0].text;
123
+  showYearPicker.value = false;
124
+  onRefresh();
125
+};
126
+
127
+const getTableData = async () => {
128
+  const url = '/sgsafe/Class/query';
129
+  const param = {
130
+    page: currentPage.value,
131
+    rows: pageSize.value,
132
+    params: JSON.stringify(query.value)
133
+  };
134
+  const response = await proxy.$axios.get(url, param);
135
+  if (response.data.code == '0') {
136
+    tableData.value = response.data.data.records;
137
+    totalRows.value = response.data.data.total;
138
+  } else {
139
+    showToast({
140
+      type: 'error',
141
+      message: '操作失败!' + response.data.msg
142
+    });
143
+  }
144
+};
145
+
146
+const onRefresh = () => {
147
+  basicReset();
148
+  onLoad();
149
+};
150
+
151
+const onLoad = async () => {
152
+  if (isRefreshing.value) {
153
+    resultData.value = [];
154
+    currentPage.value = 1;
155
+    isRefreshing.value = false;
156
+  }
157
+  try {
158
+    await getTableData();
159
+    if (pageSize.value * currentPage.value < totalRows.value) {
160
+      resultData.value = [...resultData.value, ...tableData.value];
161
+      openStatus.value = new Array(resultData.value.length).fill(true);
162
+      currentPage.value++;
163
+    } else {
164
+      resultData.value = [...resultData.value, ...tableData.value];
165
+      openStatus.value = new Array(resultData.value.length).fill(true);
166
+      isFinished.value = true;
167
+    }
168
+  } catch (error) {
169
+    console.log(error);
170
+    isFinished.value = true;
171
+  } finally {
172
+    isLoading.value = false;
173
+  }
174
+};
175
+
176
+const basicReset = () => {
177
+  isFinished.value = false;
178
+  isLoading.value = true;
179
+  currentPage.value = 1;
180
+  resultData.value = [];
181
+};
182
+
183
+const handdelect = () => {
184
+  query.value.name = '';
185
+  onRefresh();
186
+};
187
+
188
+const handAdd = () => {
189
+  router.push({
190
+    path: "/courseManagementList",
191
+    query: {
192
+      mark: -1
193
+    }
194
+  });
195
+};
196
+
197
+const openTrain = (row) => {
198
+  router.push({
199
+    path: "/courseManagementList",
200
+    query: {
201
+      mark: 1,
202
+      data: JSON.stringify(row)
203
+    }
204
+  });
205
+};
206
+
207
+const openClassName = (item) => {
208
+  router.push({
209
+    path: '/courseAddPeo',
210
+    query: {
211
+      data: JSON.stringify(item)
212
+    }
213
+  });
214
+};
215
+
216
+const jumpsection = (row) => {
217
+  router.push({
218
+    path: '/section',
219
+    query: {
220
+      data: JSON.stringify(row)
221
+    }
222
+  });
223
+};
224
+
225
+const queryInfo = (row) => {
226
+  router.push({
227
+    path: "/courseManagementList",
228
+    query: {
229
+      mark: 1,
230
+      data: JSON.stringify(row),
231
+      readOnly: 'true'
232
+    }
233
+  });
234
+};
235
+
236
+const handleDelete = (item) => {
237
+  deleteId.value = item.id;
238
+  deleteDialogVisible.value = true;
239
+};
240
+
241
+const confirmDelete = () => {
242
+  const qiyong = ref({});
243
+  qiyong.value = { ...resultData.value.find(item => item.id === deleteId.value) };
244
+  qiyong.value.cancelFlag = '1';
245
+  const url = '/sgsafe/Class/save';
246
+  const param = {
247
+    json: JSON.stringify(qiyong.value)
248
+  };
249
+  proxy.$axios.post(url, param).then(response => {
250
+    if (response.data.code == '0') {
251
+      showSuccessToast('删除成功');
252
+      deleteDialogVisible.value = false;
253
+      onRefresh();
254
+    } else {
255
+      showToast({
256
+        type: 'error',
257
+        message: '操作失败!' + response.data.msg
258
+      });
259
+    }
260
+  });
261
+};
262
+
263
+const openStatus = ref([]);
264
+const swipeCellRefs = ref([]);
265
+const getSwipeCellRef = (el, index) => {
266
+  if (el) {
267
+    swipeCellRefs.value[index] = el;
268
+  }
269
+};
270
+
271
+const openSwipe = (idx) => {
272
+  openStatus.value = new Array(resultData.value.length).fill(true);
273
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
274
+    openStatus.value[idx] = false;
275
+    swipeCellRefs.value[idx].open('right');
276
+  }
277
+  document.addEventListener('click', handleDocumentClick);
278
+};
279
+
280
+const handleDocumentClick = (event) => {
281
+  openStatus.value = new Array(resultData.value.length).fill(true);
282
+};
283
+
284
+const closeSwipe = (idx) => {
285
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
286
+    openStatus.value[idx] = true;
287
+    swipeCellRefs.value[idx].close();
288
+  }
289
+};
290
+
291
+onMounted(() => {
292
+  query.value.year = String(currentYear.value);
293
+  selectedYearText.value = String(currentYear.value);
294
+});
295
+</script>
296
+
297
+<style scoped>
298
+.h5-container {
299
+  width: 100%;
300
+  padding: 5px;
301
+  box-sizing: border-box;
302
+}
303
+
304
+.cell-title {
305
+  display: -webkit-box;
306
+  -webkit-box-orient: vertical;
307
+  -webkit-line-clamp: 2;
308
+  line-clamp: 2;
309
+  overflow: hidden;
310
+  text-overflow: ellipsis;
311
+  line-height: 1.5;
312
+  max-height: calc(1.5em * 2);
313
+  font-size: 16px;
314
+  font-weight: bold;
315
+  color: #333;
316
+}
317
+
318
+.swipe-cell-default {
319
+  display: flex;
320
+  background-color: #ffffff;
321
+  justify-content: center;
322
+  align-items: center;
323
+}
324
+
325
+.swipe-cell-default-icon {
326
+  width: 60px;
327
+  display: flex;
328
+  justify-content: center;
329
+}
330
+
331
+.delete-button {
332
+  height: 100%;
333
+  border: none;
334
+  color: #ff0000;
335
+  background-image: url('@/assets/img/del.png');
336
+  background-size: auto 100%;
337
+  background-repeat: no-repeat;
338
+}
339
+
340
+.edit-button {
341
+  height: 100%;
342
+  border: none;
343
+  color: #F9CC9D;
344
+  background-image: url('@/assets/img/edit.png');
345
+  background-size: auto 100%;
346
+  background-repeat: no-repeat;
347
+}
348
+
349
+.submit-button {
350
+  height: 100%;
351
+  border: none;
352
+  color: #07c160;
353
+  background-image: url('@/assets/img/sub.png');
354
+  background-size: auto 100%;
355
+  background-repeat: no-repeat;
356
+}
357
+</style>

+ 499
- 0
src/view/dati/courseManagement/courseManagementList.vue Целия файл

@@ -0,0 +1,499 @@
1
+<template>
2
+  <div class="page-container">
3
+    <van-sticky class="header">
4
+      <van-nav-bar
5
+        :title="title"
6
+        left-text="返回"
7
+        left-arrow
8
+        @click-left="onClickLeft">
9
+      </van-nav-bar>
10
+    </van-sticky>
11
+    <div class="scroll-container">
12
+      <van-form @submit="onSubmit">
13
+        <van-field
14
+          v-model="fromVue.projectName"
15
+          label="课程名称"
16
+          name="projectName"
17
+          required
18
+          placeholder="请输入课程名称"
19
+          :readonly="ifReadOnly"
20
+          :rules="[{required: true, message: '请输入课程名称'}]"
21
+        />
22
+
23
+        <van-field
24
+          v-model="selectedYearText"
25
+          readonly
26
+          is-link
27
+          label="培训年份"
28
+          placeholder="请选择年份"
29
+          :readonly="ifReadOnly"
30
+          @click="!ifReadOnly && (showYearPicker = true)"
31
+        />
32
+
33
+        <van-field
34
+          v-model="selectedTypeText"
35
+          readonly
36
+          is-link
37
+          label="培训类别"
38
+          placeholder="请选择培训类别"
39
+          :readonly="ifReadOnly"
40
+          @click="!ifReadOnly && (showTypePicker = true)"
41
+        />
42
+
43
+        <van-field
44
+          v-model="fromVue.trainLeader"
45
+          readonly
46
+          is-link
47
+          label="培训负责人"
48
+          placeholder="请选择培训负责人"
49
+          :readonly="ifReadOnly"
50
+          @click="!ifReadOnly && handleDepartmentLeaderNameOne()"
51
+        />
52
+
53
+        <van-field
54
+          v-model="selectedPlanYonText"
55
+          readonly
56
+          is-link
57
+          label="是否计划内"
58
+          placeholder="请选择"
59
+          :readonly="ifReadOnly"
60
+          @click="!ifReadOnly && (showPlanYonPicker = true)"
61
+        />
62
+
63
+        <van-field
64
+          v-if="planWhyShow"
65
+          v-model="fromVue.planWhy"
66
+          label="计划外原因"
67
+          name="planWhy"
68
+          placeholder="请输入计划外原因"
69
+          :readonly="ifReadOnly"
70
+        />
71
+
72
+        <van-field
73
+          v-model="fromVue.trainHours"
74
+          label="培训学时"
75
+          name="trainHours"
76
+          type="number"
77
+          placeholder="请输入培训学时"
78
+          :readonly="ifReadOnly"
79
+        />
80
+
81
+        <van-field
82
+          v-model="selectedPlanNameText"
83
+          readonly
84
+          is-link
85
+          label="计划名称"
86
+          placeholder="请选择计划"
87
+          :readonly="ifReadOnly"
88
+          @click="!ifReadOnly && (showPlanNamePicker = true)"
89
+        />
90
+
91
+        <van-field
92
+          v-model="fromVue.fromDate"
93
+          readonly
94
+          is-link
95
+          label="开始时间"
96
+          placeholder="请选择开始时间"
97
+          :readonly="ifReadOnly"
98
+          @click="!ifReadOnly && (showStartDatePicker = true)"
99
+        />
100
+
101
+        <van-field
102
+          v-model="fromVue.toDate"
103
+          readonly
104
+          is-link
105
+          label="结束时间"
106
+          placeholder="请选择结束时间"
107
+          :readonly="ifReadOnly"
108
+          @click="!ifReadOnly && (showEndDatePicker = true)"
109
+        />
110
+
111
+        <van-field
112
+          v-model="fromVue.trainContent"
113
+          label="培训内容"
114
+          name="trainContent"
115
+          type="textarea"
116
+          rows="3"
117
+          autosize
118
+          placeholder="请输入培训内容"
119
+          :readonly="ifReadOnly"
120
+        />
121
+
122
+        <van-field label="附件上传">
123
+          <template #input>
124
+            <AttachmentS3 :f-id="result" />
125
+          </template>
126
+        </van-field>
127
+
128
+        <div style="margin: 16px;" v-if="!ifReadOnly">
129
+          <van-button round block type="primary" native-type="submit">
130
+            {{ isEdit ? '保存' : '提交' }}
131
+          </van-button>
132
+        </div>
133
+      </van-form>
134
+
135
+      <!-- 年份选择器 -->
136
+      <van-popup v-model:show="showYearPicker" round position="bottom">
137
+        <van-picker
138
+          :columns="yearOptions"
139
+          @confirm="onYearConfirm"
140
+          @cancel="showYearPicker = false"
141
+        />
142
+      </van-popup>
143
+
144
+      <!-- 培训类别选择器 -->
145
+      <van-popup v-model:show="showTypePicker" round position="bottom">
146
+        <van-picker
147
+          :columns="typeOptions"
148
+          @confirm="onTypeConfirm"
149
+          @cancel="showTypePicker = false"
150
+        />
151
+      </van-popup>
152
+
153
+      <!-- 是否计划内选择器 -->
154
+      <van-popup v-model:show="showPlanYonPicker" round position="bottom">
155
+        <van-picker
156
+          :columns="planYonOptions"
157
+          @confirm="onPlanYonConfirm"
158
+          @cancel="showPlanYonPicker = false"
159
+        />
160
+      </van-popup>
161
+
162
+      <!-- 计划名称选择器 -->
163
+      <van-popup v-model:show="showPlanNamePicker" round position="bottom">
164
+        <van-picker
165
+          :columns="planNameOptions"
166
+          @confirm="onPlanNameConfirm"
167
+          @cancel="showPlanNamePicker = false"
168
+        />
169
+      </van-popup>
170
+
171
+      <!-- 开始日期选择器 -->
172
+      <van-popup v-model:show="showStartDatePicker" round position="bottom">
173
+        <van-date-picker
174
+          v-model="currentStartDate"
175
+          @confirm="onStartDateConfirm"
176
+          @cancel="showStartDatePicker = false"
177
+        />
178
+      </van-popup>
179
+
180
+      <!-- 结束日期选择器 -->
181
+      <van-popup v-model:show="showEndDatePicker" round position="bottom">
182
+        <van-date-picker
183
+          v-model="currentEndDate"
184
+          @confirm="onEndDateConfirm"
185
+          @cancel="showEndDatePicker = false"
186
+        />
187
+      </van-popup>
188
+    </div>
189
+  </div>
190
+
191
+  <!-- 人员选择组件 -->
192
+  <OrganizationalWithLeafUserForCourse ref="PopupDepartmentLeaderNameRef2"
193
+    @receiveFromChild="getDepartmentLeaderNameOne" />
194
+</template>
195
+
196
+<script setup>
197
+import { getCurrentInstance, onMounted, ref, computed } from 'vue';
198
+import { useRoute, useRouter } from 'vue-router';
199
+import tools from '@/tools';
200
+import OrganizationalWithLeafUserForCourse from '@/components/OrganizationalWithLeafUserForCourse.vue';
201
+import AttachmentS3 from '@/components/AttachmentS3.vue';
202
+import { showFailToast, showLoadingToast, showSuccessToast } from 'vant';
203
+
204
+const { proxy } = getCurrentInstance();
205
+const router = useRouter();
206
+const route = useRoute();
207
+
208
+let title = '新增课程';
209
+
210
+const onClickLeft = () => {
211
+  router.go(-1);
212
+};
213
+
214
+const currentYear = ref(new Date().getFullYear());
215
+const years = Array.from({ length: 5 }, (_, i) => {
216
+  const year = new Date().getFullYear();
217
+  return year - 4 + i;
218
+});
219
+
220
+const yearOptions = years.map(year => ({
221
+  text: String(year),
222
+  value: String(year)
223
+}));
224
+
225
+const jsonArray = localStorage.getItem('dept');
226
+const deptInformation = ref([]);
227
+try {
228
+  deptInformation.value = jsonArray ? JSON.parse(jsonArray) : [];
229
+} catch (error) {
230
+  deptInformation.value = [];
231
+}
232
+const deptName = deptInformation.value[0]?.deptName || '';
233
+const deptCode = deptInformation.value[0]?.deptCode || '';
234
+
235
+const generateCode = () => {
236
+  const now = new Date();
237
+  const year = now.getFullYear();
238
+  const month = String(now.getMonth() + 1).padStart(2, '0');
239
+  const day = String(now.getDate()).padStart(2, '0');
240
+  const formattedDate = `${year}${month}${day}`;
241
+  const hours = String(now.getHours()).padStart(2, '0');
242
+  const minutes = String(now.getMinutes()).padStart(2, '0');
243
+  const seconds = String(now.getSeconds()).padStart(2, '0');
244
+  const formattedTime = `${hours}${minutes}${seconds}`;
245
+  const sequenceNumber = Math.floor(Math.random() * 1000);
246
+  const paddedSequence = String(sequenceNumber).padStart(3, '0');
247
+  return `XJXX${formattedDate}${formattedTime}${paddedSequence}`;
248
+};
249
+
250
+const result = ref('');
251
+const fromVue = ref({});
252
+const isEdit = ref(route.query.mark === '1');
253
+const isReadOnly = ref(route.query.readOnly === 'true');
254
+const ifReadOnly = computed(() => isReadOnly.value && isEdit.value);
255
+
256
+let planInfo = {};
257
+if (route.query.mark) {
258
+  planInfo = JSON.parse(route.query.mark);
259
+}
260
+
261
+if (planInfo == -1) {
262
+  const fileId = generateCode();
263
+  result.value = fileId;
264
+  fromVue.value = {
265
+    id: '',
266
+    projectName: '',
267
+    projectNo: '',
268
+    trainYear: String(currentYear.value),
269
+    projectType: '',
270
+    trainLeader: localStorage.getItem('userDesc') || '',
271
+    planYon: '',
272
+    planName: '',
273
+    planWhy: '',
274
+    fromDate: '',
275
+    toDate: '',
276
+    trainContent: '',
277
+    trainHours: '',
278
+    fileId: fileId
279
+  };
280
+}
281
+
282
+if (planInfo == 1) {
283
+  title = '修改课程';
284
+  fromVue.value = JSON.parse(route.query.data);
285
+  if (fromVue.value.fileId) {
286
+    result.value = fromVue.value.fileId;
287
+  } else {
288
+    result.value = generateCode();
289
+    fromVue.value.fileId = result.value;
290
+  }
291
+}
292
+
293
+const dicList = ref([]);
294
+const getDicList = () => {
295
+  tools.dic.getDicList(['SGSAFE_CLASS']).then((response => {
296
+    dicList.value = response.data.data.SGSAFE_CLASS || [];
297
+  }));
298
+};
299
+
300
+const PlantableData = ref([]);
301
+const getDeptPlanData = () => {
302
+  const url = '/sgsafe/Plan/query';
303
+  const param = {
304
+    page: 1,
305
+    rows: 100,
306
+    params: JSON.stringify({ deptCode: deptCode })
307
+  };
308
+  proxy.$axios.get(url, param).then(response => {
309
+    if (response.data.code == '0') {
310
+      PlantableData.value = response.data.data.records.map(item => item.planName);
311
+    }
312
+  });
313
+};
314
+
315
+const selectedYearText = ref('');
316
+const selectedTypeText = ref('');
317
+const selectedPlanYonText = ref('');
318
+const selectedPlanNameText = ref('');
319
+const planWhyShow = ref(false);
320
+
321
+const showYearPicker = ref(false);
322
+const showTypePicker = ref(false);
323
+const showPlanYonPicker = ref(false);
324
+const showPlanNamePicker = ref(false);
325
+const showStartDatePicker = ref(false);
326
+const showEndDatePicker = ref(false);
327
+
328
+const currentStartDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
329
+const currentEndDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
330
+
331
+const typeOptions = computed(() => {
332
+  return dicList.value.map(item => ({
333
+    text: item.dicName,
334
+    value: item.dicCode
335
+  }));
336
+});
337
+
338
+const planYonOptions = [
339
+  { text: '是', value: '是' },
340
+  { text: '否', value: '否' }
341
+];
342
+
343
+const planNameOptions = computed(() => {
344
+  return PlantableData.value.map(name => ({
345
+    text: name,
346
+    value: name
347
+  }));
348
+});
349
+
350
+const onYearConfirm = ({ selectedOptions }) => {
351
+  fromVue.value.trainYear = selectedOptions[0].value;
352
+  selectedYearText.value = selectedOptions[0].text;
353
+  showYearPicker.value = false;
354
+};
355
+
356
+const onTypeConfirm = ({ selectedOptions }) => {
357
+  fromVue.value.projectType = selectedOptions[0].value;
358
+  selectedTypeText.value = selectedOptions[0].text;
359
+  showTypePicker.value = false;
360
+};
361
+
362
+const onPlanYonConfirm = ({ selectedOptions }) => {
363
+  fromVue.value.planYon = selectedOptions[0].value;
364
+  selectedPlanYonText.value = selectedOptions[0].text;
365
+  if (selectedOptions[0].value === '是') {
366
+    planWhyShow.value = false;
367
+  } else {
368
+    planWhyShow.value = true;
369
+  }
370
+  showPlanYonPicker.value = false;
371
+};
372
+
373
+const onPlanNameConfirm = ({ selectedOptions }) => {
374
+  fromVue.value.planName = selectedOptions[0].value;
375
+  selectedPlanNameText.value = selectedOptions[0].text;
376
+  showPlanNamePicker.value = false;
377
+};
378
+
379
+const onStartDateConfirm = ({ selectedValues }) => {
380
+  fromVue.value.fromDate = `${selectedValues[0]}-${String(selectedValues[1]).padStart(2, '0')}-${String(selectedValues[2]).padStart(2, '0')}`;
381
+  showStartDatePicker.value = false;
382
+};
383
+
384
+const onEndDateConfirm = ({ selectedValues }) => {
385
+  fromVue.value.toDate = `${selectedValues[0]}-${String(selectedValues[1]).padStart(2, '0')}-${String(selectedValues[2]).padStart(2, '0')}`;
386
+  showEndDatePicker.value = false;
387
+};
388
+
389
+const PopupDepartmentLeaderNameRef2 = ref();
390
+const handleDepartmentLeaderNameOne = () => {
391
+  if (PopupDepartmentLeaderNameRef2.value) {
392
+    PopupDepartmentLeaderNameRef2.value.open();
393
+  }
394
+};
395
+
396
+const getDepartmentLeaderNameOne = (item) => {
397
+  fromVue.value.trainLeader = item.user.userDesc;
398
+};
399
+
400
+const addEmergencyDrillPlan = async () => {
401
+  const loadingToast = showLoadingToast({
402
+    duration: 0,
403
+    message: '加载中',
404
+    forbidClick: true
405
+  });
406
+
407
+  fromVue.value.addDeptCode = deptCode;
408
+  if (!fromVue.value.fileId) {
409
+    fromVue.value.fileId = result.value;
410
+  }
411
+
412
+  const url = '/sgsafe/Class/save';
413
+  const params = {
414
+    json: JSON.stringify(fromVue.value)
415
+  };
416
+
417
+  try {
418
+    const res = await proxy.$axios.post(url, params);
419
+    if (res.data.code === 0 || res.data.code == '0') {
420
+      loadingToast.close();
421
+      showSuccessToast('保存成功');
422
+      onClickLeft();
423
+    } else {
424
+      loadingToast.close();
425
+      showFailToast('操作失败!' + (res.data.msg || '未知错误'));
426
+    }
427
+  } catch (error) {
428
+    loadingToast.close();
429
+    console.error('保存失败:', error);
430
+    showFailToast('保存失败:' + (error.message || '网络错误,请稍后重试'));
431
+  }
432
+};
433
+
434
+const onSubmit = (values) => {
435
+  addEmergencyDrillPlan();
436
+};
437
+
438
+onMounted(() => {
439
+  getDicList();
440
+  getDeptPlanData();
441
+
442
+  if (fromVue.value.trainYear) {
443
+    selectedYearText.value = fromVue.value.trainYear;
444
+  } else {
445
+    selectedYearText.value = String(currentYear.value);
446
+    fromVue.value.trainYear = String(currentYear.value);
447
+  }
448
+
449
+  if (fromVue.value.projectType) {
450
+    const typeItem = dicList.value.find(item => item.dicCode === fromVue.value.projectType);
451
+    if (typeItem) {
452
+      selectedTypeText.value = typeItem.dicName;
453
+    }
454
+  }
455
+
456
+  if (fromVue.value.planYon) {
457
+    selectedPlanYonText.value = fromVue.value.planYon;
458
+    planWhyShow.value = fromVue.value.planYon === '否';
459
+  }
460
+
461
+  if (fromVue.value.planName) {
462
+    selectedPlanNameText.value = fromVue.value.planName;
463
+  }
464
+
465
+  if (fromVue.value.fromDate) {
466
+    const dateParts = fromVue.value.fromDate.split('-');
467
+    currentStartDate.value = [parseInt(dateParts[0]), parseInt(dateParts[1]), parseInt(dateParts[2])];
468
+  }
469
+
470
+  if (fromVue.value.toDate) {
471
+    const dateParts = fromVue.value.toDate.split('-');
472
+    currentEndDate.value = [parseInt(dateParts[0]), parseInt(dateParts[1]), parseInt(dateParts[2])];
473
+  }
474
+});
475
+</script>
476
+
477
+<style scoped>
478
+.page-container {
479
+  height: 100vh;
480
+  display: flex;
481
+  flex-direction: column;
482
+}
483
+
484
+.scroll-container {
485
+  flex: 1;
486
+  overflow: auto;
487
+  -webkit-overflow-scrolling: touch;
488
+}
489
+
490
+.scroll-container::-webkit-scrollbar {
491
+  display: none;
492
+}
493
+
494
+.header {
495
+  flex-shrink: 0;
496
+  background: #f5f5f5;
497
+  padding: 12px;
498
+}
499
+</style>

+ 707
- 0
src/view/dati/courseManagement/section.vue Целия файл

@@ -0,0 +1,707 @@
1
+<template>
2
+  <div class="h5-container">
3
+    <van-nav-bar title="小节管理" @click-left="onClickLeft">
4
+      <template #right>
5
+        <van-dropdown-menu>
6
+          <van-dropdown-item v-model="addType" :options="addTypeOptions" @change="handleAddType" />
7
+        </van-dropdown-menu>
8
+      </template>
9
+    </van-nav-bar>
10
+
11
+    <!-- 项目列表 -->
12
+    <van-pull-refresh v-model="isRefreshing" success-text="刷新成功" @refresh="onRefresh">
13
+      <van-list v-model:loading="isLoading" :finished="isFinished" finished-text="没有更多了" offset="200" @load="onLoad">
14
+        <div v-for="(item, idx) in resultData" :key="item.id">
15
+          <van-swipe-cell title-style="color: #007aff" style="height: 80px;" :ref="el => getSwipeCellRef(el, idx)">
16
+            <template #default>
17
+              <div class="swipe-cell-default">
18
+                <van-cell style="height: 100%; display: flex; align-items: flex-start;" @click="openTrain(item)">
19
+                  <template #title>
20
+                    <div class="cell-title">
21
+                      {{ item.sectionName }}
22
+                    </div>
23
+                  </template>
24
+                  <template #label>
25
+                    <div>小节类型:{{ item.studyorExam }}</div>
26
+                    <div v-if="item.studyorExam === 'study'">观看时长:{{ item.watchTime }} 分钟</div>
27
+                    <div v-if="item.studyorExam === 'exam'">关联题库:{{ item.questionName }}</div>
28
+                  </template>
29
+                </van-cell>
30
+                <div class="swipe-cell-default-icon">
31
+                  <van-icon v-if="openStatus[idx]" name="arrow-double-left" @click.stop="openSwipe(idx)" />
32
+                  <van-icon v-else name="arrow-double-right" @click.stop="closeSwipe(idx)" />
33
+                </div>
34
+              </div>
35
+            </template>
36
+
37
+            <template #right>
38
+              <van-button square class="edit-button" text="编辑" @click="openTrain(item)" />
39
+              <van-button square class="delete-button" text="删除" @click="deleteRow(item)" />
40
+            </template>
41
+          </van-swipe-cell>
42
+        </div>
43
+      </van-list>
44
+    </van-pull-refresh>
45
+
46
+    <!-- 考试弹窗 -->
47
+    <van-popup v-model:show="examDialogVisible" round position="bottom" :style="{ height: '90%' }">
48
+      <div class="popup-container">
49
+        <van-nav-bar title="新增考试" left-arrow @click-left="examDialogVisible = false" />
50
+        <div class="popup-content">
51
+          <van-form @submit="submitStudy">
52
+            <van-field
53
+              v-model="form.sectionName"
54
+              label="小节名称"
55
+              name="sectionName"
56
+              required
57
+              placeholder="请输入小节名称"
58
+              :rules="[{required: true, message: '请输入小节名称'}]"
59
+            />
60
+
61
+            <van-field
62
+              v-model="form.sectionOrder"
63
+              label="顺序"
64
+              name="sectionOrder"
65
+              type="number"
66
+              required
67
+              placeholder="请输入顺序"
68
+              :rules="[{required: true, message: '请输入顺序'}]"
69
+            />
70
+
71
+            <van-field
72
+              v-model="selectedQuestionText"
73
+              readonly
74
+              is-link
75
+              label="关联题库"
76
+              placeholder="请选择题库"
77
+              @click="showQuestionPicker = true"
78
+            />
79
+
80
+            <van-field
81
+              v-model="totalExamScoreText"
82
+              label="试卷总分"
83
+              readonly
84
+            />
85
+
86
+            <van-field
87
+              v-model="form.singleChoice"
88
+              label="单选数量"
89
+              name="singleChoice"
90
+              type="number"
91
+              placeholder="请输入单选数量"
92
+            />
93
+
94
+            <van-field
95
+              v-model="form.singleChoiceScore"
96
+              label="每题分数"
97
+              name="singleChoiceScore"
98
+              type="number"
99
+              placeholder="请输入每题分数"
100
+            />
101
+
102
+            <van-field
103
+              v-model="totalScoreText"
104
+              label="总分"
105
+              readonly
106
+            />
107
+
108
+            <van-field
109
+              v-model="form.multipleChoice"
110
+              label="多选数量"
111
+              name="multipleChoice"
112
+              type="number"
113
+              placeholder="请输入多选数量"
114
+            />
115
+
116
+            <van-field
117
+              v-model="form.multipleChoiceScore"
118
+              label="每题分数"
119
+              name="multipleChoiceScore"
120
+              type="number"
121
+              placeholder="请输入每题分数"
122
+            />
123
+
124
+            <van-field
125
+              v-model="totalChoiceText"
126
+              label="总分"
127
+              readonly
128
+            />
129
+
130
+            <van-field
131
+              v-model="form.judge"
132
+              label="判断数量"
133
+              name="judge"
134
+              type="number"
135
+              placeholder="请输入判断数量"
136
+            />
137
+
138
+            <van-field
139
+              v-model="form.judgeScore"
140
+              label="每题分数"
141
+              name="judgeScore"
142
+              type="number"
143
+              placeholder="请输入每题分数"
144
+            />
145
+
146
+            <van-field
147
+              v-model="totaljudgeText"
148
+              label="总分"
149
+              readonly
150
+            />
151
+
152
+            <van-field
153
+              v-model="form.qualifiedScore"
154
+              label="及格分数"
155
+              name="qualifiedScore"
156
+              type="number"
157
+              placeholder="请输入及格分数"
158
+            />
159
+
160
+            <div style="margin: 16px;">
161
+              <van-button round block type="primary" native-type="submit">
162
+                确定
163
+              </van-button>
164
+            </div>
165
+          </van-form>
166
+
167
+          <van-popup v-model:show="showQuestionPicker" round position="bottom">
168
+            <van-picker
169
+              :columns="questionOptions"
170
+              @confirm="onQuestionConfirm"
171
+              @cancel="showQuestionPicker = false"
172
+            />
173
+          </van-popup>
174
+        </div>
175
+      </div>
176
+    </van-popup>
177
+
178
+    <!-- 学习弹窗 -->
179
+    <van-popup v-model:show="studyDialogVisible" round position="bottom" :style="{ height: '90%' }">
180
+      <div class="popup-container">
181
+        <van-nav-bar title="新增学习" left-arrow @click-left="studyDialogVisible = false" />
182
+        <div class="popup-content">
183
+          <van-form @submit="submitStudy">
184
+            <van-field
185
+              v-model="form.sectionName"
186
+              label="小节名称"
187
+              name="sectionName"
188
+              required
189
+              placeholder="请输入小节名称"
190
+              :rules="[{required: true, message: '请输入小节名称'}]"
191
+            />
192
+
193
+            <van-field
194
+              v-model="form.sectionOrder"
195
+              label="顺序"
196
+              name="sectionOrder"
197
+              type="number"
198
+              required
199
+              placeholder="请输入顺序"
200
+              :rules="[{required: true, message: '请输入顺序'}]"
201
+            />
202
+
203
+            <van-field
204
+              v-model="form.watchTime"
205
+              label="观看时长/分"
206
+              name="watchTime"
207
+              type="number"
208
+              required
209
+              placeholder="请输入观看时长"
210
+              :rules="[{required: true, message: '请输入观看时长'}]"
211
+            />
212
+
213
+            <van-field label="附件上传">
214
+              <template #input>
215
+                <AttachmentS3Required ref="attachmentRef" :f-id="fId" />
216
+              </template>
217
+            </van-field>
218
+
219
+            <div style="margin: 16px;">
220
+              <van-button round block type="primary" native-type="submit">
221
+                确定
222
+              </van-button>
223
+            </div>
224
+          </van-form>
225
+        </div>
226
+      </div>
227
+    </van-popup>
228
+  </div>
229
+</template>
230
+
231
+<script setup>
232
+import { ref, computed, onMounted, getCurrentInstance } from 'vue';
233
+import { showFailToast, showSuccessToast, showToast, showConfirmDialog } from 'vant';
234
+import { useRoute, useRouter } from 'vue-router';
235
+import AttachmentS3Required from '@/components/AttachmentS3Required.vue';
236
+
237
+const { proxy } = getCurrentInstance();
238
+const router = useRouter();
239
+const route = useRoute();
240
+
241
+const onClickLeft = () => {
242
+  router.back();
243
+};
244
+
245
+const rowData = ref({});
246
+const ClassId = ref('');
247
+
248
+onMounted(async () => {
249
+  if (route.query.data) {
250
+    const rowData1 = JSON.parse(route.query.data);
251
+    rowData.value = rowData1;
252
+    ClassId.value = rowData.value.id;
253
+    queryFetch();
254
+  } else {
255
+    showToast({
256
+      type: 'error',
257
+      message: '未接收到数据参数'
258
+    });
259
+  }
260
+  getQuestionId();
261
+});
262
+
263
+const queryFetch = () => {
264
+  basicReset();
265
+  onLoad();
266
+};
267
+
268
+const currentPage = ref(1);
269
+const totalRows = ref(0);
270
+const pageSize = ref(10);
271
+const tableData = ref([]);
272
+const isRefreshing = ref(false);
273
+const isLoading = ref(false);
274
+const isFinished = ref(false);
275
+const resultData = ref([]);
276
+
277
+const form = ref({
278
+  sectionName: '',
279
+  sectionOrder: '',
280
+  studyorExam: '',
281
+  classId: '',
282
+  watchTime: '',
283
+  fileId: '',
284
+  sectionId: '',
285
+  questionName: '',
286
+  questionId: '',
287
+  singleChoice: '',
288
+  multipleChoice: '',
289
+  judge: '',
290
+  singleChoiceScore: '',
291
+  multipleChoiceScore: '',
292
+  judgeScore: '',
293
+  qualifiedScore: ''
294
+});
295
+
296
+const getTableData = async () => {
297
+  if (!ClassId.value) {
298
+    return;
299
+  }
300
+
301
+  const url = '/sgsafe/Class/querySection';
302
+  const param = {
303
+    page: currentPage.value,
304
+    rows: pageSize.value,
305
+    params: ClassId.value
306
+  };
307
+
308
+  try {
309
+    const response = await proxy.$axios.get(url, param);
310
+    if (response.data.code == '0') {
311
+      tableData.value = response.data.data.records || [];
312
+      totalRows.value = response.data.data.total || 0;
313
+    } else {
314
+      showToast({
315
+        type: 'error',
316
+        message: '操作失败!' + response.data.msg
317
+      });
318
+    }
319
+  } catch (error) {
320
+    console.error('获取数据失败:', error);
321
+    showToast({
322
+      type: 'error',
323
+      message: '获取数据失败'
324
+    });
325
+  }
326
+};
327
+
328
+const onRefresh = () => {
329
+  basicReset();
330
+  onLoad();
331
+};
332
+
333
+const onLoad = async () => {
334
+  if (isRefreshing.value) {
335
+    resultData.value = [];
336
+    currentPage.value = 1;
337
+    isRefreshing.value = false;
338
+  }
339
+
340
+  try {
341
+    await getTableData();
342
+    if (pageSize.value * currentPage.value < totalRows.value) {
343
+      resultData.value = [...resultData.value, ...tableData.value];
344
+      openStatus.value = new Array(resultData.value.length).fill(true);
345
+      currentPage.value++;
346
+    } else {
347
+      resultData.value = [...resultData.value, ...tableData.value];
348
+      openStatus.value = new Array(resultData.value.length).fill(true);
349
+      isFinished.value = true;
350
+    }
351
+  } catch (error) {
352
+    console.log(error);
353
+    isFinished.value = true;
354
+  } finally {
355
+    isLoading.value = false;
356
+  }
357
+};
358
+
359
+const basicReset = () => {
360
+  isFinished.value = false;
361
+  isLoading.value = true;
362
+  currentPage.value = 1;
363
+  resultData.value = [];
364
+};
365
+
366
+const addType = ref('');
367
+const addTypeOptions = [
368
+  { text: '新增', value: '' },
369
+  { text: '考试', value: 'exam' },
370
+  { text: '学习', value: 'study' }
371
+];
372
+
373
+const examDialogVisible = ref(false);
374
+const studyDialogVisible = ref(false);
375
+const fId = ref('');
376
+
377
+const generateCode = () => {
378
+  const now = new Date();
379
+  const year = now.getFullYear();
380
+  const month = String(now.getMonth() + 1).padStart(2, '0');
381
+  const day = String(now.getDate()).padStart(2, '0');
382
+  const formattedDate = `${year}${month}${day}`;
383
+  const hours = String(now.getHours()).padStart(2, '0');
384
+  const minutes = String(now.getMinutes()).padStart(2, '0');
385
+  const seconds = String(now.getSeconds()).padStart(2, '0');
386
+  const formattedTime = `${hours}${minutes}${seconds}`;
387
+  const sequenceNumber = Math.floor(Math.random() * 1000);
388
+  const paddedSequence = String(sequenceNumber).padStart(3, '0');
389
+  return `XJXX${formattedDate}${formattedTime}${paddedSequence}`;
390
+};
391
+
392
+const generatedCode = ref(generateCode());
393
+
394
+const regenerateCode = () => {
395
+  generatedCode.value = generateCode();
396
+};
397
+
398
+const handleAddType = (value) => {
399
+  if (!value) {
400
+    return;
401
+  }
402
+
403
+  const cId = ClassId.value;
404
+
405
+  if (value === 'exam') {
406
+    form.value = {
407
+      sectionName: '',
408
+      sectionOrder: '',
409
+      studyorExam: 'exam',
410
+      classId: cId,
411
+      questionName: '',
412
+      questionId: '',
413
+      singleChoice: '',
414
+      multipleChoice: '',
415
+      judge: '',
416
+      singleChoiceScore: '',
417
+      multipleChoiceScore: '',
418
+      judgeScore: '',
419
+      qualifiedScore: ''
420
+    };
421
+    examDialogVisible.value = true;
422
+  } else if (value === 'study') {
423
+    regenerateCode();
424
+    form.value = {
425
+      sectionName: '',
426
+      sectionOrder: '',
427
+      studyorExam: 'study',
428
+      classId: cId,
429
+      watchTime: '',
430
+      fileId: generatedCode.value
431
+    };
432
+    fId.value = generatedCode.value;
433
+    studyDialogVisible.value = true;
434
+  }
435
+  addType.value = '';
436
+};
437
+
438
+const questionIds = ref([]);
439
+const getQuestionId = () => {
440
+  const url = '/sgsafe/Rule/getQuestionId';
441
+  const param = {};
442
+  proxy.$axios.get(url, param).then(response => {
443
+    if (response.data.code == '0') {
444
+      questionIds.value = response.data.data || [];
445
+    } else {
446
+      showToast({
447
+        type: 'error',
448
+        message: '操作失败!' + response.data.msg
449
+      });
450
+    }
451
+  });
452
+};
453
+
454
+const selectedQuestionText = ref('');
455
+const showQuestionPicker = ref(false);
456
+
457
+const questionOptions = computed(() => {
458
+  return questionIds.value.map(item => ({
459
+    text: item.questionName,
460
+    value: item.id
461
+  }));
462
+});
463
+
464
+const onQuestionConfirm = ({ selectedOptions }) => {
465
+  form.value.questionId = selectedOptions[0].value;
466
+  const targetItem = questionIds.value.find(item => item.id === selectedOptions[0].value);
467
+  if (targetItem) {
468
+    form.value.questionName = targetItem.questionName;
469
+    selectedQuestionText.value = targetItem.questionName;
470
+  }
471
+  showQuestionPicker.value = false;
472
+};
473
+
474
+const totalScore = computed(() => {
475
+  const num = parseInt(form.value.singleChoice) || 0;
476
+  const score = parseFloat(form.value.singleChoiceScore) || 0;
477
+  if (num > 0 && score >= 0) {
478
+    return (num * score).toFixed(0);
479
+  }
480
+  return '0';
481
+});
482
+
483
+const totalScoreText = computed(() => totalScore.value);
484
+
485
+const totalChoice = computed(() => {
486
+  const num = parseInt(form.value.multipleChoice) || 0;
487
+  const score = parseFloat(form.value.multipleChoiceScore) || 0;
488
+  if (num > 0 && score >= 0) {
489
+    return (num * score).toFixed(0);
490
+  }
491
+  return '0';
492
+});
493
+
494
+const totalChoiceText = computed(() => totalChoice.value);
495
+
496
+const totaljudge = computed(() => {
497
+  const num = parseInt(form.value.judge) || 0;
498
+  const score = parseFloat(form.value.judgeScore) || 0;
499
+  if (num > 0 && score >= 0) {
500
+    return (num * score).toFixed(0);
501
+  }
502
+  return '0';
503
+});
504
+
505
+const totaljudgeText = computed(() => totaljudge.value);
506
+
507
+const totalExamScore = computed(() => {
508
+  const s1 = parseFloat(totalScore.value) || 0;
509
+  const s2 = parseFloat(totalChoice.value) || 0;
510
+  const s3 = parseFloat(totaljudge.value) || 0;
511
+  const total = (s1 + s2 + s3).toFixed(0);
512
+  return isNaN(total) ? '0' : total;
513
+});
514
+
515
+const totalExamScoreText = computed(() => totalExamScore.value);
516
+
517
+const attachmentRef = ref(null);
518
+
519
+const submitStudy = async () => {
520
+  if (form.value.studyorExam == 'study') {
521
+    // 验证必须上传至少一个附件
522
+    if (!attachmentRef.value || !attachmentRef.value.validate()) {
523
+      showFailToast('请至少上传一个附件');
524
+      return;
525
+    }
526
+    form.value.fileId = fId.value;
527
+  }
528
+
529
+  const param = {
530
+    json: JSON.stringify(form.value)
531
+  };
532
+
533
+  try {
534
+    const response = await proxy.$axios.post('/sgsafe/Class/saveStudySection', param);
535
+    if (response.data.code == '0') {
536
+      showSuccessToast('保存成功');
537
+      // 关闭弹窗
538
+      studyDialogVisible.value = false;
539
+      examDialogVisible.value = false;
540
+      // 重置表单
541
+      form.value = {
542
+        sectionName: '',
543
+        sectionOrder: '',
544
+        studyorExam: '',
545
+        classId: '',
546
+        watchTime: '',
547
+        fileId: '',
548
+        sectionId: '',
549
+        questionName: '',
550
+        questionId: '',
551
+        singleChoice: '',
552
+        multipleChoice: '',
553
+        judge: '',
554
+        singleChoiceScore: '',
555
+        multipleChoiceScore: '',
556
+        judgeScore: '',
557
+        qualifiedScore: ''
558
+      };
559
+      fId.value = '';
560
+      selectedQuestionText.value = '';
561
+      // 刷新列表
562
+      queryFetch();
563
+    } else {
564
+      showFailToast('保存失败:' + response.data.msg);
565
+    }
566
+  } catch (error) {
567
+    console.error('保存失败:', error);
568
+    showFailToast('保存失败:' + (error.message || '网络错误'));
569
+  }
570
+};
571
+
572
+const openTrain = (row) => {
573
+  if (row.studyorExam == 'study') {
574
+    form.value = { ...row };
575
+    fId.value = row.fileId || '';
576
+    studyDialogVisible.value = true;
577
+  } else {
578
+    form.value = { ...row };
579
+    if (row.questionId) {
580
+      const targetItem = questionIds.value.find(item => item.id === row.questionId);
581
+      if (targetItem) {
582
+        selectedQuestionText.value = targetItem.questionName;
583
+      }
584
+    }
585
+    examDialogVisible.value = true;
586
+  }
587
+};
588
+
589
+const deleteRow = (row) => {
590
+  showConfirmDialog({
591
+    title: '提示',
592
+    message: '确定要删除吗?'
593
+  }).then(() => {
594
+    const qiyong = ref({});
595
+    qiyong.value = { ...row };
596
+    qiyong.value.cancelFlag = '1';
597
+    const url = '/sgsafe/Class/saveStudySection';
598
+    const param = {
599
+      json: JSON.stringify(qiyong.value)
600
+    };
601
+    proxy.$axios.post(url, param).then(response => {
602
+      if (response.data.code == '0') {
603
+        showSuccessToast('删除成功');
604
+        queryFetch();
605
+      } else {
606
+        showToast({
607
+          type: 'error',
608
+          message: '操作失败!' + response.data.msg
609
+        });
610
+      }
611
+    });
612
+  }).catch(() => {
613
+  });
614
+};
615
+
616
+const openStatus = ref([]);
617
+const swipeCellRefs = ref([]);
618
+const getSwipeCellRef = (el, index) => {
619
+  if (el) {
620
+    swipeCellRefs.value[index] = el;
621
+  }
622
+};
623
+
624
+const openSwipe = (idx) => {
625
+  openStatus.value = new Array(resultData.value.length).fill(true);
626
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
627
+    openStatus.value[idx] = false;
628
+    swipeCellRefs.value[idx].open('right');
629
+  }
630
+  document.addEventListener('click', handleDocumentClick);
631
+};
632
+
633
+const handleDocumentClick = (event) => {
634
+  openStatus.value = new Array(resultData.value.length).fill(true);
635
+};
636
+
637
+const closeSwipe = (idx) => {
638
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
639
+    openStatus.value[idx] = true;
640
+    swipeCellRefs.value[idx].close();
641
+  }
642
+};
643
+</script>
644
+
645
+<style scoped>
646
+.h5-container {
647
+  width: 100%;
648
+  padding: 5px;
649
+  box-sizing: border-box;
650
+}
651
+
652
+.cell-title {
653
+  display: -webkit-box;
654
+  -webkit-box-orient: vertical;
655
+  -webkit-line-clamp: 2;
656
+  overflow: hidden;
657
+  text-overflow: ellipsis;
658
+  line-height: 1.5;
659
+  max-height: calc(1.5em * 2);
660
+  font-size: 16px;
661
+  font-weight: bold;
662
+  color: #333;
663
+}
664
+
665
+.swipe-cell-default {
666
+  display: flex;
667
+  background-color: #ffffff;
668
+  justify-content: center;
669
+  align-items: center;
670
+}
671
+
672
+.swipe-cell-default-icon {
673
+  width: 60px;
674
+  display: flex;
675
+  justify-content: center;
676
+}
677
+
678
+.delete-button {
679
+  height: 100%;
680
+  border: none;
681
+  color: #ff0000;
682
+  background-image: url('@/assets/img/del.png');
683
+  background-size: auto 100%;
684
+  background-repeat: no-repeat;
685
+}
686
+
687
+.edit-button {
688
+  height: 100%;
689
+  border: none;
690
+  color: #F9CC9D;
691
+  background-image: url('@/assets/img/edit.png');
692
+  background-size: auto 100%;
693
+  background-repeat: no-repeat;
694
+}
695
+
696
+.popup-container {
697
+  height: 100%;
698
+  display: flex;
699
+  flex-direction: column;
700
+}
701
+
702
+.popup-content {
703
+  flex: 1;
704
+  overflow: auto;
705
+  -webkit-overflow-scrolling: touch;
706
+}
707
+</style>

+ 0
- 6
src/view/knowledge/accident.vue Целия файл

@@ -254,9 +254,6 @@ const onLoad = async () => {
254 254
   getDicList()
255 255
 
256 256
 
257
-
258
-
259
-
260 257
   try {
261 258
     await getTableData();
262 259
 
@@ -443,9 +440,6 @@ const confirmDelete = () => {
443 440
   });
444 441
 };
445 442
 
446
-
447
-
448
-
449 443
 const resetForm = () => {
450 444
   form.value = {
451 445
     projectName: '',

+ 117
- 113
src/view/knowledge/accidentList.vue Целия файл

@@ -1,14 +1,15 @@
1 1
 <script setup>
2
-import { getCurrentInstance, onMounted, ref , computed } from 'vue';
3
-import { useRoute, useRouter } from 'vue-router';
2
+import {getCurrentInstance, onMounted, ref, computed} from 'vue';
3
+import {useRoute, useRouter} from 'vue-router';
4 4
 import tools from '@/tools'
5 5
 import OrganizationalWithLeafUserOne from '@/components/OrganizationalWithLeafUserOne.vue';
6
+
6 7
 const {
7 8
   proxy
8 9
 } = getCurrentInstance()
9 10
 const accidentDictList = ref([])
10
-const columns =  ref([])
11
-const columnsLevel =  ref([])
11
+const columns = ref([])
12
+const columnsLevel = ref([])
12 13
 
13 14
 //     [
14 15
 //   { text: '杭州', value: 'Hangzhou' },
@@ -44,12 +45,11 @@ const regenerateCode = () => {
44 45
 };
45 46
 
46 47
 
47
-
48 48
 const getAccidentDicList = () => {
49
-  tools.dic.getDicList([ 'case_type','SEX', 'case_source','accident_level','accident_type','sgsafe_taccidentTags']).then((response => {
49
+  tools.dic.getDicList(['case_type', 'SEX', 'case_source', 'accident_level', 'accident_type', 'sgsafe_taccidentTags']).then((response => {
50 50
     console.log(JSON.stringify(response.data.data))
51 51
     accidentDictList.value = response.data.data
52
-    console.log('case_source',accidentDictList.value.case_source)
52
+    console.log('case_source', accidentDictList.value.case_source)
53 53
     columns.value = accidentDictList.value.case_source.map(item => ({
54 54
       text: item.dicName,
55 55
       value: item.dicCode
@@ -60,7 +60,7 @@ const getAccidentDicList = () => {
60 60
     }));
61 61
 
62 62
 
63
-    console.log('accident_level',accidentDictList.value.accident_level)
63
+    console.log('accident_level', accidentDictList.value.accident_level)
64 64
 
65 65
   }))
66 66
 }
@@ -90,38 +90,39 @@ try {
90 90
 const deptName = deptInformation.value[0].deptName
91 91
 const deptCode = deptInformation.value[0].deptCode
92 92
 
93
-const guid = () =>  {
93
+const guid = () => {
94 94
   function S4() {
95
-    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
95
+    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
96 96
   }
97
-  return (S4()+S4()+S4()+S4()+S4()+S4()+S4()+S4())
97
+
98
+  return (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4())
98 99
 }
99 100
 const formJieguo = ref({
100
-  resultWhether:'',
101
-  resultDetail:'',
102
-  resultWhy:'',
103
-  resultFile:'',
104
-  itemsId:'',
105
-  resultFileId:'',
106
-  resType:''
101
+  resultWhether: '',
102
+  resultDetail: '',
103
+  resultWhy: '',
104
+  resultFile: '',
105
+  itemsId: '',
106
+  resultFileId: '',
107
+  resType: ''
107 108
 })
108 109
 const route = useRoute()
109 110
 let planInfo = {}
110
-const  userName1=localStorage.getItem('userName');
111
+const userName1 = localStorage.getItem('userName');
111 112
 const isEdit = ref(route.query.mark === '1');
112 113
 const isReadOnly = ref(route.query.readOnly === 'true');
113 114
 const isCaseSubmitted = computed(() => isReadOnly.value && isEdit.value);
114
-const result=ref('')
115
-const fromVue=ref({})
115
+const result = ref('')
116
+const fromVue = ref({})
116 117
 if (route.query.mark) {
117 118
   planInfo = JSON.parse(route.query.mark)
118 119
 }
119 120
 console.log(planInfo);
120 121
 // 新增模式
121
-if (planInfo==-1){
122
+if (planInfo == -1) {
122 123
   const caseNumber = generateCode();
123 124
   const fileId = ref()
124
-  result.value= caseNumber
125
+  result.value = caseNumber
125 126
   //初始化 fromVue
126 127
   fromVue.value = {
127 128
     caseNumber: caseNumber,
@@ -140,10 +141,10 @@ if (planInfo==-1){
140 141
     viewCount: '0',
141 142
     downloadCount: '0'
142 143
   };
143
-  console.log( result.value);
144
+  console.log(result.value);
144 145
 }
145 146
 
146
-const resDetail=ref('')
147
+const resDetail = ref('')
147 148
 const ruleIds = ref([]);
148 149
 /*const getRuleId = () => {
149 150
   var url = '/sgsafe/ExamHead/getCheckRuleId'
@@ -158,13 +159,13 @@ const ruleIds = ref([]);
158 159
   console.log('ruleIds', ruleIds)
159 160
 }
160 161
 getRuleId()*/
161
-const showRule=ref(false);
162
+const showRule = ref(false);
162 163
 
163
-const distestType=ref(false)
164
-if (planInfo==1) {
164
+const distestType = ref(false)
165
+if (planInfo == 1) {
165 166
   console.log(planInfo);
166 167
   title = '查看事故案例'
167
-  fromVue.value= JSON.parse(route.query.data)
168
+  fromVue.value = JSON.parse(route.query.data)
168 169
   if (!fromVue.value.fileId) {
169 170
     const newFileId = guid();
170 171
     fromVue.value.fileId = newFileId;
@@ -175,9 +176,9 @@ if (planInfo==1) {
175 176
 
176 177
   console.log('编辑模式 - fileId:', result.value);
177 178
 }
178
- const  whether=ref(false)
179
+const whether = ref(false)
179 180
 
180
-const planLevelList1=ref([])
181
+const planLevelList1 = ref([])
181 182
 
182 183
 const onConfirmDatetime = () => {
183 184
   const year = currentDate.value[0];
@@ -256,25 +257,25 @@ const cancelTimePicker = () => {
256 257
 const showActionSheet = ref(false)
257 258
 const showActionSheet1 = ref(false)
258 259
 const planLevelList = [
259
-  { name: '转发', value: '转发' },
260
-  { name: '内部', value: '内部' },
261
-  { name: '文章', value: '文章' }
260
+  {name: '转发', value: '转发'},
261
+  {name: '内部', value: '内部'},
262
+  {name: '文章', value: '文章'}
262 263
 ];
263
-const isdisabled=ref(true)
264
-const isdisabled2=ref(true)
264
+const isdisabled = ref(true)
265
+const isdisabled2 = ref(true)
265 266
 const onSelect = (item) => {
266
-fromVue.value.fileContent = item.name
267
+  fromVue.value.fileContent = item.name
267 268
 
268
-  showActionSheet.value=false
269
+  showActionSheet.value = false
269 270
 }
270 271
 
271 272
 const onSelect2 = (item) => {
272
-  fromVue.value.fileType=item.name
273
-  showActionSheet2.value=false
274
-  }
273
+  fromVue.value.fileType = item.name
274
+  showActionSheet2.value = false
275
+}
275 276
 
276 277
 const onCaseSourseSelect = (item) => {
277
-  fromVue.value.caseSource=item.dicCode
278
+  fromVue.value.caseSource = item.dicCode
278 279
   caseSourceFlag.value = false
279 280
 }
280 281
 
@@ -288,13 +289,13 @@ const displayFileName = ref('')
288 289
 const onSelect1 = (item) => {
289 290
   console.log(item);
290 291
   formJieguo.value.resultFile = item.value
291
-  result.value=formJieguo.value.resultFile
292
+  result.value = formJieguo.value.resultFile
292 293
   displayFileName.value = item.name
293 294
   console.log(result.value);
294 295
   showActionSheet1.value = false
295 296
 }
296 297
 const questionIds = ref([])
297
-const actions=ref([])
298
+const actions = ref([])
298 299
 /*const getQuestionId = () => {
299 300
   var url = '/sgsafe/AssessmentRecord/queryRuleNoPage';
300 301
   var param = {};
@@ -335,7 +336,7 @@ const baocun3 = (ruleFormRef) => {
335 336
           if (ruleFormRef.value) {
336 337
             ruleFormRef.value.resetFields();
337 338
           }
338
-          dialogVisibletemplate.value=false
339
+          dialogVisibletemplate.value = false
339 340
         } else {
340 341
           ElMessage.error('操作失败!' + response.data.msg)
341 342
         }
@@ -348,7 +349,8 @@ const baocun3 = (ruleFormRef) => {
348 349
 /* 组织树选择 */
349 350
 const showBottom = ref(false)
350 351
 import OrganizationalWithLeaf from '@/components/OrganizationalWithLeaf.vue';
351
-import { showFailToast, showLoadingToast, showSuccessToast } from 'vant';
352
+import {showFailToast, showLoadingToast, showSuccessToast} from 'vant';
353
+
352 354
 const handleTableDataUserDeptUpdate = async (nodeData) => {
353 355
   formJieguo.value.drillDept = nodeData.deptCode + '-' + nodeData.deptName
354 356
   showBottom.value = false
@@ -360,14 +362,14 @@ const addEmergencyDrillPlan = async () => {
360 362
     message: '加载中',
361 363
     forbidClick: true
362 364
   })
363
-  
365
+
364 366
   fromVue.value.fileId = result.value
365 367
 
366 368
   var url = '/sgsafe/Manager/saveAccident';
367 369
   const params = {
368 370
     json: JSON.stringify(fromVue.value)
369 371
   }
370
-  proxy.$axios.post(url,params).then(res=>{
372
+  proxy.$axios.post(url, params).then(res => {
371 373
     if (res.data.code === 0) {
372 374
       loadingToast.close()
373 375
       showSuccessToast('保存成功')
@@ -393,7 +395,7 @@ const dicList = ref([])
393 395
 const getDicList = () => {
394 396
   tools.dic.getDicList(['systemTypes']).then((response => {
395 397
 
396
-   const rawData = response.data.data
398
+    const rawData = response.data.data
397 399
     dicList.value = rawData.systemTypes.map(item => ({
398 400
       name: item.dicName,      // 必须有 name 字段!
399 401
       code: item.dicCode       // 可选,保留原始 code 供后续使用
@@ -430,7 +432,7 @@ onMounted(() => {
430 432
   //selectedDateText.value = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
431 433
   getDicList()
432 434
 })
433
-const showActionSheet2=ref(false)
435
+const showActionSheet2 = ref(false)
434 436
 /* 文件上传 */
435 437
 import AttachmentS3 from '@/components/AttachmentS3.vue';
436 438
 
@@ -448,12 +450,12 @@ const getDepartmentLeaderNameysr = (item) => {
448 450
   fromVue.value.punId = item.user.id
449 451
 };
450 452
 
451
-const onCaseSourceConfirm = ({ selectedOptions }) => {
453
+const onCaseSourceConfirm = ({selectedOptions}) => {
452 454
   caseSourceFlag.value = false;
453 455
   fromVue.value.caseSource = selectedOptions[0].text;
454 456
 };
455 457
 
456
-const onAccidentLevelConfirm = ({ selectedOptions }) => {
458
+const onAccidentLevelConfirm = ({selectedOptions}) => {
457 459
   accidentLevelFlag.value = false;
458 460
   fromVue.value.accidentLevel = selectedOptions[0].text;
459 461
 };
@@ -465,55 +467,55 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
465 467
   <div class="page-container">
466 468
     <van-sticky class="header">
467 469
       <van-nav-bar
468
-        :title="title"
469
-        left-text="返回"
470
-        left-arrow
471
-        @click-left="onClickLeft" >
470
+          :title="title"
471
+          left-text="返回"
472
+          left-arrow
473
+          @click-left="onClickLeft">
472 474
       </van-nav-bar>
473 475
     </van-sticky>
474 476
     <div class="scroll-container">
475 477
       <van-form @submit="onSubmit">
476
-<!--        <van-field-->
477
-<!--          v-model="fromVue.caseNumber"-->
478
-<!--          label="案例编号"-->
479
-<!--          name="caseNumber"-->
480
-<!--          required-->
481
-<!--          placeholder="请输入案例编号"-->
482
-<!--          :rules="[{required: true, message: '请输入文件编号'}]"-->
483
-<!--        />-->
478
+        <!--        <van-field-->
479
+        <!--          v-model="fromVue.caseNumber"-->
480
+        <!--          label="案例编号"-->
481
+        <!--          name="caseNumber"-->
482
+        <!--          required-->
483
+        <!--          placeholder="请输入案例编号"-->
484
+        <!--          :rules="[{required: true, message: '请输入文件编号'}]"-->
485
+        <!--        />-->
484 486
         <van-field
485 487
             v-model="fromVue.caseNumber"
486
-        label="案例编号"
487
-        name="caseNumber"
488
+            label="案例编号"
489
+            name="caseNumber"
488 490
             readonly
489
-        :rules="[{required: true, message: '编号生成失败,请点击“重新生成”'}]"
491
+            :rules="[{required: true, message: '编号生成失败,请点击“重新生成”'}]"
490 492
         />
491 493
 
492 494
         <van-field
493
-          v-model="fromVue.caseTitle"
494
-          label="案例标题"
495
-          name="caseTitle"
496
-          :readonly="isCaseSubmitted"
497
-          required
498
-          placeholder="请输入案例标题"
499
-          :rules="[{required: true, message: '请输入标题名称'}]"
495
+            v-model="fromVue.caseTitle"
496
+            label="案例标题"
497
+            name="caseTitle"
498
+            :readonly="isCaseSubmitted"
499
+            required
500
+            placeholder="请输入案例标题"
501
+            :rules="[{required: true, message: '请输入标题名称'}]"
500 502
         />
501 503
 
502 504
         <van-field
503
-          v-model="fromVue.caseSource"
504
-          readonly
505
-          label="案例来源"
506
-          name="caseSource"
507
-          :readonly="isCaseSubmitted"
508
-          @click="!isCaseSubmitted && (caseSourceFlag = true)"
505
+            v-model="fromVue.caseSource"
506
+            readonly
507
+            label="案例来源"
508
+            name="caseSource"
509
+            :readonly="isCaseSubmitted"
510
+            @click="!isCaseSubmitted && (caseSourceFlag = true)"
509 511
         />
510 512
         <van-field
511
-          readonly
512
-          v-model="fromVue.accidentLevel"
513
-          label="事故等级"
514
-          name="accidentLevel"
515
-          :readonly="isCaseSubmitted"
516
-          @click="!isCaseSubmitted && (accidentLevelFlag = true)"
513
+            readonly
514
+            v-model="fromVue.accidentLevel"
515
+            label="事故等级"
516
+            name="accidentLevel"
517
+            :readonly="isCaseSubmitted"
518
+            @click="!isCaseSubmitted && (accidentLevelFlag = true)"
517 519
         />
518 520
 
519 521
         <van-field
@@ -534,7 +536,7 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
534 536
             :rules="[{required: true, message: '请输入事故地点'}]"
535 537
         />
536 538
 
537
-<!--        //时间-->
539
+        <!--        //时间-->
538 540
         <van-field
539 541
             v-model="fromVue.accidentTime"
540 542
             is-link
@@ -586,7 +588,7 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
586 588
             </template>
587 589
           </van-time-picker>
588 590
         </van-popup>
589
-<!--        // 标签-->
591
+        <!--        // 标签-->
590 592
 
591 593
         <van-field
592 594
             v-model="fromVue.accidentTags"
@@ -630,9 +632,9 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
630 632
             :rules="[{required: true, message: '请输入防范与整改措施'}]"
631 633
         />
632 634
 
633
-        <van-field label="附件上传" >
635
+        <van-field label="附件上传">
634 636
           <template #input>
635
-            <AttachmentS3 :f-id="result" />
637
+            <AttachmentS3 :f-id="result"/>
636 638
           </template>
637 639
         </van-field>
638 640
         <div style="margin: 16px;">
@@ -643,14 +645,14 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
643 645
       </van-form>
644 646
 
645 647
       <van-action-sheet
646
-        v-model:show="showActionSheet"
647
-        :actions="planLevelList"
648
-        @select="onSelect"
648
+          v-model:show="showActionSheet"
649
+          :actions="planLevelList"
650
+          @select="onSelect"
649 651
       />
650 652
       <van-action-sheet
651
-        v-model:show="showActionSheet2"
652
-        :actions="dicList"
653
-        @select="onSelect2"
653
+          v-model:show="showActionSheet2"
654
+          :actions="dicList"
655
+          @select="onSelect2"
654 656
       />
655 657
 
656 658
       <van-action-sheet
@@ -675,27 +677,27 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
675 677
         />
676 678
       </van-popup>
677 679
 
678
-<!--      <van-action-sheet-->
679
-<!--          v-model:show="accidentLevelFlag"-->
680
-<!--          :actions="accidentDictList.accident_level"-->
681
-<!--          @select="onAccidentLevelSelect"-->
682
-<!--      />-->
680
+      <!--      <van-action-sheet-->
681
+      <!--          v-model:show="accidentLevelFlag"-->
682
+      <!--          :actions="accidentDictList.accident_level"-->
683
+      <!--          @select="onAccidentLevelSelect"-->
684
+      <!--      />-->
683 685
 
684
-<!--      <van-action-sheet-->
685
-<!--        v-model:show="showActionSheet1"-->
686
-<!--        :actions="planLevelList1"-->
687
-<!--        field-names="{ text: 'fileName', value: 'fileId' }"-->
688
-<!--        @select="onSelect1"-->
689
-<!--      />-->
686
+      <!--      <van-action-sheet-->
687
+      <!--        v-model:show="showActionSheet1"-->
688
+      <!--        :actions="planLevelList1"-->
689
+      <!--        field-names="{ text: 'fileName', value: 'fileId' }"-->
690
+      <!--        @select="onSelect1"-->
691
+      <!--      />-->
690 692
 
691 693
       <van-popup v-model:show="showBottom" position="bottom" :style="{ height: '30%' }">
692
-        <OrganizationalWithLeaf @update:selected-node="handleTableDataUserDeptUpdate" />
694
+        <OrganizationalWithLeaf @update:selected-node="handleTableDataUserDeptUpdate"/>
693 695
       </van-popup>
694 696
       <van-popup v-model:show="showDatePicker" position="bottom">
695 697
         <van-date-picker
696
-          v-model="currentDate"
697
-          @confirm="onDatePicker"
698
-          @cancel="showDatePicker = false" />
698
+            v-model="currentDate"
699
+            @confirm="onDatePicker"
700
+            @cancel="showDatePicker = false"/>
699 701
       </van-popup>
700 702
     </div>
701 703
   </div>
@@ -708,6 +710,7 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
708 710
   flex-direction: column;
709 711
 
710 712
 }
713
+
711 714
 /*  overflow-y: auto; !* 启用垂直滚动 *!*/
712 715
 
713 716
 
@@ -728,10 +731,11 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
728 731
   background: #f5f5f5;
729 732
   padding: 12px;
730 733
 }
734
+
731 735
 .row-container {
732 736
   display: flex;
733 737
   justify-content: space-between; /* 关键:左右分布 */
734
-  align-items: center;             /* 垂直居中 */
738
+  align-items: center; /* 垂直居中 */
735 739
 
736 740
 }
737 741
 
@@ -748,6 +752,6 @@ const onAccidentLevelConfirm = ({ selectedOptions }) => {
748 752
   height: 22px;
749 753
   font-size: 12px;
750 754
   padding: 0 8px;
751
-  min-width: 60px;                 /* 可选:保持按钮最小宽度一致 */
755
+  min-width: 60px; /* 可选:保持按钮最小宽度一致 */
752 756
 }
753 757
 </style>

Loading…
Отказ
Запис