ソースを参照

Merge branch 'develop' of http://123.206.9.27:3000/ShinSoft_Xxhsyb/Proj_SafePlat_Vue_Sgh5 into develop

# Conflicts:
#	src/view/Home2.vue
wangqi 2ヶ月前
コミット
545668823b

+ 1
- 0
package.json ファイルの表示

@@ -32,6 +32,7 @@
32 32
 		"marked": "^4.3.0",
33 33
 		"pinia": "^2.1.7",
34 34
 		"pinia-plugin-persistedstate": "^3.2.1",
35
+		"qrcode": "^1.5.4",
35 36
 		"qs": "^6.11.2",
36 37
 		"vant": "^4.0.0",
37 38
 		"video.js": "^8.23.4",

+ 351
- 0
src/components/AttachmentS3Required.vue ファイルの表示

@@ -0,0 +1,351 @@
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
+
347
+
348
+
349
+
350
+
351
+

+ 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>

+ 118
- 0
src/components/QuizResultPopup.vue ファイルの表示

@@ -0,0 +1,118 @@
1
+
2
+<template>
3
+  <van-popup v-model:show="innerShow" position="top" style="height: 100%">
4
+    <van-sticky>
5
+      <van-nav-bar title="错题回顾" left-arrow @click-left="close" />
6
+    </van-sticky>
7
+
8
+    <div v-if="loading" class="loading-wrapper">
9
+      <van-loading>加载中...</van-loading>
10
+    </div>
11
+
12
+    <div v-else-if="error" class="error">加载失败:{{ error }}</div>
13
+
14
+    <div v-else>
15
+      <div style="margin: 10px 20px; font-weight: bold;">
16
+        本次得分:{{ totalScore }}
17
+      </div>
18
+      <van-divider />
19
+
20
+      <!-- 遍历错题/全部题 -->
21
+      <div v-for="item in mistakeData" :key="item.id" class="question">
22
+        <p>
23
+          <span v-if="item.category === '单选'">[单选]</span>
24
+          <span v-if="item.category === '多选'">[多选]</span>
25
+          <span v-if="item.category === '判断'">[判断]</span>
26
+          {{ item.stem }}
27
+        </p>
28
+
29
+        <p>
30
+          <span :style="{ color: item.userAnswer === item.answer ? '#007aff' : 'red' }">
31
+            提交答案: {{ item.userAnswer || '未作答' }}
32
+          </span>
33
+        </p>
34
+        <p style="color: #007aff">正确答案: {{ item.answer }}</p>
35
+
36
+        <div v-if="item.category === '单选' || item.category === '多选'" class="kong">
37
+          <div>A. {{ item.optionA }}</div>
38
+          <div>B. {{ item.optionB }}</div>
39
+          <div>C. {{ item.optionC }}</div>
40
+          <div v-if="item.optionD">D. {{ item.optionD }}</div>
41
+          <div v-if="item.optionE">E. {{ item.optionE }}</div>
42
+        </div>
43
+        <div v-if="item.category === '判断'" class="kong">
44
+          <div>A. 正确</div>
45
+          <div>B. 错误</div>
46
+        </div>
47
+
48
+        <van-divider />
49
+      </div>
50
+
51
+      <!-- ✅ 新增:底部确定按钮 -->
52
+      <div style="text-align: center; margin: 20px 0;">
53
+        <van-button type="primary" size="large" @click="close">确定</van-button>
54
+      </div>
55
+    </div>
56
+  </van-popup>
57
+</template>
58
+
59
+<script setup>
60
+import { getCurrentInstance, ref, watch } from 'vue';
61
+import { showFailToast } from 'vant';
62
+const { proxy } = getCurrentInstance();
63
+const props = defineProps({
64
+  show: Boolean,
65
+  courseId: String,
66
+  userId: String
67
+});
68
+
69
+const emit = defineEmits(['update:show']);
70
+
71
+const innerShow = ref(false);
72
+const loading = ref(false);
73
+const error = ref('');
74
+const mistakeData = ref([]);
75
+const totalScore = ref(0);
76
+
77
+// 同步外部 show 控制
78
+watch(() => props.show, (val) => {
79
+  innerShow.value = val;
80
+  if (val) {
81
+    loadMistakeData();
82
+  }
83
+});
84
+
85
+const close = () => {
86
+  innerShow.value = false;
87
+  emit('update:show', false);
88
+};
89
+
90
+const loadMistakeData = async () => {
91
+  loading.value = true;
92
+  error.value = '';
93
+  try {
94
+    const query = { headId: props.courseId };
95
+    const res = await proxy.$axios.post('/sgsafe/ExamLine/queryMistake', {
96
+      params: JSON.stringify(query)
97
+    });
98
+
99
+    if (res.data.code === 0) {
100
+      const data = res.data.data || [];
101
+      mistakeData.value = data;
102
+
103
+      // 注意:queryMistake 返回的是每道题的 userAnswer 和 answer
104
+      // 总分通常是 userScore 字段之和
105
+      totalScore.value = data.reduce((sum, item) => sum + (Number(item.userScore) || 0), 0);
106
+    } else {
107
+      error.value = res.data.msg || '获取错题失败';
108
+      showFailToast(error.value);
109
+    }
110
+  } catch (err) {
111
+    console.error('加载错题失败', err);
112
+    error.value = '网络错误';
113
+    showFailToast('加载错题失败');
114
+  } finally {
115
+    loading.value = false;
116
+  }
117
+};
118
+</script>

+ 69
- 25
src/router/index.ts ファイルの表示

@@ -659,23 +659,80 @@ const router = createRouter({
659 659
 			name: '安全费用预算编辑',
660 660
 			component: () => import('@/view/moneySafe/safeMoneyBudgetList.vue')
661 661
 		},
662
+		{
663
+			path: '/class2',
664
+			name: '学习课程库',
665
+			component: () => import("@/view/dati/classOne/class2.vue")
666
+		},
667
+		{
668
+			path: '/courseManagement',
669
+			name: '课程管理',
670
+			component: () => import('@/view/dati/courseManagement/courseManagement.vue')
671
+		},
672
+		{
673
+			path: '/courseManagementList',
674
+			name: '课程管理编辑',
675
+			component: () => import('@/view/dati/courseManagement/courseManagementList.vue')
676
+		},
677
+		{
678
+			path: '/courseAddPeo',
679
+			name: '课程添加人员',
680
+			component: () => import('@/view/dati/courseManagement/addPeo.vue')
681
+		},
682
+		{
683
+			path: '/section',
684
+			name: '课程添加小节',
685
+			component: () => import('@/view/dati/courseManagement/section.vue')
686
+		},
687
+		{
688
+			path: '/sectionList',
689
+			name: '人员小节',
690
+			component: () => import('@/view/dati/classOne/sectionList.vue')
691
+		},
692
+		{
693
+			path: '/line',
694
+			name: '人员小节答题',
695
+			component: () => import('@/view/dati/classOne/line.vue')
696
+		},
697
+		{
698
+			path: '/learning1',
699
+			name: '人员学习',
700
+			component: () => import('@/view/dati/classOne/learning.vue')
701
+		},
702
+		{
703
+			path: '/fcbkdatistart',
704
+			name: '逢查必考跳转答题',
705
+			component: () => import('@/view/dati/examCheck/fcbkdatistart.vue')
706
+		},
707
+
708
+		{
709
+			path: '/guest-register',
710
+			name: '游客模式(答题)',
711
+			component: () => import('@/view/login/Register.vue'),
712
+			meta: { guestMode: true }
713
+		}
662 714
 	]
663 715
 })
664
-
716
+function isInWeCom(): boolean {
717
+	return /wxwork/i.test(navigator.userAgent);
718
+}
665 719
 // 路由守卫
666 720
 router.beforeEach(async (to, from, next) => {
667 721
 
668
-	/*
669
-		const requiresWeCom = to.matched.some(record => record.meta.requiresWeCom);
670
-
671
-		if (requiresWeCom && typeof wx === 'undefined') {
672
-			console.warn('该页面需在企业微信内打开');
673
-			// 可以跳转提示页或者 Toast 提示
674
-			return next(false);
722
+	const token1 = localStorage.getItem('token');
723
+	const isWeCom = isInWeCom();
724
+	if (to.path === '/fcbkdatistart') {
725
+		// 如果不在企微中,且没有有效 token,且不是已经在游客页
726
+		if (!isWeCom && (!token1 || !isTokenValid(token1)) && from.path !== '/guest-register') {
727
+			console.log('【游客模式】非企微访问 fcbkdatistart,跳转游客注册');
728
+			// ✅ 关键修复:保留 to.query(即 examId 等参数)
729
+			return next({
730
+				path: '/guest-register',
731
+				query: to.query
732
+			});
675 733
 		}
676 734
 
677
-		wxReady((requestSignature().data))
678
-	*/
735
+	}
679 736
 	function isTokenValid(token : string) : boolean {
680 737
 		try {
681 738
 			// 检查 token 是否为合法 JWT 格式(3段)
@@ -738,21 +795,8 @@ router.beforeEach(async (to, from, next) => {
738 795
 		return null; // 或者抛出错误,根据你的业务需求
739 796
 	}
740 797
 	console.log(to);
741
-	if (to.path == '/Home1'||to.path=='/yinhuan/todo_detail/index'|| to.path == '/lz-rcd-detail'||to.path == '/emergencyResources'||to.path == '/cardManager/specialWork'||to.path == '/cardManager/equipment'||to.path == '/cardManager/engineer') {
742
-
743
-		/*const token = localStorage.getItem('token');
744
-		// 判断是否已经初始化过用户信息
745
-		const hasUserInfo = !!localStorage.getItem('userId');
746
-		if (hasUserInfo) {
747
-			// @ts-ignore
748
-			if (token && isTokenValid(token)) {
749
-				next();
750
-				return;
751
-			}
752
-		}*/
753
-
754
-		// const userId=ref('3E4C4009430211B28E367D90704E23CA')
755
-
798
+	if (to.path == '/Home1'||to.path=='/yinhuan/todo_detail/index'|| to.path == '/lz-rcd-detail'||to.path == '/emergencyResources'||to.path == '/cardManager/specialWork'
799
+		||to.path == '/cardManager/equipment'||to.path == '/cardManager/engineer') {
756 800
 		const userId=ref(import.meta.env.VITE_USER_ID)
757 801
 		console.log(userId.value);
758 802
 		console.log("当前环境:");

+ 245
- 34
src/view/announ/annexList.vue ファイルの表示

@@ -24,16 +24,51 @@ try {
24 24
   deptInformation.value = jsonArray ? JSON.parse(jsonArray) : [];
25 25
 } catch (error) {
26 26
   deptInformation.value = [];
27
+  console.error('解析部门信息失败:', error)
27 28
 }
28
-const deptName = deptInformation.value[0].deptName
29
-const deptCode = deptInformation.value[0].deptCode
30 29
 
31
-const guid = () =>  {
32
-  function S4() {
33
-    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
34
-  }
35
-  return (S4()+S4()+S4()+S4()+S4()+S4()+S4()+S4())
30
+
31
+const deptName = deptInformation.value[0]?.deptName || ''
32
+const deptCode = deptInformation.value[0]?.deptCode || ''
33
+
34
+//  如果部门信息为空,给出提示
35
+if (!deptCode) {
36
+  console.warn('部门编码为空,可能影响数据保存')
36 37
 }
38
+
39
+
40
+
41
+// 添加:与Web端一致的 generateCode 函数
42
+const generateCode = () => {
43
+  // 获取当前日期并格式化为 YYYYMMDD
44
+  const now = new Date();
45
+  const year = now.getFullYear();
46
+  const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需加1
47
+  const day = String(now.getDate()).padStart(2, '0');
48
+  const formattedDate = `${year}${month}${day}`;
49
+  
50
+  // 时间部分:HHmmss
51
+  const hours = String(now.getHours()).padStart(2, '0');
52
+  const minutes = String(now.getMinutes()).padStart(2, '0');
53
+  const seconds = String(now.getSeconds()).padStart(2, '0');
54
+  const formattedTime = `${hours}${minutes}${seconds}`;
55
+  
56
+  // 模拟生成三位流水号(可以根据需要替换为递增逻辑)
57
+  const sequenceNumber = Math.floor(Math.random() * 1000); // 随机生成 0-999
58
+  const paddedSequence = String(sequenceNumber).padStart(3, '0'); // 补零到三位
59
+  
60
+  // 拼接编号
61
+  return `LZFJ${formattedDate}${formattedTime}${paddedSequence}`;
62
+};
63
+
64
+// 使用 ref 存储生成的编号
65
+const generatedCode = ref(generateCode());
66
+
67
+// 定义重新生成编号的方法
68
+const regenerateCode = () => {
69
+  generatedCode.value = generateCode();
70
+};
71
+
37 72
 const formJieguo = ref({
38 73
   resultWhether:'',
39 74
   resultDetail:'',
@@ -49,13 +84,32 @@ const  userName1=localStorage.getItem('userName');
49 84
 
50 85
 const result=ref('')
51 86
 const fromVue=ref({})
87
+
88
+// 修复:新增时初始化基础字段(与Web端保持一致)
52 89
 if (route.query.mark) {
53 90
   planInfo = JSON.parse(route.query.mark)
54 91
 }
92
+
55 93
 console.log(planInfo);
56 94
 if (planInfo==-1){
57
-  result.value=guid()
58
-  console.log( result.value);
95
+  // 新增模式:初始化基础字段(与Web端handAdd函数保持一致)
96
+  regenerateCode() // 重新生成编号
97
+  result.value = generatedCode.value // 使用 generateCode 生成的编号
98
+  fromVue.value = {
99
+    id: '',  // 新增时 id 为空字符串
100
+    fileName: '',
101
+    fileType: '',
102
+    fileContent: '',
103
+    fileTime: '',  // 添加:Web端有此字段
104
+    filePeoples: '',  // 添加:Web端有此字段
105
+    fileSubmit: '',
106
+    remarks: '',
107
+    enable: '',
108
+    fileNumber: '',
109
+    yearValue: '',  // H5端特有
110
+    fileId: result.value
111
+  }
112
+  console.log('生成的 fileId:', result.value);
59 113
 }
60 114
 
61 115
 const resDetail=ref('')
@@ -80,7 +134,6 @@ if (planInfo==1) {
80 134
   console.log(planInfo);
81 135
   title = '修改安全文件'
82 136
   fromVue.value= JSON.parse(route.query.data)
83
-
84 137
   result.value=fromVue.value.fileId
85 138
   console.log(result.value);
86 139
 }
@@ -100,15 +153,18 @@ const planLevelList = [
100 153
 const isdisabled=ref(true)
101 154
 const isdisabled2=ref(true)
102 155
 const onSelect = (item) => {
103
-fromVue.value.fileContent = item.name
104
-
156
+  fromVue.value.fileContent = item.name
105 157
   showActionSheet.value=false
158
+  // 性质改变时也触发文件编号生成
159
+  generateFileSumbmit()
106 160
 }
107 161
 
108 162
 const onSelect2 = (item) => {
109 163
   fromVue.value.fileType=item.name
110 164
   showActionSheet2.value=false
111
-  }
165
+  // 文件类别改变时触发文件编号生成
166
+  generateFileSumbmit()
167
+}
112 168
 
113 169
 const displayFileName = ref('')
114 170
 const onSelect1 = (item) => {
@@ -119,6 +175,39 @@ const onSelect1 = (item) => {
119 175
   console.log(result.value);
120 176
   showActionSheet1.value = false
121 177
 }
178
+
179
+const yearOptions = Array.from({ length: 10 }, (_, i) => {
180
+  const year = currentYear.value - 5 + i;  // 修复:使用 currentYear.value
181
+  return {
182
+    text: String(year),
183
+    value: String(year)
184
+  };
185
+}).reverse();
186
+const showYearPicker = ref(false);
187
+
188
+const formData = ref({
189
+  year: String(currentYear.value),  // 修复:使用 currentYear.value
190
+  quarter: null
191
+})
192
+
193
+// 年份选择确认
194
+const onConfirmYear = (value) => {
195
+  let selectedYear = '';
196
+  if (value && value.selectedOptions && value.selectedOptions.length > 0) {
197
+    const selectedOption = value.selectedOptions[0];
198
+    selectedYear = typeof selectedOption === 'string' ? selectedOption : selectedOption.value || selectedOption.text;
199
+  } else if (typeof value === 'string') {
200
+    selectedYear = value;
201
+  }
202
+
203
+  formData.value.year = selectedYear;
204
+  fromVue.value.yearValue = selectedYear; // 修复:同步到 fromVue
205
+  showYearPicker.value = false;
206
+  // 触发文件编号生成,而不是调用不存在的 search()
207
+  generateFileSumbmit();
208
+}
209
+
210
+
122 211
 const questionIds = ref([])
123 212
 const actions=ref([])
124 213
 /*const getQuestionId = () => {
@@ -129,7 +218,7 @@ const actions=ref([])
129 218
       questionIds.value = response.data.data; // 先赋值给 questionIds.value
130 219
 
131 220
 
132
-      // 关键:使用 questionIds.value.map
221
+      // 关键:使用 questionIds.value.map
133 222
       actions.value = questionIds.value.map(item => ({
134 223
         name: item.ruleName,
135 224
         value: item.id
@@ -181,26 +270,73 @@ const handleTableDataUserDeptUpdate = async (nodeData) => {
181 270
 }
182 271
 
183 272
 const addEmergencyDrillPlan = async () => {
273
+  // 参数验证
274
+  if (!deptCode) {
275
+    showFailToast('获取部门信息失败,请重新登录')
276
+    return
277
+  }
278
+  
279
+  if (!result.value) {
280
+    showFailToast('文件ID生成失败,请重试')
281
+    return
282
+  }
283
+  
284
+  // 表单验证
285
+  if (!fromVue.value.fileName) {
286
+    showFailToast('请输入文件名称')
287
+    return
288
+  }
289
+  
290
+  if (!fromVue.value.fileType) {
291
+    showFailToast('请选择文件类别')
292
+    return
293
+  }
294
+  
184 295
   const loadingToast = showLoadingToast({
185 296
     duration: 0,
186 297
     message: '加载中',
187 298
     forbidClick: true
188 299
   })
189
-  var url = '/sgsafe/ResponFile/save1';
190
-  const params = {
191
-    json: JSON.stringify(fromVue.value)
192
-  }
193
-  proxy.$axios.post(url,params).then(res=>{
300
+  
301
+  try {
302
+    // 与Web端保持一致:只设置 addDeptCode,其他字段已经在 fromVue.value 中
303
+    fromVue.value.addDeptCode = deptCode
304
+    
305
+    // 确保 fileId 正确设置
306
+    if (!fromVue.value.fileId) {
307
+      fromVue.value.fileId = result.value
308
+    }
309
+    
310
+    // 确保文件编号已生成
311
+    if (!fromVue.value.fileSubmit && fromVue.value.fileType && fromVue.value.yearValue) {
312
+      generateFileSumbmit()
313
+    }
314
+    
315
+    // 调试:打印完整数据(与Web端保持一致)
316
+    console.log('保存的数据:', JSON.stringify(fromVue.value))
317
+    
318
+    var url = '/sgsafe/ResponFile/save1';
319
+    const params = {
320
+      json: JSON.stringify(fromVue.value)
321
+    }
322
+    
323
+    const res = await proxy.$axios.post(url, params)
324
+    
194 325
     if (res.data.code === 0) {
195 326
       loadingToast.close()
196 327
       showSuccessToast('保存成功')
197 328
       onClickLeft()
198
-
199 329
     } else {
200 330
       loadingToast.close()
201
-      showFailToast('操作失败!' + res.data.msg)
331
+      showFailToast('操作失败!' + (res.data.msg || '未知错误'))
202 332
     }
203
-  })
333
+  } catch (error) {
334
+    loadingToast.close()
335
+    console.error('保存失败:', error)
336
+    // 打印详细错误信息
337
+    console.error('错误详情:', error.response?.data || error)
338
+    showFailToast('保存失败:' + (error.message || '网络错误,请稍后重试'))
339
+  }
204 340
 }
205 341
 const showDatePicker = ref(false)
206 342
 const onDatePicker = (value) => {
@@ -214,24 +350,69 @@ const dicList = ref([])
214 350
 //获取字典集合
215 351
 import tools from '@/tools'
216 352
 const getDicList = () => {
217
-  tools.dic.getDicList(['systemTypes']).then((response => {
218
-
219
-   const rawData = response.data.data
220
-    dicList.value = rawData.systemTypes.map(item => ({
221
-      name: item.dicName,      // 必须有 name 字段!
222
-      code: item.dicCode       // 可选,保留原始 code 供后续使用
353
+  // 修复:使用 'fileTypes' 而不是 'systemTypes'
354
+  tools.dic.getDicList(['fileTypes']).then((response => {
355
+    const rawData = response.data.data
356
+    // 修复:使用 fileTypes 而不是 systemTypes,value 使用 dicName
357
+    dicList.value = rawData.fileTypes.map(item => ({
358
+      name: item.dicName,      // 使用 dicName
359
+      value: item.dicName       // value 也使用 dicName,与 web 端保持一致
223 360
     }));
224 361
     console.log(JSON.stringify(dicList.value))
225 362
   }))
226 363
 }
364
+
365
+// 新增:文件编号自动生成函数
366
+const generateFileSumbmit = () => {
367
+  let fileSubmit = ''
368
+  
369
+  // 1. 根据文件类别拼接前缀
370
+  if (fromVue.value.fileType && fromVue.value.fileType !== '其他') {
371
+    const prefixMap = {
372
+      '安全正式发文': '山信安管便字',
373
+      '安全管理便文': '山信软件安字',
374
+      '环保正式发文': '山信软件环字',
375
+      '安委会纪要': '山信软件纪要安委字'
376
+    }
377
+    const prefix = prefixMap[fromVue.value.fileType]
378
+    if (prefix) {
379
+      fileSubmit += prefix
380
+    }
381
+  }
382
+  
383
+  // 2. 拼接年份
384
+  if (fromVue.value.yearValue) {
385
+    fileSubmit += `〔${fromVue.value.yearValue}〕`
386
+  }
387
+  
388
+  // 3. 拼接编号
389
+  if (fromVue.value.fileNumber) {
390
+    fileSubmit += `${fromVue.value.fileNumber}号`
391
+  }
392
+  
393
+  // 更新文件编号
394
+  fromVue.value.fileSubmit = fileSubmit
395
+}
396
+
227 397
 onMounted(() => {
228 398
   const today = new Date()
229 399
   const year = today.getFullYear()
230
-  const month = today.getMonth() + 1 // 月份从 0 开始
400
+  const month = today.getMonth() + 1
231 401
   const day = today.getDate()
232 402
   currentDate.value = [year, month, day]
233
-  //selectedDateText.value = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
403
+  
404
+  // 新增:初始化年份值
405
+  if (!fromVue.value.yearValue) {
406
+    fromVue.value.yearValue = String(year)
407
+    formData.value.year = String(year)
408
+  }
409
+  
234 410
   getDicList()
411
+  
412
+  // 新增:如果是编辑模式,初始化文件编号生成
413
+  if (planInfo == 1) {
414
+    generateFileSumbmit()
415
+  }
235 416
 })
236 417
 const showActionSheet2=ref(false)
237 418
 /* 文件上传 */
@@ -240,7 +421,7 @@ import AttachmentS3 from '@/components/AttachmentS3.vue';
240 421
 const onSubmit = (values) => {
241 422
   addEmergencyDrillPlan()
242 423
 }
243
-const leaderKeyysr = guid();
424
+const leaderKeyysr = generateCode(); // 或者保留 guid 用于其他用途
244 425
 const PopupDepartmentLeaderNameRefysr = ref();
245 426
 const handleDepartmentLeaderNameysr = () => {
246 427
   PopupDepartmentLeaderNameRefysr.value.open();
@@ -266,12 +447,14 @@ const getDepartmentLeaderNameysr = (item) => {
266 447
     </van-sticky>
267 448
     <div class="scroll-container">
268 449
       <van-form @submit="onSubmit">
450
+        <!-- 修复:文件编号改为只读,提示自动生成 -->
269 451
         <van-field
270 452
           v-model="fromVue.fileSubmit"
271 453
           label="文件编号"
272 454
           name="fileSubmit"
273 455
           required
274
-          placeholder="请输入文件编号"
456
+          placeholder="选择文件类别、年份、编号后自动生成"
457
+          readonly
275 458
           :rules="[{required: true, message: '请输入文件编号'}]"
276 459
         />
277 460
 
@@ -280,7 +463,7 @@ const getDepartmentLeaderNameysr = (item) => {
280 463
           label="名称"
281 464
           name="fileName"
282 465
           required
283
-          placeholder="请输入文件编号"
466
+          placeholder="请输入文件名称"
284 467
           :rules="[{required: true, message: '请输入文件名称'}]"
285 468
         />
286 469
 
@@ -292,6 +475,33 @@ const getDepartmentLeaderNameysr = (item) => {
292 475
           required
293 476
           @click="showActionSheet2 = true"
294 477
         />
478
+        
479
+        <van-field
480
+          :model-value="fromVue.yearValue"
481
+          is-link
482
+          readonly
483
+          name="yearValue"
484
+          label="年份"
485
+          placeholder="点击选择年份"
486
+          @click="showYearPicker = true"
487
+        />
488
+        <van-popup v-model:show="showYearPicker" position="bottom">
489
+          <van-picker
490
+            :columns="yearOptions"
491
+            @confirm="onConfirmYear"
492
+            @cancel="showYearPicker = false"
493
+          />
494
+        </van-popup>
495
+        
496
+        <!-- 新增:编号输入框 -->
497
+        <van-field
498
+          v-model="fromVue.fileNumber"
499
+          label="编号"
500
+          name="fileNumber"
501
+          placeholder="请输入编号"
502
+          @input="generateFileSumbmit"
503
+        />
504
+        
295 505
         <van-field
296 506
           readonly
297 507
           v-model="fromVue.fileContent"
@@ -304,13 +514,14 @@ const getDepartmentLeaderNameysr = (item) => {
304 514
           v-model="fromVue.remarks"
305 515
           label="备注"
306 516
           name="remarks"
307
-
308 517
         />
518
+        
309 519
         <van-field label="附件上传" >
310 520
           <template #input>
311 521
             <AttachmentS3 :f-id="result" />
312 522
           </template>
313 523
         </van-field>
524
+        
314 525
         <div style="margin: 16px;">
315 526
           <van-button round block type="primary" native-type="submit">
316 527
             提交

+ 20
- 10
src/view/dati/checkTake/jieguo2.vue ファイルの表示

@@ -116,7 +116,7 @@
116 116
 		proxy
117 117
 	} = getCurrentInstance()
118 118
 	const route = useRoute();
119
-  const userAnswers = ref({});
119
+  const userAnswers = reactive({});
120 120
 
121 121
   const questions = ref([]);
122 122
   //获取试卷
@@ -135,22 +135,32 @@ const confirmResult = () => {
135 135
   showResult.value=false
136 136
 }
137 137
   const getForm = async () => {
138
-    var url = '/sgsafe/ExamLine/query'
139
-    const query = ref({
140
-      headId: courseId
141
-    })
142
-    var param = {
143
-      params: JSON.stringify(query.value)
144
-    }
138
+    const url = '/sgsafe/ExamLine/query';
139
+    const query = {
140
+      headId: courseId.value // ✅ 不需要用 ref 包裹临时对象
141
+    };
142
+    const param = {
143
+      params: JSON.stringify(query)
144
+    };
145 145
     try {
146 146
       const res = await proxy.$axios.post(url, param);
147 147
       if (res.data.code === 0) {
148
-        questions.value = res.data.data
148
+        questions.value = res.data.data;
149
+
150
+        // ✅ 修复点2:清空 reactive 对象(不能用 .value)
151
+        for (const key in userAnswers) {
152
+          delete userAnswers[key];
153
+        }
154
+
155
+        // ✅ 修复点3:直接赋值,不要 .value!
156
+        res.data.data.forEach(q => {
157
+          userAnswers[q.id] = q.userAnswer; // 假设后端字段名为 userAnswer
158
+        });
149 159
       } else {
150 160
         console.log('操作失败!' + res.data.msg);
151 161
       }
152 162
     } catch (error) {
153
-      console.log('请求出错:', questions);
163
+      console.log('请求出错:', error);
154 164
     }
155 165
   };
156 166
 

+ 951
- 0
src/view/dati/classOne/class2.vue ファイルの表示

@@ -0,0 +1,951 @@
1
+<template>
2
+  <div class="h5-container">
3
+    <van-nav-bar title="学习课程管理">
4
+
5
+      <template #right>
6
+        <van-icon
7
+          name="add"
8
+          size="24"
9
+          color="var(--van-nav-bar-icon-color)"
10
+          @click="handAdd"
11
+        />
12
+        <van-popover
13
+          v-model:show="showPopover"
14
+          :actions="actions"
15
+          placement="bottom-end"
16
+          @select="onSelect"
17
+        >
18
+          <template #reference>
19
+            <van-icon name="filter-o" size="24px" />
20
+          </template>
21
+        </van-popover>
22
+      </template>
23
+    </van-nav-bar>
24
+
25
+    <van-search v-model="query.name" show-action placeholder="请输入学习任务名称" @search="onRefresh"
26
+                @cancel="handdelect" />
27
+    <!-- 项目列表 -->
28
+    <van-pull-refresh v-model="isRefreshing" success-text="刷新成功" @refresh="onRefresh">
29
+      <van-list v-model:loading="isLoading" :finished="isFinished" finished-text="没有更多了" offset="200" @load="onLoad">
30
+        <div v-for="(item, idx) in resultData" :key="item.id">
31
+          <van-swipe-cell title-style="color: #007aff" style="height: 80px;" :ref="el => getSwipeCellRef(el, idx)">
32
+            <template #default>
33
+              <div class="swipe-cell-default">
34
+                <van-cell style="height: 100%; display: flex; align-items: center;">
35
+                  <template #title>
36
+                    <div class="cell-title">
37
+                      {{ item.projectName }}
38
+                    </div>
39
+                  </template>
40
+                  <template #label>
41
+                    <div> 培训学时:{{item.trainHours}}  ||培训类别:{{ getProjectTypeName(item.projectType)}}</div>
42
+                    <div style="width: 112px" :class="getStatusClass(item.statusFlag)">
43
+                      状态:
44
+                      <span v-if="item.statusFlag === '1'" style="width: 200px">已完成</span>
45
+                      <span v-else-if="item.statusFlag != '1'" style="width: 200px">未完成</span>
46
+                      <span v-else>未知</span>
47
+                    </div>
48
+                  </template>
49
+                </van-cell>
50
+                <div class="swipe-cell-default-icon">
51
+                  <van-icon v-if="openStatus[idx]" name="arrow-double-left" @click.stop="openSwipe(idx)" />
52
+                  <van-icon v-else name="arrow-double-right" @click.stop="closeSwipe(idx)" />
53
+                </div>
54
+              </div>
55
+            </template>
56
+
57
+            <template #right>
58
+
59
+              <div style="display: flex; align-items: center; justify-content: flex-end; height: 100%;">
60
+                <van-button v-if="isAfter(item.toDate)" class="red-rounded-box-wide" text="已结束"/>
61
+                <van-button v-else-if="isBefore(item.fromDate)" class="red-rounded-box-wide" text="未开始"/>
62
+                <van-button v-else class="submit-button"  @click="goaddPeo(item)"  text="进入小节"/>
63
+              </div>
64
+
65
+            </template>
66
+          </van-swipe-cell>
67
+        </div>
68
+
69
+      </van-list>
70
+    </van-pull-refresh>
71
+
72
+    <!-- 删除确认弹窗 -->
73
+    <van-dialog v-model:show="deleteDialogVisible" show-cancel-button @confirm="confirmDelete">
74
+      <template #title>
75
+        <div>删除确认</div>
76
+      </template>
77
+      <div style="padding: 30px;">确定要删除该项目吗?</div>
78
+    </van-dialog>
79
+
80
+  </div>
81
+</template>
82
+
83
+<script setup>
84
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, toRaw } from 'vue';
85
+import { Dialog, showDialog, showSuccessToast, showToast, Toast } from 'vant';
86
+
87
+const { proxy } = getCurrentInstance();
88
+
89
+// 将字符串转为 Date 对象进行比较
90
+const parseDate = (str) => {
91
+  if (!str) return null;
92
+  // 支持 "YYYY-MM-DD HH:mm:ss" 或 ISO 格式
93
+  return new Date(str);
94
+};
95
+
96
+// 当前时间是否在 fromDate 之前(未开始)
97
+const isBefore = (fromDateStr) => {
98
+  const now = new Date();
99
+  const fromDate = parseDate(fromDateStr);
100
+  return fromDate && now < fromDate;
101
+};
102
+
103
+// 当前时间是否在 toDate 之后(已结束)
104
+const isAfter = (toDateStr) => {
105
+  const now = new Date();
106
+  const toDate = parseDate(toDateStr);
107
+  return toDate && now > toDate;
108
+};
109
+
110
+const getProjectTypeName = (code) => {
111
+  const map = {
112
+    'Gbm': '安全管理',
113
+    'Jja': '生产安全技术',
114
+    'Jjb': '消防安全技术',
115
+    'Jjc': '安全保卫技术',
116
+    'Jjd': '特种作业安全技术',
117
+
118
+  };
119
+  return map[code] || '未知类型';
120
+}
121
+const onClickLeft = () => {
122
+  history.back();
123
+};
124
+const headers = ref({
125
+  token: localStorage.getItem('token'),
126
+  userId: localStorage.getItem('userId'),
127
+  dept: JSON.parse(localStorage.getItem('dept'))[0].deptCode
128
+});
129
+const switchIconState = (idx) => {
130
+  openStatus.value[idx] = !openStatus.value[idx]
131
+  openStatus.value = new Array(resultData.value.length).fill(true);
132
+}
133
+
134
+// const onClickRight = () =>{
135
+//   searchShow.value = !searchShow.value;
136
+// }
137
+
138
+const searchShow = ref(false);
139
+
140
+/**
141
+ 时间
142
+ */
143
+// 当前年份
144
+const currentYear = new Date().getFullYear();
145
+
146
+// 生成年份:往前3年,往后1年 → 共5年
147
+const yearRange = Array.from({ length: 5 }, (_, i) => currentYear - 3 + i);
148
+
149
+// 构造 actions(符合 van-popover 要求)
150
+const actions = yearRange.map(year => ({
151
+  text: `${year}年`
152
+}));
153
+
154
+// 默认选中的年份(用于请求)
155
+const selectedYear = ref(currentYear);
156
+
157
+// 控制 popover 显示(可选,你用了 v-model:show 就够了)
158
+const showPopover = ref(false);
159
+
160
+// 选择回调
161
+const onSelect = (action, index) => {
162
+  query.value.year = yearRange[index];
163
+  // 👇 触发刷新(带上 selectedYear)
164
+  console.log(selectedYear.value);
165
+  resetAndRefresh();
166
+};
167
+const query = ref({
168
+  year: currentYear, // ← 默认就是今年
169
+  name: '',
170
+});
171
+
172
+// 重置并刷新(你已有类似逻辑)
173
+const resetAndRefresh = () => {
174
+  isFinished.value = false;
175
+  isLoading.value = false;
176
+
177
+  resultData.value = [];
178
+  onRefresh();
179
+};
180
+
181
+function formatDate(date, format) {
182
+  const year = date.getFullYear();
183
+  const month = date.getMonth() + 1;
184
+  const day = date.getDate();
185
+  const hours = date.getHours();
186
+  const minutes = date.getMinutes();
187
+  const seconds = date.getSeconds();
188
+
189
+  return format
190
+    .replace('yyyy', year)
191
+    .replace('MM', month.toString().padStart(2, '0'))
192
+    .replace('dd', day.toString().padStart(2, '0'))
193
+    .replace('HH', hours.toString().padStart(2, '0'))
194
+    .replace('mm', minutes.toString().padStart(2, '0'))
195
+    .replace('ss', seconds.toString().padStart(2, '0'));
196
+}
197
+
198
+const tableData = ref([]);
199
+const selectedRows = ref([]);
200
+const dialogVisibleLook = ref(false);
201
+const deleteDialogVisible = ref(false);
202
+const currentDeleteItem = ref([]);
203
+const dialogVisible = ref(false);
204
+const dialogVisibleFile = ref(false);
205
+const date = ref(null);
206
+
207
+const kz = ref(true);
208
+import { useRouter } from 'vue-router';
209
+const router = useRouter();
210
+const handAdd =  () => {
211
+
212
+  router.push({ path: "/checkList",
213
+    query: {
214
+      mark:-1
215
+    } });
216
+
217
+};
218
+const goaddPeo = (item) => {
219
+  router.push({
220
+    path: '/sectionList',
221
+    query: {
222
+      data: JSON.stringify(item)
223
+    }
224
+  })
225
+}
226
+
227
+const edits = (row) => {
228
+  kz.value = true;
229
+  form.value = { ...row };
230
+  router.push({ path: "/checkList",
231
+    query: {
232
+      mark:1,
233
+      data:JSON.stringify(form.value)
234
+    } });
235
+};
236
+// 定义表单数据
237
+const form = ref({
238
+  hdPicId: '',
239
+  hdId: '',
240
+  hdType: '',
241
+  discoveryTime: '',
242
+  hdSubtype: '',
243
+  discoverer: '',
244
+  discovererOther: '',
245
+  hdDescription: '',
246
+  hdLevel: '',
247
+  bz: '',
248
+  hdLocation: '',
249
+  picBefore: '',
250
+
251
+  equipmentId: '',
252
+  hdmanageLevel: '',
253
+  handlingProcesses: '',
254
+  companyId: '',
255
+  repairLeader: '',
256
+  repairOther: '',
257
+  repairSuggest: '',
258
+  repairDdl: '',
259
+  repairDept: '',
260
+  acceptLeader: '',
261
+  acceptOther: '',
262
+  picAfter: '',
263
+  picTemp: '',
264
+  repairDescription: '',
265
+  discovererDept: '',
266
+  discovererDeptCode: '',
267
+  hdLocationCode: '',
268
+  hdLocationName: '',
269
+
270
+  status: '',
271
+
272
+
273
+  hdSelect: '正常登记',
274
+  id: ''
275
+});
276
+const resetForma = () => {
277
+  form.value = {
278
+    hdPicId: '',
279
+    hdId: '',
280
+    hdType: '',
281
+    discoveryTime: '',
282
+    hdSubtype: '',
283
+    discoverer: '',
284
+    discovererOther: '',
285
+    hdDescription: '',
286
+    hdLevel: '',
287
+    bz: '',
288
+    hdLocation: '',
289
+    picBefore: '',
290
+
291
+    equipmentId: '',
292
+    hdmanageLevel: '',
293
+    handlingProcesses: '',
294
+    companyId: '',
295
+    repairLeader: '',
296
+    repairOther: '',
297
+    repairSuggest: '',
298
+    repairDdl: '',
299
+    repairDept: '',
300
+    acceptLeader: '',
301
+    acceptOther: '',
302
+    picAfter: '',
303
+    picTemp: '',
304
+    repairDescription: '',
305
+    status: '',
306
+    discovererDept: '',
307
+    discovererDeptCode: '',
308
+    hdLocationCode: '',
309
+    hdLocationName: '',
310
+
311
+    hdSelect: '正常登记',
312
+    id: ''
313
+  };
314
+};
315
+
316
+const isRefreshing = ref(false);
317
+const isLoading = ref(false);
318
+const isFinished = ref(false);
319
+const currentPage = ref(1);
320
+const pageSize = ref(10);
321
+const totalRows = ref(0);
322
+const resultData = ref([]);
323
+
324
+const dept=localStorage.getItem("dept")[0].deptCode;
325
+const getTableData = async () => {
326
+  query.value.addDeptCode=dept
327
+  const url = 'sgsafe/Class/queryClassUserSignboard';
328
+  const param = {
329
+    page: currentPage.value,
330
+    rows: pageSize.value,
331
+    params: JSON.stringify(query.value)
332
+  };
333
+  const response = await proxy.$axios.get(url, param);
334
+  if (response.data.code === 0) {
335
+    tableData.value = response.data.data.records;
336
+    totalRows.value = response.data.data.total;
337
+  } else {
338
+    showToast({
339
+      type: 'error',
340
+      message: '操作失败!' + response.data.msg
341
+    });
342
+  }
343
+};
344
+const ruleIds = ref([]);
345
+const getRuleId = () => {
346
+  var url = '/sgsafe/ExamHead/getCheckRuleId'
347
+  var param = {}
348
+  proxy.$axios.get(url, param).then(response => {
349
+    if (response.data.code == '0') {
350
+      ruleIds.value = response.data.data
351
+    } else {
352
+      console.log("1111111");
353
+    }
354
+  })
355
+  console.log('ruleIds', ruleIds)
356
+}
357
+
358
+const onRefresh = () => {
359
+  basicReset();
360
+  onLoad();
361
+};
362
+
363
+const onLoad = async () => {
364
+  if (isRefreshing.value) {
365
+    resultData.value = [];
366
+    currentPage.value = 1;
367
+    isRefreshing.value = false;
368
+  }
369
+  try {
370
+    await getTableData();
371
+    await getRuleId()
372
+    if (pageSize.value * currentPage.value < totalRows.value) {
373
+      resultData.value = [...resultData.value, ...tableData.value];
374
+      openStatus.value = new Array(resultData.value.length).fill(true);
375
+      currentPage.value++;
376
+
377
+    } else {
378
+      resultData.value = [...resultData.value, ...tableData.value];
379
+      openStatus.value = new Array(resultData.value.length).fill(true);
380
+      isFinished.value = true;
381
+    }
382
+  } catch (error) {
383
+    console.log(error);
384
+    isFinished.value = true;
385
+  } finally {
386
+    isLoading.value = false;
387
+  }
388
+};
389
+/* 通用方法: 重置list数据 */
390
+const basicReset = () => {
391
+  isFinished.value = false;
392
+  isLoading.value = true;
393
+  currentPage.value = 1;
394
+  resultData.value = [];
395
+};
396
+
397
+/*onMounted(() => {
398
+  handleSearch();
399
+});
400
+
401
+const handleSearch = () => {
402
+/!*  currentPage.value = 1;
403
+  isFinished.value = false;
404
+  tableData.value = [];*!/
405
+  basicReset()
406
+  onLoad()
407
+};*/
408
+
409
+const handdelect = () => {
410
+  query.value.checkName = '';
411
+  onRefresh()
412
+};
413
+
414
+const handleDetailLook = (row) => {
415
+  form.value = { ...row };
416
+  proxy.$router.push({
417
+    name: 'taiZhang_detail',
418
+    query: {
419
+      form: form.value.id
420
+    }
421
+  });
422
+  // dialogVisibleLook.value = true;
423
+};
424
+const deleteData=ref({})
425
+
426
+const handleDelete = (item) => {
427
+  deleteData.value=item
428
+  deleteData.value.cancelFlag='1'
429
+  var url = '/sgsafe/EduCheckMaster/save';
430
+  var param = {
431
+    json: JSON.stringify(item)
432
+  };
433
+  proxy.$axios.post(url, param).then(response => {
434
+    if (response.data.code == '0') {
435
+      showSuccessToast("删除成功")
436
+      onRefresh();
437
+
438
+    } else {
439
+    }
440
+
441
+  })
442
+};
443
+
444
+
445
+const confirmDelete = () => {
446
+  for (let item of currentDeleteItem.value) {
447
+    if (item.addId !== headers.value.userId) {
448
+      showToast({
449
+        type: 'warning',
450
+        message: '只能删除自己添加的数据!'
451
+      });
452
+      return;
453
+    }
454
+  }
455
+
456
+  if (currentDeleteItem.value[0].status !== '0' && currentDeleteItem.value[0].hdSelect !== '下发隐患'
457
+    && currentDeleteItem.value[0].hdSelect !== '即查即改') {
458
+    showToast({
459
+      type: 'fail',
460
+      message: '只有尚未提交流程的记录或回到起点的流程经过作废后才可以删除!'
461
+    });
462
+    return;
463
+  }
464
+  if (currentDeleteItem.value[0].status !== '2' && currentDeleteItem.value[0].hdSelect === '下发隐患'
465
+    && currentDeleteItem.value[0].hdSelect !== '即查即改') {
466
+    showToast({
467
+      type: 'fail',
468
+      message: '只有尚未提交流程的记录或回到起点的流程经过作废后才可以删除!'
469
+    });
470
+    return;
471
+  }
472
+
473
+  var url = 'sgsafe/Hiddendanger/remove';
474
+  var param = {
475
+    params: JSON.stringify({ ...currentDeleteItem.value.map(x => x.id) })
476
+  };
477
+  proxy.$axios.get(url, param).then(response => {
478
+    if (response.data.code == 0) {
479
+      showToast({
480
+        type: 'success',
481
+        message: '删除成功'
482
+      });
483
+      onRefresh();
484
+    } else {
485
+      showToast({
486
+        type: 'fail',
487
+        message: '操作失败!' + response.data.msg
488
+      });
489
+    }
490
+  });
491
+};
492
+
493
+
494
+
495
+
496
+const resetForm = () => {
497
+  form.value = {
498
+    projectName: '',
499
+    projectLeader: '',
500
+    phone: '',
501
+    dept: ''
502
+  };
503
+};
504
+
505
+const baocun = () => {
506
+  nextTick(() => {
507
+    if (form.value.hdSelect === '正常登记') {
508
+      form.value.status = '0';
509
+    } else if (form.value.hdSelect === '下发隐患') {
510
+      form.value.status = '2';
511
+    } else {
512
+      form.value.status = '-1';
513
+    }
514
+  });
515
+  // 原有保存逻辑保持不变
516
+  var url = 'sgsafe/Hiddendanger/save';
517
+  var param = {
518
+    json: JSON.stringify(form.value)
519
+  };
520
+  proxy.$axios.post(url, param).then(response => {
521
+    if (response.data.code == '0') {
522
+      showSuccessToast('保存成功!');
523
+      getTableData();
524
+      orJsons();
525
+      // clearDeptUsers()
526
+    } else {
527
+      showToast({
528
+        type: 'fail',
529
+        message: '操作失败!' + response.data.msg
530
+      });
531
+    }
532
+  });
533
+};
534
+
535
+//处理人员code
536
+const repairLL = ref('');
537
+const repairOO = ref('');
538
+const acceptLL = ref('');
539
+const orJsons = () => {
540
+  // console.log('forms',form.value)
541
+  if (form.value.hdSelect === '正常登记') {
542
+    nextTick(() => {
543
+      nextTick(() => {
544
+        repairLL.value = qq('repairLL', form.value.discoverer);//隐患发现人
545
+        nextTick(() => {
546
+          repairOO.value = qq('repairOO', form.value.discovererOther);//其他隐患发现人
547
+          nextTick(() => {
548
+            acceptLL.value = qq('acceptLL', form.value.discoverer);//隐患销号人
549
+          });
550
+        });
551
+      });
552
+
553
+      // acceptOO.value = qq('acceptOO', form.value.acceptOther)
554
+    });
555
+  } else {
556
+    // console.log('noiajdoifjpoewjfopjp')
557
+    nextTick(() => {
558
+      nextTick(() => {
559
+        repairLL.value = qq('repairLL', form.value.acceptLeader);//隐患发现人
560
+        nextTick(() => {
561
+          repairOO.value = qq('repairOO', form.value.acceptOther);//其他隐患发现人
562
+          nextTick(() => {
563
+            acceptLL.value = qq('acceptLL', form.value.discoverer);//隐患销号人
564
+          });
565
+        });
566
+      });
567
+
568
+      // acceptOO.value = qq('acceptOO', form.value.acceptOther)
569
+    });
570
+  }
571
+};
572
+
573
+const jsons = ref({});
574
+const qq = (a, val) => {
575
+  let aa = '';
576
+  var url = 'sgsafe/Hiddendanger/qqId';
577
+  var param = {
578
+    params: val
579
+  };
580
+  proxy.$axios.post(url, param).then(response => {
581
+    if (response.data.code == 0) {
582
+
583
+      aa = response.data.data;
584
+      switch (a) {
585
+        case 'repairLL':
586
+          repairLL.value = response.data.data;
587
+          // console.log('repairLL',repairLL.value);
588
+          break;
589
+        case 'repairOO':
590
+          repairOO.value = response.data.data;
591
+          // console.log('repairOO',repairLL.value);
592
+          break;
593
+        case 'acceptLL':
594
+          acceptLL.value = response.data.data;
595
+          // console.log('acceptLL',repairLL.value);
596
+          break;
597
+        default:
598
+          break;
599
+      }
600
+      jsons.value = {
601
+        hdConfirm: repairLL.value,
602
+        hdConfirmO: repairOO.value,
603
+        hdCancel: acceptLL.value
604
+      };
605
+
606
+      // 处理函数
607
+      function processValue(value) {
608
+        // 将逗号替换为分号
609
+        const replacedValue = value.replace(/,/g, ';');
610
+        // 分割值
611
+        const parts = replacedValue.split(';');
612
+        // 每个部分前加上 U_
613
+        const processedParts = parts.map(part => `U_${part.trim()}`);
614
+        // 重新组合
615
+        return processedParts.join(';');
616
+      }
617
+
618
+      // 处理整个对象
619
+      const processedData = {};
620
+      for (const key in jsons.value) {
621
+        if (jsons.value.hasOwnProperty(key)) {
622
+          processedData[key] = processValue(jsons.value[key]);
623
+        }
624
+      }
625
+
626
+      console.log('对象', toRaw(processedData));
627
+
628
+      let b = {
629
+        acceptL: processedData.hdConfirm,
630
+        acceptO: processedData.hdConfirmO,
631
+        id: form.value.id
632
+      };
633
+
634
+      if (form.value.hdSelect === '即查即改') {
635
+        b = {
636
+          hdFxr: processedData.hdCancel,
637
+          id: form.value.id
638
+        };
639
+      }
640
+
641
+      if (form.value.hdSelect === '正常登记') {
642
+        b = {
643
+          // hdConfirm: processedData.hdConfirm,
644
+          // hdConfirmO: processedData.hdConfirmO,
645
+          id: form.value.id
646
+        };
647
+      }
648
+
649
+      const aaa = JSON.stringify(toRaw(b));
650
+      sessionStorage.setItem('variables', aaa);
651
+      console.log('aaa', aaa);
652
+    } else {
653
+      showToast({
654
+        type: 'fail',
655
+        message: '操作失败!' + response.data.msg
656
+      });
657
+    }
658
+  });
659
+  return aa;
660
+};
661
+
662
+const reback = () => {
663
+  // 返回逻辑
664
+};
665
+
666
+const deleteRow = (row) => {
667
+  selectedRows.value = [row];
668
+  handleDelete(row);
669
+};
670
+
671
+const deleteRowa = (row) => {
672
+  deleteRow(row);
673
+};
674
+
675
+const bm = (val) => {
676
+  // 部门选择逻辑
677
+};
678
+
679
+//提交审批流程
680
+import { workflowSubmit, workflowCancel } from '@/tools/workflow.js';
681
+
682
+const flowId = ref('');
683
+flowId.value = 'hazardManagementFlowId';
684
+const handleSubmit2 = (val, idx) => {
685
+  openStatus.value[idx] = !openStatus.value[idx]
686
+  openStatus.value = new Array(resultData.value.length).fill(true);
687
+  console.log('提交');
688
+  console.log('selectedRows', selectedRows.value);
689
+  let row = val;
690
+  form.value = { ...row }
691
+  form.value.workCreate = headers.value.dept;
692
+
693
+  let b = {
694
+    id: form.value.id,
695
+  }
696
+  const aaa = JSON.stringify(toRaw(b))
697
+  sessionStorage.setItem('variables', aaa)
698
+
699
+  if (form.value.hdSelect === '正常登记') {
700
+    flowId.value = 'hazardManagementFlowId'
701
+  } else if (form.value.hdSelect === '下发隐患') {
702
+    flowId.value = 'hazardImmediatelyCM'
703
+    form.value.status = '2'
704
+  } else {
705
+    flowId.value = 'hazardImmediatelyCMUpdate'
706
+    form.value.status = '-1'
707
+  }
708
+
709
+  console.log('----');
710
+  console.log(flowId.value);
711
+  console.log(sessionStorage.getItem('variables'));
712
+  console.log(row.workId);
713
+  console.log(row.trackId);
714
+  let titles = '隐患排查治理'
715
+  showDialog({
716
+      title: '提示',
717
+      message: '确定提交审批?',
718
+      showCancelButton: true,
719
+      confirmButtonText: '确定',
720
+      type: 'warning',
721
+      cancelButtonText: '取消'
722
+    }
723
+  ).then(() => {
724
+    return workflowSubmit(
725
+      flowId.value,
726
+      '隐患排查治理',
727
+      '初始化提交',
728
+      // JSON.stringify({}),
729
+      sessionStorage.getItem('variables'),
730
+      row.workId,
731
+      row.trackId);
732
+  }).then((result) => {
733
+    if (result.status === 'success') {
734
+      // 将结果返回的workId和trackId保存
735
+      var url = 'sgsafe/Hiddendanger/saveProcessInfo';
736
+      console.log('id', result, row.id);
737
+      var process = {
738
+        'id': form.value.id,
739
+        'workId': result.workId,
740
+        'trackId': result.trackId
741
+      };
742
+      var param = {
743
+        json: JSON.stringify(process)
744
+      };
745
+      proxy.$axios.post(url, param).then(response => {
746
+        if (response.data.code === 0) {
747
+          form.value = response.data.data;
748
+          console.log('我要进来啦保存成功');
749
+          showToast({
750
+            type: 'success',
751
+            message: '提交审批成功'
752
+          });
753
+          onRefresh()
754
+        }
755
+      });
756
+    } else {
757
+      showToast({
758
+        type: 'error',
759
+        message: '提交审批失败,' + result.msg
760
+      });
761
+    }
762
+  }).catch(() => {
763
+    showToast({
764
+      type: 'info',
765
+      message: '已取消提交'
766
+    });
767
+  });
768
+};
769
+
770
+
771
+const getStatusClass = (status) => {
772
+  switch (status) {
773
+    case '1':
774
+      return 'status-rectifying';
775
+    default:
776
+      return 'status-closed';
777
+  }
778
+};
779
+
780
+/**
781
+ * 按钮实现swipe-cell滑动
782
+ */
783
+const openStatus = ref([])
784
+const swipeCellRefs = ref([])
785
+const getSwipeCellRef = (el, index) => {
786
+  if (el) {
787
+    swipeCellRefs.value[index] = el;
788
+  }
789
+}
790
+const openSwipe = (idx) => {
791
+  openStatus.value = new Array(resultData.value.length).fill(true);
792
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
793
+    openStatus.value[idx] = false
794
+    swipeCellRefs.value[idx].open('right')
795
+  }
796
+  document.addEventListener('click', handleDocumentClick)
797
+}
798
+/**
799
+ * 当点击滑动单元格时,开始监听点击事件
800
+ */
801
+const handleDocumentClick = (event) => {
802
+  openStatus.value = new Array(resultData.value.length).fill(true);
803
+}
804
+
805
+const closeSwipe = (idx) => {
806
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
807
+    openStatus.value[idx] = true
808
+    swipeCellRefs.value[idx].close()
809
+  }
810
+}
811
+
812
+</script>
813
+
814
+<style scoped>
815
+.h5-container {
816
+  width: 100%;
817
+  padding: 5px;
818
+  box-sizing: border-box;
819
+}
820
+
821
+.status-pending {
822
+  background-color: #fff3cd;
823
+  color: #856404;
824
+  padding: 2px 4px;
825
+  border-radius: 4px;
826
+}
827
+
828
+.status-registered {
829
+  background-color: #d1ecf1;
830
+  color: #0c5460;
831
+  padding: 2px 4px;
832
+  border-radius: 4px;
833
+}
834
+
835
+.status-analyzing {
836
+  background-color: #fff8e1;
837
+  color: #ff8f00;
838
+  padding: 2px 4px;
839
+  border-radius: 4px;
840
+}
841
+
842
+.status-rectifying {
843
+  background-color: #e8f5e9;
844
+  color: #2e7d32;
845
+  padding: 2px 4px;
846
+  border-radius: 4px;
847
+}
848
+
849
+.status-accepting {
850
+  background-color: #e3f2fd;
851
+  color: #1565c0;
852
+  padding: 2px 4px;
853
+  border-radius: 4px;
854
+}
855
+
856
+.status-closed {
857
+  background-color: #f8bbd0;
858
+  color: #b71c1c;
859
+  padding: 2px 4px;
860
+  border-radius: 4px;
861
+}
862
+
863
+.status-finished {
864
+  background-color: #e8eaf6;
865
+  color: #311b92;
866
+  padding: 2px 4px;
867
+  border-radius: 4px;
868
+}
869
+
870
+.status-unknown {
871
+  background-color: #efebe9;
872
+  color: #424242;
873
+  padding: 2px 4px;
874
+  border-radius: 4px;
875
+}
876
+
877
+.cell-title {
878
+  display: -webkit-box;
879
+  /* 旧版弹性盒子模型 */
880
+  -webkit-box-orient: vertical;
881
+  /* 内容垂直排列 */
882
+  -webkit-line-clamp: 2;
883
+  /* 限制显示行数 */
884
+  overflow: hidden;
885
+  /* 超出隐藏 */
886
+  text-overflow: ellipsis;
887
+  /* 省略号 */
888
+  line-height: 1.5;
889
+  /* 可选:设置行高 */
890
+  max-height: calc(1.5em * 2);
891
+  /* 可选:根据行高限制最大高度 */
892
+  font-size: 16px;
893
+  font-weight: bold;
894
+  color: #333;
895
+  /* 字号 */
896
+}
897
+
898
+.swipe-cell-default {
899
+  display: flex;
900
+  background-color: #ffffff;
901
+  justify-content: center;
902
+  align-items: center;
903
+}
904
+
905
+.swipe-cell-default-icon {
906
+  width: 60px;
907
+  display: flex;
908
+  justify-content: center;
909
+}
910
+
911
+.delete-button {
912
+  height: 100%;
913
+  border: none;
914
+  color: #ff0000;
915
+  background-image: url('@/assets/img/del.png');
916
+  background-size: auto 100%;
917
+  background-repeat: no-repeat;
918
+}
919
+
920
+.red-rounded-box-wide {
921
+  width: 80px;           /* 可容纳3个字 */
922
+  height: 36px;          /* 高度适中 */
923
+  border: 2px solid red;
924
+  border-radius: 18px;   /* 圆角 = 高度 / 2 → 完美胶囊形 */
925
+  display: flex;
926
+  align-items: center;
927
+  justify-content: center;
928
+  font-size: 14px;
929
+  color: #ee0631;
930
+  background-color: transparent;
931
+  box-sizing: border-box;
932
+}
933
+
934
+.submit-button {
935
+  height: 100%;
936
+  border: none;
937
+  color: #07c160;
938
+  background-image: url('@/assets/img/sub.png');
939
+  background-size: auto 100%;
940
+  background-repeat: no-repeat;
941
+}
942
+
943
+.subsuccess {
944
+  height: 100%;
945
+  border: none;
946
+  color: #07c160;
947
+  background-image: url('@/assets/img/sub1.png');
948
+  background-size: auto 100%;
949
+  background-repeat: no-repeat;
950
+}
951
+</style>

+ 325
- 0
src/view/dati/classOne/learning.vue ファイルの表示

@@ -0,0 +1,325 @@
1
+<template>
2
+  <div class="learning-page">
3
+    <!-- 倒计时区域 -->
4
+    <div class="countdown-timer">
5
+      <h3>⏳ 观看倒计时</h3>
6
+      <div class="time-display" :class="{ finished: remainingTime <= 0 }">
7
+        {{ formattedTime }}
8
+      </div>
9
+    </div>
10
+
11
+    <!-- 视频播放区域 -->
12
+    <div v-if="currentVideo.src" class="video-wrapper">
13
+      <h3>🎥 视频播放</h3>
14
+      <video
15
+        ref="videoRef"
16
+        :src="currentVideo.src"
17
+        controls
18
+        playsinline
19
+        webkit-playsinline
20
+        x5-playsinline
21
+        x5-video-player-type="h5"
22
+      x5-video-player-fullscreen="true"
23
+      preload="metadata"
24
+      @loadeddata="onVideoLoaded"
25
+      @error="onVideoError"
26
+      style="width: 100%; height: auto; aspect-ratio: 16/9; background: #000;"
27
+      >
28
+      您的设备不支持视频播放。
29
+      </video>
30
+
31
+      <!-- 多视频控制 -->
32
+      <div v-if="videoList.length > 1" class="video-controls">
33
+        <van-button size="small" @click="prevVideo" :disabled="currentVideoIndex === 0">
34
+          上一个
35
+        </van-button>
36
+        <span class="video-index">{{ currentVideoIndex + 1 }} / {{ videoList.length }}</span>
37
+        <van-button size="small" @click="nextVideo" :disabled="currentVideoIndex === videoList.length - 1">
38
+          下一个
39
+        </van-button>
40
+      </div>
41
+    </div>
42
+
43
+    <!-- 文档提示(H5 不支持内嵌预览,改为下载) -->
44
+    <div v-if="previewList.length > 0" class="doc-tip">
45
+      <van-notice-bar
46
+        left-icon="volume-o"
47
+        text="当前有文档资料,请点击下方按钮下载查看"
48
+        color="#1989fa"
49
+        background="#ecf9ff"
50
+      />
51
+      <div class="doc-list">
52
+        <van-button
53
+          v-for="item in previewList"
54
+          :key="item.id"
55
+          type="primary"
56
+          plain
57
+          block
58
+          style="margin-top: 8px"
59
+          @click="downloadFile(item)"
60
+        >
61
+          📄 下载 {{ item.fileName }}
62
+        </van-button>
63
+      </div>
64
+    </div>
65
+
66
+    <!-- 加载中 -->
67
+    <van-loading v-if="loading" size="24px" vertical>加载中...</van-loading>
68
+
69
+    <!-- 完成学习按钮 -->
70
+    <div v-if="remainingTime <= 0" class="completion-actions">
71
+      <van-button type="primary" block @click="goBackAndMarkComplete">
72
+        ✅ 完成学习
73
+      </van-button>
74
+    </div>
75
+  </div>
76
+</template>
77
+
78
+<script setup>
79
+import { ref, computed, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue'
80
+import { useRoute, useRouter } from 'vue-router'
81
+import { showToast } from 'vant'
82
+import { Base64 } from 'js-base64'
83
+
84
+const route = useRoute()
85
+const router = useRouter()
86
+const { proxy } = getCurrentInstance()
87
+
88
+// ========== 响应式数据 ==========
89
+const loading = ref(false)
90
+const videoList = ref([])
91
+const previewList = ref([])
92
+const currentVideoIndex = ref(0)
93
+const watchTime = ref(0) // 单位:秒
94
+const remainingTime = ref(0)
95
+const timer = ref(null)
96
+const onVideoLoaded = () => {
97
+  loading.value = false // 视频元数据加载完成,可隐藏全局 loading
98
+}
99
+
100
+const onVideoError = () => {
101
+  loading.value = false
102
+  showToast('视频加载失败,请检查网络')
103
+}
104
+// 视频引用
105
+const videoRef = ref(null)
106
+
107
+// ========== 计算属性 ==========
108
+const currentVideo = computed(() => {
109
+  return videoList.value[currentVideoIndex.value] || {}
110
+})
111
+
112
+const formattedTime = computed(() => {
113
+  const mins = Math.floor(remainingTime.value / 60).toString().padStart(2, '0')
114
+  const secs = (remainingTime.value % 60).toString().padStart(2, '0')
115
+  return `${mins}:${secs}`
116
+})
117
+
118
+// ========== 倒计时逻辑 ==========
119
+const startCountdown = () => {
120
+  if (watchTime.value <= 0) return
121
+  remainingTime.value = watchTime.value
122
+  clearInterval(timer.value)
123
+  timer.value = setInterval(() => {
124
+    if (remainingTime.value > 0) {
125
+      remainingTime.value--
126
+    } else {
127
+      clearInterval(timer.value)
128
+      showToast('观看时间结束,学习已完成!')
129
+    }
130
+  }, 1000)
131
+}
132
+
133
+// ========== 视频控制 ==========
134
+const prevVideo = () => {
135
+  if (currentVideoIndex.value > 0) {
136
+    currentVideoIndex.value--
137
+    resetVideo()
138
+  }
139
+}
140
+
141
+const nextVideo = () => {
142
+  if (currentVideoIndex.value < videoList.value.length - 1) {
143
+    currentVideoIndex.value++
144
+    resetVideo()
145
+  }
146
+}
147
+
148
+const resetVideo = () => {
149
+  if (videoRef.value) {
150
+    videoRef.value.load()
151
+    videoRef.value.play().catch(() => {})
152
+  }
153
+}
154
+
155
+const onVideoEnded = () => {
156
+  // 可选:自动播放下一个
157
+}
158
+
159
+// ========== 文件处理 ==========
160
+const downloadFile = (file) => {
161
+  const link = document.createElement('a')
162
+  link.href = file.downloadUrl
163
+  link.download = file.fileName
164
+  link.target = '_blank'
165
+  link.click()
166
+}
167
+
168
+const createPreview = (file) => {
169
+  const { id, fileName, fileType } = file
170
+  const type = fileType?.toLowerCase()
171
+  const bucket = import.meta.env.VITE_BUCKET
172
+  const originUrl = `${import.meta.env.VITE_BASE_API}/framework/Common/downloadFileS3?bucket=${bucket}&id=${id}`
173
+  const downloadUrl = `${originUrl}&fullfilename=${encodeURIComponent(fileName)}`
174
+
175
+  if (type === 'mp4') {
176
+    videoList.value.push({
177
+      id,
178
+      fileName,
179
+      src: downloadUrl
180
+    })
181
+  } else if (['pdf', 'doc', 'docx'].includes(type)) {
182
+    previewList.value.push({
183
+      id,
184
+      fileName,
185
+      downloadUrl
186
+    })
187
+  }
188
+}
189
+
190
+// ========== 数据加载(保留 proxy.$axios)==========
191
+const getFile = async (fileId) => {
192
+  loading.value = true
193
+  previewList.value = []
194
+  videoList.value = []
195
+
196
+  try {
197
+    const response = await proxy.$axios.get('framework/Common/queryFileWithValues', {
198
+      fId: fileId
199
+    })
200
+
201
+    if (response.data.code === 0) {
202
+      const data = response.data.data
203
+      if (!data || !Array.isArray(data) || data.length === 0) {
204
+        showToast('暂无文件数据,请联系管理员')
205
+        return
206
+      }
207
+
208
+      data.forEach(file => createPreview(file))
209
+
210
+      await nextTick()
211
+      if (videoList.value.length > 0) {
212
+        resetVideo()
213
+      }
214
+    } else {
215
+      showToast('获取文件失败:' + response.data.msg)
216
+    }
217
+  } catch (error) {
218
+    console.error('文件加载异常:', error)
219
+    showToast('网络错误,请重试')
220
+  } finally {
221
+    loading.value = false
222
+  }
223
+}
224
+
225
+// ========== 页面初始化 ==========
226
+onMounted(async () => {
227
+  try {
228
+    const rowData = JSON.parse(route.query.data)
229
+    const fileId = rowData.fileId
230
+    const timeInMinutes = Number(rowData.watchTime)
231
+    console.log(timeInMinutes);
232
+    if (!fileId || isNaN(timeInMinutes) || timeInMinutes <= 0) {
233
+      showToast('参数无效')
234
+      return
235
+    }
236
+
237
+    watchTime.value = Math.floor(timeInMinutes * 60)
238
+    startCountdown()
239
+
240
+    await getFile(fileId)
241
+  } catch (error) {
242
+    console.error('路由参数解析失败:', error)
243
+    showToast('页面参数错误')
244
+  }
245
+})
246
+
247
+// ========== 完成学习 ==========
248
+const goBackAndMarkComplete = async () => {
249
+  try {
250
+    const rowData = JSON.parse(route.query.data)
251
+    const taskId = rowData.id
252
+
253
+    const res = await proxy.$axios.post('/sgsafe/Class/saveStatus', {
254
+      json: taskId
255
+    })
256
+
257
+    if (res.data.code === 0) {
258
+      showToast('学习记录已保存')
259
+    } else {
260
+      showToast(res.data.msg || '保存成功')
261
+    }
262
+  } catch (error) {
263
+    console.error('保存失败:', error)
264
+    showToast('保存学习状态失败')
265
+  }
266
+
267
+  router.back()
268
+}
269
+
270
+// ========== 清理定时器 ==========
271
+onUnmounted(() => {
272
+  if (timer.value) clearInterval(timer.value)
273
+})
274
+</script>
275
+
276
+<style scoped>
277
+.learning-page {
278
+  padding: 16px;
279
+  max-width: 100%;
280
+  box-sizing: border-box;
281
+}
282
+
283
+.countdown-timer {
284
+  text-align: center;
285
+  margin-bottom: 20px;
286
+}
287
+
288
+.time-display {
289
+  font-size: 28px;
290
+  font-weight: bold;
291
+  color: #333;
292
+}
293
+
294
+.time-display.finished {
295
+  color: #ee0a24;
296
+}
297
+
298
+.video-wrapper {
299
+  margin-bottom: 20px;
300
+}
301
+
302
+.video-controls {
303
+  display: flex;
304
+  justify-content: space-between;
305
+  align-items: center;
306
+  margin-top: 12px;
307
+}
308
+
309
+.video-index {
310
+  font-size: 14px;
311
+  color: #666;
312
+}
313
+
314
+.doc-tip {
315
+  margin-top: 20px;
316
+}
317
+
318
+.doc-list {
319
+  margin-top: 12px;
320
+}
321
+
322
+.completion-actions {
323
+  margin-top: 30px;
324
+}
325
+</style>

+ 641
- 0
src/view/dati/classOne/line.vue ファイルの表示

@@ -0,0 +1,641 @@
1
+<template>
2
+  <van-sticky>
3
+    <van-nav-bar>
4
+      <!-- <template #left>
5
+        <van-icon name="arrow-left" size="18" @click="goBack" />
6
+      </template> -->
7
+      <template #title> 答题考试 </template>
8
+      <template #right>
9
+        <!-- 提交按钮 -->
10
+        <van-button @click="checkBeforeSubmit" round color="linear-gradient(to right, #18FFFF, #304FFE)"
11
+                    style="height: 30px; width: 80px">交卷</van-button>
12
+      </template>
13
+    </van-nav-bar>
14
+  </van-sticky>
15
+  <van-overlay :show="overlayloading">
16
+    <div class="wrapper">
17
+      <van-loading color="#0094ff"> 加载中... </van-loading>
18
+    </div>
19
+  </van-overlay>
20
+
21
+  <div class="quiz-page">
22
+    <div class="question-number">
23
+      {{ activeIndex + 1 }}/{{ questions.length }}
24
+    </div>
25
+    <div v-if="questions.length > 0" class="question-content">
26
+      <!-- 题干 -->
27
+      <p class="kong">
28
+        <img
29
+          :src="getQuestionTypeImage(currentQuestion.category)"
30
+          class="question-type-img"
31
+          alt=""
32
+        />
33
+        {{ currentQuestion.stem }}
34
+      </p>
35
+
36
+
37
+      <!-- 单选题 -->
38
+      <div v-if="currentQuestion.category === '单选'">
39
+        <van-radio-group v-model="userAnswers[currentQuestion.id]">
40
+          <van-radio :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-radio>
41
+          <van-radio :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-radio>
42
+          <van-radio :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-radio>
43
+          <van-radio :name="'D'" class="kong" v-if="currentQuestion.optionD">D.
44
+            {{ currentQuestion.optionD }}</van-radio>
45
+          <van-radio :name="'E'" class="kong" v-if="currentQuestion.optionE">E.
46
+            {{ currentQuestion.optionE }}</van-radio>
47
+        </van-radio-group>
48
+      </div>
49
+
50
+      <!-- 多选题 -->
51
+      <div v-if="currentQuestion.category === '多选'">
52
+        <van-checkbox-group v-model="userAnswers[currentQuestion.id]" shape="square">
53
+          <van-checkbox :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-checkbox>
54
+          <van-checkbox :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-checkbox>
55
+          <van-checkbox :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-checkbox>
56
+          <van-checkbox :name="'D'" class="kong"
57
+                        v-if="currentQuestion.optionD">D.{{ currentQuestion.optionD }}</van-checkbox>
58
+          <van-checkbox :name="'E'" class="kong"
59
+                        v-if="currentQuestion.optionE">E.{{ currentQuestion.optionE }}</van-checkbox>
60
+        </van-checkbox-group>
61
+      </div>
62
+
63
+      <!-- 判断题 -->
64
+      <div v-if="currentQuestion.category === '判断'">
65
+        <van-radio-group v-model="userAnswers[currentQuestion.id]">
66
+          <van-radio :name="'A'" class="kong">A.正确</van-radio>
67
+          <van-radio :name="'B'" class="kong">B.错误</van-radio>
68
+        </van-radio-group>
69
+      </div>
70
+    </div>
71
+
72
+    <!-- 底部固定栏 -->
73
+    <div class="footer">
74
+      <van-button @click="prevQuestion" :disabled="activeIndex === 0"
75
+                  style="height: 40px; width: 45%">上一题</van-button>
76
+      <van-button @click="nextQuestion" :disabled="activeIndex === questions.length - 1" style="
77
+          height: 40px;
78
+          width: 45%;
79
+          background-color: var(--van-radio-checked-icon-color);
80
+          border-color: var(--van-radio-checked-icon-color);
81
+          color: #fff;
82
+        ">下一题</van-button>
83
+    </div>
84
+
85
+    <!-- 提交前确认弹窗 -->
86
+    <van-dialog v-model:show="confirmSubmitDialog" title="确认交卷" show-cancel-button @confirm="submitForm">
87
+      <p :class="{ 'van-dialog__message': true, 'text-center': true }">
88
+        <span v-if="hasUnanswered">{{ unansweredText }}</span>
89
+        <span v-else>{{ completedText }}</span>
90
+      </p>
91
+    </van-dialog>
92
+
93
+    <!-- 结果弹窗 -->
94
+    <van-popup v-model:show="showResult" position="top" style="height: 100%">
95
+      <van-sticky>
96
+        <van-nav-bar title="答题结果" />
97
+      </van-sticky>
98
+      <div style="
99
+          margin-top: 10px;
100
+          margin-left: 20px;
101
+          margin-bottom: 20px;
102
+          font-weight: bold;
103
+        ">
104
+        本次得分:{{ totalScore }}
105
+        <!--取接口成绩-->
106
+      </div>
107
+      <van-divider />
108
+      <!-- 题干 -->
109
+      <div v-for="question in questions" :key="question.id" class="question">
110
+        <p>
111
+          <span v-if="question.category === '单选'">[单选]</span>
112
+          <span v-if="question.category === '多选'">[多选]</span>
113
+          <span v-if="question.category === '判断'">[判断]</span>
114
+          {{ question.stem }}
115
+        </p>
116
+        <!-- 显示提交答案 -->
117
+        <p>
118
+					<span :style="{
119
+              color: Array.isArray(userAnswers[question.id])
120
+                ? userAnswers[question.id].sort().join('') ===
121
+                  question.answer
122
+                  ? '#007aff'
123
+                  : 'red'
124
+                : userAnswers[question.id] === question.answer
125
+                ? '#007aff'
126
+                : 'red',
127
+            }">
128
+						提交答案:{{
129
+              Array.isArray(userAnswers[question.id])
130
+                ? userAnswers[question.id].sort().join("")
131
+                : userAnswers[question.id] || "未作答"
132
+            }}
133
+					</span>
134
+        </p>
135
+        <!-- 显示正确答案 -->
136
+        <p style="color: #007aff">正确答案:{{ question.answer }}</p>
137
+        <div v-if="question.category === '单选'" class="kong">
138
+          <div>A. {{ question.optionA }}</div>
139
+          <div>B. {{ question.optionB }}</div>
140
+          <div>C. {{ question.optionC }}</div>
141
+          <div v-if="question.optionD">D. {{ question.optionD }}</div>
142
+          <div v-if="question.optionE">E. {{ question.optionE }}</div>
143
+        </div>
144
+        <div v-if="question.category === '多选'" class="kong">
145
+          <div>A. {{ question.optionA }}</div>
146
+          <div>B. {{ question.optionB }}</div>
147
+          <div>C. {{ question.optionC }}</div>
148
+          <div v-if="question.optionD">D. {{ question.optionD }}</div>
149
+          <div v-if="question.optionE">E. {{ question.optionE }}</div>
150
+        </div>
151
+        <div v-if="question.category === '判断'" class="kong">
152
+          <div>A.正确</div>
153
+          <div>B.错误</div>
154
+        </div>
155
+
156
+        <!-- AI解析按钮和内容 -->
157
+        <div style="margin: 10px 0;">
158
+          <van-button
159
+            type="primary"
160
+            size="small"
161
+            :loading="analysisLoading[question.id]"
162
+            @click="generateAIAnalysis(question,true)"
163
+          >
164
+            {{ aiAnalysis[question.id] ? '重新解析' : 'AI解析' }}
165
+          </van-button>
166
+
167
+          <!-- 显示AI解析内容 -->
168
+          <div v-if="aiAnalysis[question.id]" class="ai-analysis-content">
169
+            <div v-html="renderAnalysis(aiAnalysis[question.id])"></div>
170
+          </div>
171
+        </div>
172
+
173
+        <van-divider />
174
+      </div>
175
+      <div style="margin-top: 20px; text-align: center; margin-bottom: 20px">
176
+        <van-button class="questionBtn" type="primary" @click="confirmResult">确定</van-button>
177
+      </div>
178
+    </van-popup>
179
+  </div>
180
+</template>
181
+
182
+<script setup>
183
+import {
184
+  onMounted,
185
+  ref,
186
+  getCurrentInstance,
187
+  computed
188
+} from "vue";
189
+import {
190
+  showConfirmDialog,
191
+  showSuccessToast,
192
+  showFailToast,
193
+  showDialog
194
+} from 'vant';
195
+import {
196
+  useRouter,
197
+  useRoute
198
+} from "vue-router";
199
+import {
200
+  examResult,
201
+  formNew,
202
+  myDefaultCourse,
203
+  saveScore,
204
+  sortData,
205
+} from "@/api/dati";
206
+const {
207
+  proxy
208
+} = getCurrentInstance()
209
+
210
+const router = useRouter();
211
+const route = useRoute();
212
+
213
+const courseId = route.query.courseId;
214
+const userId = route.query.userId;
215
+
216
+const todayStr = route.query.todayStr;
217
+
218
+const questions = ref([]);
219
+
220
+const userAnswers = ref({});
221
+const activeIndex = ref(0);
222
+const totalScore = ref(0);
223
+const showResult = ref(false);
224
+
225
+const confirmSubmitDialog = ref(false);
226
+const hasUnanswered = ref(false);
227
+const unansweredText = "有题目未完成,是否确认交卷?";
228
+const completedText = "已完成所有题目,是否确认交卷?";
229
+const overlayloading = ref(false);
230
+// 在组件挂载时获取试卷
231
+onMounted(async () => {
232
+  overlayloading.value = true;
233
+  await getForm();
234
+  overlayloading.value = false;
235
+});
236
+
237
+//获取试卷
238
+const getForm = async () => {
239
+  var url = '/sgsafe/ExamLine/query'
240
+  const query = ref({
241
+    headId: courseId
242
+  })
243
+  var param = {
244
+    params: JSON.stringify(query.value)
245
+  }
246
+  try {
247
+    const res = await proxy.$axios.post(url, param);
248
+    if (res.data.code === 0) {
249
+      questions.value = res.data.data
250
+    } else {
251
+      console.log('操作失败!' + res.data.msg);
252
+    }
253
+  } catch (error) {
254
+    console.log('请求出错:', questions);
255
+  }
256
+};
257
+
258
+// 获取当前题目
259
+const currentQuestion = computed(() => {
260
+  return questions.value[activeIndex.value];
261
+});
262
+
263
+// 获取题目类型对应的图片路径
264
+import danxuan from '@/assets/img/dx.svg'
265
+import duoxuanImg from '@/assets/img/ksdx.svg'
266
+import panduanImg from '@/assets/img/kspd.svg'
267
+const getQuestionTypeImage = (category) => {
268
+  switch (category) {
269
+    case "单选": // 单选
270
+      return danxuan;
271
+    case "多选": // 多选
272
+      return duoxuanImg;
273
+    case "判断": // 判断
274
+      return panduanImg;
275
+    default:
276
+      return "";
277
+  }
278
+};
279
+
280
+//返回答题首页
281
+const goBack = () => {
282
+  router.push({
283
+    path: "/dailyproblem"
284
+  });
285
+};
286
+
287
+// 切换到下一题
288
+const nextQuestion = () => {
289
+  if (activeIndex.value < questions.value.length - 1) {
290
+    activeIndex.value++;
291
+  }
292
+};
293
+
294
+// 切换到上一题
295
+const prevQuestion = () => {
296
+  if (activeIndex.value > 0) {
297
+    activeIndex.value--;
298
+  }
299
+};
300
+
301
+
302
+const getUserAnswers = () => {
303
+  let useranswers = [];
304
+  questions.value.forEach((question) => {
305
+    const userAnswer = userAnswers.value[question.id]; // 获取用户的答案
306
+    let userAnswerString;
307
+    if (Array.isArray(userAnswer)) {
308
+      // 多选题,将数组转换为字符串
309
+      userAnswerString = userAnswer.sort().join(""); // 排序并转换为字符串,如 "ABC"
310
+    } else {
311
+      // 单选题,直接是字符串
312
+      userAnswerString = userAnswer || ""; // 如果未选择答案,则设为空字符串
313
+    }
314
+    // 将答案保存到 answers 数组中
315
+    useranswers.push({
316
+      id: question.id, // 题目 ID
317
+      userAnswer: userAnswerString, // 用户的答案
318
+    });
319
+  });
320
+  return useranswers;
321
+};
322
+
323
+//交卷
324
+const submitForm = async () => {
325
+  overlayloading.value = true;
326
+  try {
327
+    // 1. 保存答案
328
+    const answers = getUserAnswers();
329
+    const saveRes = await proxy.$axios.post('/sgsafe/ExamLine/appSaveMyScore', {
330
+      json: JSON.stringify(answers)
331
+    });
332
+    if (saveRes.data.code !== 0) {
333
+      showFailToast("答案保存失败");
334
+      return;
335
+    }
336
+
337
+    // 2. 触发判卷
338
+    const gradeRes = await proxy.$axios.post('/sgsafe/Package/doProc', {
339
+      procName: 'safeplat.sxsp_grade_exam_class',
340
+      param: JSON.stringify([courseId])
341
+    });
342
+    if (gradeRes.data.code !== 0) {
343
+      showFailToast("判卷失败,请稍后重试");
344
+      return;
345
+    }
346
+    const query = ref({
347
+      headId: courseId
348
+    })
349
+
350
+    // 3. 调用 queryMistake 获取错题及得分明细
351
+    const mistakeRes = await proxy.$axios.post(
352
+      '/sgsafe/ExamLine/queryMistake',
353
+      {
354
+        params: JSON.stringify(query.value) // 注意:这里和后端参数名一致
355
+      }
356
+    );
357
+    console.log(mistakeRes.data);
358
+    if (mistakeRes.data.code != 0) {
359
+      console.log('获取答题结果失败!' + res.data.msg);
360
+      return;
361
+    }
362
+
363
+    const data = mistakeRes.data.data || [];
364
+
365
+    // 直接计算总分和用户得分(不要用 computed!)
366
+    // totalScore.value = data.reduce((sum, item) => sum + (Number(item.score) || 0), 0);
367
+   totalScore.value = data.reduce((sum, item) => sum + (Number(item.userScore) || 0), 0);
368
+
369
+    // (可选)如果后续页面需要错题列表,也可以存下来
370
+    // tableData2.value = data;
371
+
372
+    // 4. 弹出结果提示
373
+    showConfirmDialog({
374
+      message: "判卷完成",
375
+      confirmButtonText: "查看本次答题结果",
376
+      cancelButtonText: "退出答题"
377
+    })
378
+      .then(() => {
379
+        showResult.value = true; // 显示结果页(会用到 totalScore 和 userTotalScore)
380
+      })
381
+      .catch(() => {
382
+        router.back()
383
+      });
384
+
385
+  } catch (error) {
386
+    console.error("交卷过程出错:", error);
387
+    showFailToast("交卷失败,请重试");
388
+  } finally {
389
+    overlayloading.value = false;
390
+  }
391
+};
392
+// 确认结果并返回
393
+const confirmResult = () => {
394
+  showResult.value = false;
395
+  // router.back();
396
+  router.back()
397
+};
398
+
399
+// 检查是否所有题目都已作答
400
+const checkBeforeSubmit = () => {
401
+  hasUnanswered.value = questions.value.some((question) => {
402
+    const userAnswer = userAnswers.value[question.id];
403
+    return (
404
+      !userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)
405
+    );
406
+  });
407
+  confirmSubmitDialog.value = true;
408
+};
409
+
410
+// AI解析功能
411
+// AI解析相关变量
412
+const aiAnalysis = ref({}); // 存储每道题的AI解析内容
413
+const analysisLoading = ref({}); // 存储每道题的解析加载状态
414
+import { fetchHuaweiResponse } from "@/tools/deepseek.js";
415
+// 动态导入依赖
416
+let marked, DOMPurify;
417
+
418
+const initMarkdownLibs = async () => {
419
+  try {
420
+    // 尝试导入marked
421
+    const markedModule = await import('marked');
422
+    marked = markedModule.marked || markedModule.default || markedModule;
423
+
424
+    // 尝试导入DOMPurify
425
+    const dompurifyModule = await import('dompurify');
426
+    DOMPurify = dompurifyModule.default || dompurifyModule;
427
+  } catch (error) {
428
+    console.warn('Markdown libraries not available, using plain text', error);
429
+    // 如果导入失败,使用基础功能
430
+    marked = {
431
+      parse: (text) => text
432
+    };
433
+    DOMPurify = {
434
+      sanitize: (html) => html
435
+    };
436
+  }
437
+};
438
+
439
+// 在组件挂载时初始化
440
+onMounted(() => {
441
+  initMarkdownLibs();
442
+});
443
+
444
+// 生成AI解析
445
+const generateAIAnalysis = async (question, force = false) => {
446
+  // 如果该题已有解析且不是强制重新生成,直接返回
447
+  if (aiAnalysis.value[question.id] && !force) {
448
+    return;
449
+  }
450
+
451
+  // 如果是重新解析,先清空之前的内容
452
+  if (force) {
453
+    aiAnalysis.value[question.id] = '';
454
+  }
455
+
456
+  // 确保依赖已加载
457
+  if (!marked || !DOMPurify) {
458
+    await initMarkdownLibs();
459
+  }
460
+
461
+  // 设置加载状态
462
+  analysisLoading.value[question.id] = true;
463
+
464
+  try {
465
+    // 构造提示词
466
+    let prompt = `请为以下题目提供详细解析:
467
+题目类型:${question.category}题干:${question.stem}`;
468
+
469
+    // 添加选项
470
+    if (question.optionA) prompt += `\nA. ${question.optionA}`;
471
+    if (question.optionB) prompt += `\nB. ${question.optionB}`;
472
+    if (question.optionC) prompt += `\nC. ${question.optionC}`;
473
+    if (question.optionD) prompt += `\nD. ${question.optionD}`;
474
+    if (question.optionE) prompt += `\nE. ${question.optionE}`;
475
+
476
+    prompt += `\n正确答案:${question.answer}`;
477
+
478
+    // 添加用户答案(如果已作答)
479
+    const userAnswer = userAnswers.value[question.id];
480
+    if (userAnswer) {
481
+      const userAnswerString = Array.isArray(userAnswer)
482
+        ? userAnswer.sort().join("")
483
+        : userAnswer;
484
+      prompt += `\n用户答案:${userAnswerString}`;
485
+    }
486
+
487
+    prompt += `\n\n请提供以下内容:
488
+1. 正确答案的解释
489
+2. 为什么其他选项不正确(如果用户答案错误)
490
+3. 相关知识点说明`;
491
+
492
+    // 构造消息对象
493
+    const messages = [
494
+      {
495
+        role: "user",
496
+        content: prompt
497
+      }
498
+    ];
499
+
500
+    // 调用AI接口
501
+    fetchHuaweiResponse(
502
+      messages,
503
+      (content, isThinking, isEnd) => {
504
+        // 实时更新解析内容
505
+        aiAnalysis.value[question.id] = content;
506
+
507
+        // 如果是最终结果,停止加载状态
508
+        if (isEnd) {
509
+          analysisLoading.value[question.id] = false;
510
+        }
511
+      },
512
+      null
513
+    );
514
+  } catch (error) {
515
+    console.error('AI解析生成失败:', error);
516
+    analysisLoading.value[question.id] = false;
517
+    aiAnalysis.value[question.id] = '解析生成失败';
518
+  }
519
+};
520
+
521
+// 解析内容转换为HTML
522
+const renderAnalysis = (content) => {
523
+  if (!content) return '';
524
+
525
+  try {
526
+    // 确保依赖已加载
527
+    if (!marked || !DOMPurify) {
528
+      return content.replace(/\n/g, '<br>');
529
+    }
530
+
531
+    const html = marked.parse ? marked.parse(content) : marked(content);
532
+    return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
533
+  } catch (error) {
534
+    console.error('Markdown解析错误:', error);
535
+    return content.replace(/\n/g, '<br>');
536
+  }
537
+};
538
+
539
+
540
+
541
+
542
+</script>
543
+
544
+<style scoped>
545
+.quiz-page {
546
+  padding: 20px;
547
+}
548
+
549
+.question-type-img {
550
+  width: 54px;
551
+  height: 20px;
552
+}
553
+
554
+.question {
555
+  margin-left: 20px;
556
+  margin-right: 20px;
557
+}
558
+
559
+.kong {
560
+  margin-bottom: 20px;
561
+}
562
+
563
+.footer {
564
+  position: fixed;
565
+  bottom: 0;
566
+  left: 0;
567
+  width: 100%;
568
+  background-color: #fff;
569
+  display: flex;
570
+  justify-content: space-around;
571
+  align-items: center;
572
+  margin-bottom: 10px;
573
+}
574
+
575
+.van-dialog__message {
576
+  text-align: center;
577
+}
578
+
579
+.questionBtn {
580
+  width: 40%;
581
+}
582
+
583
+/* 遮罩 */
584
+.wrapper {
585
+  display: flex;
586
+  align-items: center;
587
+  justify-content: center;
588
+  height: 100%;
589
+}
590
+
591
+.van-overlay {
592
+  z-index: 2;
593
+  background-color: rgba(0, 0, 0, 0.5);
594
+}
595
+
596
+.ai-analysis-content {
597
+  margin-top: 10px;
598
+  padding: 10px;
599
+  background-color: #f5f5f5;
600
+  border-radius: 4px;
601
+  font-size: 14px;
602
+  line-height: 1.6;
603
+}
604
+
605
+.ai-analysis-content :deep(h1),
606
+.ai-analysis-content :deep(h2),
607
+.ai-analysis-content :deep(h3) {
608
+  margin: 10px 0;
609
+  font-weight: bold;
610
+  font-size: 16px;
611
+}
612
+
613
+.ai-analysis-content :deep(p) {
614
+  margin: 8px 0;
615
+}
616
+
617
+.ai-analysis-content :deep(ul),
618
+.ai-analysis-content :deep ol {
619
+  padding-left: 20px;
620
+  margin: 8px 0;
621
+}
622
+
623
+.ai-analysis-content :deep(li) {
624
+  margin: 4px 0;
625
+}
626
+
627
+.ai-analysis-content :deep(code) {
628
+  padding: 2px 4px;
629
+  background-color: #e0e0e0;
630
+  border-radius: 3px;
631
+  font-family: monospace;
632
+}
633
+
634
+.ai-analysis-content :deep(pre) {
635
+  padding: 10px;
636
+  background-color: #e0e0e0;
637
+  border-radius: 4px;
638
+  overflow-x: auto;
639
+}
640
+
641
+</style>

+ 874
- 0
src/view/dati/classOne/sectionList.vue ファイルの表示

@@ -0,0 +1,874 @@
1
+<template>
2
+  <div class="h5-container">
3
+    <van-nav-bar title="学习课程小节">
4
+
5
+      <template #right>
6
+        <van-icon
7
+          name="add"
8
+          size="24"
9
+          color="var(--van-nav-bar-icon-color)"
10
+          @click="handAdd"
11
+        />
12
+        <van-popover
13
+          v-model:show="showPopover"
14
+          :actions="actions"
15
+          placement="bottom-end"
16
+          @select="onSelect"
17
+        >
18
+          <template #reference>
19
+            <van-icon name="filter-o" size="24px" />
20
+          </template>
21
+        </van-popover>
22
+      </template>
23
+    </van-nav-bar>
24
+
25
+
26
+    <!-- 项目列表 -->
27
+    <van-pull-refresh v-model="isRefreshing" success-text="刷新成功" @refresh="onRefresh">
28
+      <van-list v-model:loading="isLoading" :finished="isFinished" finished-text="没有更多了" offset="200" @load="onLoad">
29
+        <div v-for="(item, idx) in resultData" :key="item.id">
30
+          <van-swipe-cell title-style="color: #007aff" style="height: 80px;" :ref="el => getSwipeCellRef(el, idx)">
31
+            <template #default>
32
+              <div class="swipe-cell-default">
33
+                <van-cell style="height: 100%; display: flex; align-items: center;" >
34
+                  <template #title>
35
+                    <div class="cell-title">
36
+                      {{ item.sectionName }}
37
+                    </div>
38
+                  </template>
39
+                  <template #label>
40
+                    <div> 小节类型:{{ getProjectTypeName(item.studyorExam)}} </div>
41
+                    <div style="width: 112px" :class="getStatusClass(item.isFinish)">
42
+                      状态:
43
+                      <span v-if="item.isFinish == '1'" style="width: 200px">已完成</span>
44
+                      <span v-else-if="item.isFinish == '2'" style="width: 200px">不及格</span>
45
+                      <span v-else>未完成</span>
46
+                    </div>
47
+                  </template>
48
+                </van-cell>
49
+                <div class="swipe-cell-default-icon">
50
+                  <van-icon v-if="openStatus[idx]" name="arrow-double-left" @click.stop="openSwipe(idx)" />
51
+                  <van-icon v-else name="arrow-double-right" @click.stop="closeSwipe(idx)" />
52
+                </div>
53
+              </div>
54
+            </template>
55
+
56
+            <template #right>
57
+              <div style="display: flex; align-items: center; justify-content: flex-end; height: 100%;">
58
+                <van-button v-if="item.studyorExam =='study'" @click="goaddLearn(item)" class="red-rounded-box-wide" text="学习"/>
59
+                <van-button
60
+                  v-if="item.isFinish === '1'"
61
+                  @click="openWrongRecord(item)"
62
+                  class="red-rounded-box-wide"
63
+                  text="错题集"
64
+                />
65
+                <van-button v-else-if="item.studyorExam =='exam'"  @click="goaddPeo(item)" class="red-rounded-box-wide" text="考试"/>
66
+              </div>
67
+            </template>
68
+          </van-swipe-cell>
69
+        </div>
70
+
71
+      </van-list>
72
+    </van-pull-refresh>
73
+    <QuizResultPopup
74
+      v-model:show="showWrongPopup"
75
+      :course-id="selectedCourseId"
76
+      :user-id="userId"
77
+    />
78
+  </div>
79
+</template>
80
+
81
+<script setup>
82
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, toRaw } from 'vue';
83
+import { Dialog, showDialog, showSuccessToast, showToast, Toast } from 'vant';
84
+import QuizResultPopup from '@/components/QuizResultPopup.vue'; // 路径按你实际的改
85
+const { proxy } = getCurrentInstance();
86
+
87
+
88
+
89
+
90
+
91
+const route = useRoute()
92
+const rouData =ref({})
93
+rouData.value = JSON.parse(route.query.data)
94
+
95
+
96
+
97
+// 将字符串转为 Date 对象进行比较
98
+const parseDate = (str) => {
99
+  if (!str) return null;
100
+  // 支持 "YYYY-MM-DD HH:mm:ss" 或 ISO 格式
101
+  return new Date(str);
102
+};
103
+
104
+
105
+// 当前时间是否在 fromDate 之前(未开始)
106
+const isBefore = (fromDateStr) => {
107
+  const now = new Date();
108
+  const fromDate = parseDate(fromDateStr);
109
+  return fromDate && now < fromDate;
110
+};
111
+
112
+// 当前时间是否在 toDate 之后(已结束)
113
+const isAfter = (toDateStr) => {
114
+  const now = new Date();
115
+  const toDate = parseDate(toDateStr);
116
+  return toDate && now > toDate;
117
+};
118
+
119
+
120
+
121
+
122
+const getProjectTypeName = (code) => {
123
+  const map = {
124
+    'exam': '考试',
125
+    'study': '学习',
126
+
127
+
128
+  };
129
+  return map[code] || '未知类型';
130
+}
131
+const onClickLeft = () => {
132
+  history.back();
133
+};
134
+
135
+var userId = localStorage.getItem('userId')
136
+const switchIconState = (idx) => {
137
+  openStatus.value[idx] = !openStatus.value[idx]
138
+  openStatus.value = new Array(resultData.value.length).fill(true);
139
+}
140
+
141
+// const onClickRight = () =>{
142
+//   searchShow.value = !searchShow.value;
143
+// }
144
+
145
+const searchShow = ref(false);
146
+
147
+/**
148
+ 时间
149
+ */
150
+// 当前年份
151
+const currentYear = new Date().getFullYear();
152
+
153
+// 生成年份:往前3年,往后1年 → 共5年
154
+const yearRange = Array.from({ length: 5 }, (_, i) => currentYear - 3 + i);
155
+
156
+// 构造 actions(符合 van-popover 要求)
157
+const actions = yearRange.map(year => ({
158
+  text: `${year}年`
159
+}));
160
+
161
+// 默认选中的年份(用于请求)
162
+const selectedYear = ref(currentYear);
163
+
164
+// 控制 popover 显示(可选,你用了 v-model:show 就够了)
165
+const showPopover = ref(false);
166
+
167
+// 选择回调
168
+const onSelect = (action, index) => {
169
+  query.value.year = yearRange[index];
170
+  // 👇 触发刷新(带上 selectedYear)
171
+  console.log(selectedYear.value);
172
+  resetAndRefresh();
173
+};
174
+const query = ref({
175
+  year: currentYear, // ← 默认就是今年
176
+  name: '',
177
+});
178
+
179
+// 重置并刷新(你已有类似逻辑)
180
+const resetAndRefresh = () => {
181
+  isFinished.value = false;
182
+  isLoading.value = false;
183
+
184
+  resultData.value = [];
185
+  onRefresh();
186
+};
187
+
188
+function formatDate(date, format) {
189
+  const year = date.getFullYear();
190
+  const month = date.getMonth() + 1;
191
+  const day = date.getDate();
192
+  const hours = date.getHours();
193
+  const minutes = date.getMinutes();
194
+  const seconds = date.getSeconds();
195
+
196
+  return format
197
+    .replace('yyyy', year)
198
+    .replace('MM', month.toString().padStart(2, '0'))
199
+    .replace('dd', day.toString().padStart(2, '0'))
200
+    .replace('HH', hours.toString().padStart(2, '0'))
201
+    .replace('mm', minutes.toString().padStart(2, '0'))
202
+    .replace('ss', seconds.toString().padStart(2, '0'));
203
+}
204
+
205
+const tableData = ref([]);
206
+const selectedRows = ref([]);
207
+const dialogVisibleLook = ref(false);
208
+const deleteDialogVisible = ref(false);
209
+const currentDeleteItem = ref([]);
210
+const dialogVisible = ref(false);
211
+const dialogVisibleFile = ref(false);
212
+const date = ref(null);
213
+
214
+const kz = ref(true);
215
+import { useRoute, useRouter } from 'vue-router';
216
+const router = useRouter();
217
+
218
+const handAdd =  () => {
219
+
220
+  router.push({ path: "/checkList",
221
+    query: {
222
+      mark:-1
223
+    } });
224
+
225
+};
226
+const today = new Date();
227
+const year = today.getFullYear();
228
+const month = String(today.getMonth() + 1).padStart(2, '0');
229
+const day = String(today.getDate()).padStart(2, '0');
230
+const todayStr = `${year}-${month}-${day}`;
231
+const goaddPeo =async (item) => {
232
+  //在这的时候生成题目
233
+  await prepareQuizData(item.id)
234
+
235
+  router.push({
236
+    path: "/line",
237
+    query: {
238
+      courseId: item.id,
239
+      userId: userId,
240
+      todayStr: todayStr,
241
+
242
+    },
243
+  });
244
+}
245
+const goaddLearn =async (item) => {
246
+  router.push({
247
+    path: '/learning1',
248
+    query: {
249
+      data: JSON.stringify(item)
250
+    }
251
+  })
252
+}
253
+
254
+const showWrongPopup = ref(false);
255
+const selectedCourseId = ref('');
256
+const openWrongRecord = (item) => {
257
+  selectedCourseId.value = item.id; // 或 item.courseId,看你字段名
258
+  showWrongPopup.value = true;
259
+};
260
+
261
+const openWrongQuestions = async (item) => {
262
+  // 1. 先获取该小节的所有题目(和你 goaddPeo 里 prepareQuizData 一样)
263
+  await prepareQuizData(item.id);
264
+
265
+  // 2. 模拟或获取用户的答题记录(你可能需要调另一个接口)
266
+  //    假设你已经有 userAnswers 数据结构,或者可以从本地/接口获取
267
+  //    这里我们假设有一个 getUserAnswers(sectionId) 方法
268
+
269
+  const userAnswers = await getUserAnswers(item.id); // ← 你需要实现这个!
270
+
271
+  // 3. 把题目 + 用户答案 + 得分等传给弹窗
272
+  currentSectionWrongData.value = {
273
+    questions: questionData.value,
274
+    userAnswers: userAnswers,
275
+    totalScore: calculateScore(questionData.value, userAnswers), // 你自己的评分逻辑
276
+    sectionName: item.sectionName
277
+  };
278
+
279
+  // 4. 显示弹窗
280
+  showWrongQuestionsPopup.value = true;
281
+};
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+const  questionData=ref([])
291
+const prepareQuizData = async (sectionId) => {
292
+
293
+  var url = '/sgsafe/Class/getQuestionBysectionId'
294
+  var param = {
295
+
296
+    params: sectionId
297
+  }
298
+  const response = await proxy.$axios.get(url, param)
299
+  if (response.data.code == '0') {
300
+    questionData.value = response.data.data
301
+    console.log(questionData.value)
302
+
303
+  }
304
+
305
+}
306
+
307
+const edits = (row) => {
308
+  kz.value = true;
309
+  form.value = { ...row };
310
+  router.push({ path: "/checkList",
311
+    query: {
312
+      mark:1,
313
+      data:JSON.stringify(form.value)
314
+    } });
315
+};
316
+// 定义表单数据
317
+const form = ref({
318
+  hdPicId: '',
319
+  hdId: '',
320
+  hdType: '',
321
+  discoveryTime: '',
322
+  hdSubtype: '',
323
+  discoverer: '',
324
+  discovererOther: '',
325
+  hdDescription: '',
326
+  hdLevel: '',
327
+  bz: '',
328
+  hdLocation: '',
329
+  picBefore: '',
330
+
331
+  equipmentId: '',
332
+  hdmanageLevel: '',
333
+  handlingProcesses: '',
334
+  companyId: '',
335
+  repairLeader: '',
336
+  repairOther: '',
337
+  repairSuggest: '',
338
+  repairDdl: '',
339
+  repairDept: '',
340
+  acceptLeader: '',
341
+  acceptOther: '',
342
+  picAfter: '',
343
+  picTemp: '',
344
+  repairDescription: '',
345
+  discovererDept: '',
346
+  discovererDeptCode: '',
347
+  hdLocationCode: '',
348
+  hdLocationName: '',
349
+
350
+  status: '',
351
+
352
+
353
+  hdSelect: '正常登记',
354
+  id: ''
355
+});
356
+const resetForma = () => {
357
+  form.value = {
358
+    hdPicId: '',
359
+    hdId: '',
360
+    hdType: '',
361
+    discoveryTime: '',
362
+    hdSubtype: '',
363
+    discoverer: '',
364
+    discovererOther: '',
365
+    hdDescription: '',
366
+    hdLevel: '',
367
+    bz: '',
368
+    hdLocation: '',
369
+    picBefore: '',
370
+
371
+    equipmentId: '',
372
+    hdmanageLevel: '',
373
+    handlingProcesses: '',
374
+    companyId: '',
375
+    repairLeader: '',
376
+    repairOther: '',
377
+    repairSuggest: '',
378
+    repairDdl: '',
379
+    repairDept: '',
380
+    acceptLeader: '',
381
+    acceptOther: '',
382
+    picAfter: '',
383
+    picTemp: '',
384
+    repairDescription: '',
385
+    status: '',
386
+    discovererDept: '',
387
+    discovererDeptCode: '',
388
+    hdLocationCode: '',
389
+    hdLocationName: '',
390
+
391
+    hdSelect: '正常登记',
392
+    id: ''
393
+  };
394
+};
395
+
396
+const isRefreshing = ref(false);
397
+const isLoading = ref(false);
398
+const isFinished = ref(false);
399
+const currentPage = ref(1);
400
+const pageSize = ref(10);
401
+const totalRows = ref(0);
402
+const resultData = ref([]);
403
+
404
+const dept=localStorage.getItem("dept")[0].deptCode;
405
+const getTableData = async () => {
406
+
407
+  const url = '/sgsafe/Class/querySectionList'
408
+  const param = {
409
+    page: currentPage.value,
410
+    rows: pageSize.value,
411
+    params: rouData.value.id
412
+  };
413
+  const response = await proxy.$axios.get(url, param);
414
+  if (response.data.code === 0) {
415
+    tableData.value = response.data.data.records;
416
+    totalRows.value = response.data.data.total;
417
+  } else {
418
+    showToast({
419
+      type: 'error',
420
+      message: '操作失败!' + response.data.msg
421
+    });
422
+  }
423
+};
424
+const ruleIds = ref([]);
425
+const getRuleId = () => {
426
+  var url = '/sgsafe/ExamHead/getCheckRuleId'
427
+  var param = {}
428
+  proxy.$axios.get(url, param).then(response => {
429
+    if (response.data.code == '0') {
430
+      ruleIds.value = response.data.data
431
+    } else {
432
+      console.log("1111111");
433
+    }
434
+  })
435
+  console.log('ruleIds', ruleIds)
436
+}
437
+
438
+const onRefresh = () => {
439
+  basicReset();
440
+  onLoad();
441
+};
442
+
443
+const onLoad = async () => {
444
+  if (isRefreshing.value) {
445
+    resultData.value = [];
446
+    currentPage.value = 1;
447
+    isRefreshing.value = false;
448
+  }
449
+  try {
450
+    await getTableData();
451
+    await getRuleId()
452
+    if (pageSize.value * currentPage.value < totalRows.value) {
453
+      resultData.value = [...resultData.value, ...tableData.value];
454
+      openStatus.value = new Array(resultData.value.length).fill(true);
455
+      currentPage.value++;
456
+
457
+    } else {
458
+      resultData.value = [...resultData.value, ...tableData.value];
459
+      openStatus.value = new Array(resultData.value.length).fill(true);
460
+      isFinished.value = true;
461
+    }
462
+  } catch (error) {
463
+    console.log(error);
464
+    isFinished.value = true;
465
+  } finally {
466
+    isLoading.value = false;
467
+  }
468
+};
469
+/* 通用方法: 重置list数据 */
470
+const basicReset = () => {
471
+  isFinished.value = false;
472
+  isLoading.value = true;
473
+  currentPage.value = 1;
474
+  resultData.value = [];
475
+};
476
+
477
+/*onMounted(() => {
478
+  handleSearch();
479
+});
480
+
481
+const handleSearch = () => {
482
+/!*  currentPage.value = 1;
483
+  isFinished.value = false;
484
+  tableData.value = [];*!/
485
+  basicReset()
486
+  onLoad()
487
+};*/
488
+
489
+const handdelect = () => {
490
+  query.value.checkName = '';
491
+  onRefresh()
492
+};
493
+
494
+const handleDetailLook = (row) => {
495
+  form.value = { ...row };
496
+  proxy.$router.push({
497
+    name: 'taiZhang_detail',
498
+    query: {
499
+      form: form.value.id
500
+    }
501
+  });
502
+  // dialogVisibleLook.value = true;
503
+};
504
+const deleteData=ref({})
505
+
506
+const handleDelete = (item) => {
507
+  deleteData.value=item
508
+  deleteData.value.cancelFlag='1'
509
+  var url = '/sgsafe/EduCheckMaster/save';
510
+  var param = {
511
+    json: JSON.stringify(item)
512
+  };
513
+  proxy.$axios.post(url, param).then(response => {
514
+    if (response.data.code == '0') {
515
+      showSuccessToast("删除成功")
516
+      onRefresh();
517
+
518
+    } else {
519
+    }
520
+
521
+  })
522
+};
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+const resetForm = () => {
531
+  form.value = {
532
+    projectName: '',
533
+    projectLeader: '',
534
+    phone: '',
535
+    dept: ''
536
+  };
537
+};
538
+
539
+
540
+
541
+//处理人员code
542
+const repairLL = ref('');
543
+const repairOO = ref('');
544
+const acceptLL = ref('');
545
+const orJsons = () => {
546
+  // console.log('forms',form.value)
547
+  if (form.value.hdSelect === '正常登记') {
548
+    nextTick(() => {
549
+      nextTick(() => {
550
+        repairLL.value = qq('repairLL', form.value.discoverer);//隐患发现人
551
+        nextTick(() => {
552
+          repairOO.value = qq('repairOO', form.value.discovererOther);//其他隐患发现人
553
+          nextTick(() => {
554
+            acceptLL.value = qq('acceptLL', form.value.discoverer);//隐患销号人
555
+          });
556
+        });
557
+      });
558
+
559
+      // acceptOO.value = qq('acceptOO', form.value.acceptOther)
560
+    });
561
+  } else {
562
+    // console.log('noiajdoifjpoewjfopjp')
563
+    nextTick(() => {
564
+      nextTick(() => {
565
+        repairLL.value = qq('repairLL', form.value.acceptLeader);//隐患发现人
566
+        nextTick(() => {
567
+          repairOO.value = qq('repairOO', form.value.acceptOther);//其他隐患发现人
568
+          nextTick(() => {
569
+            acceptLL.value = qq('acceptLL', form.value.discoverer);//隐患销号人
570
+          });
571
+        });
572
+      });
573
+
574
+      // acceptOO.value = qq('acceptOO', form.value.acceptOther)
575
+    });
576
+  }
577
+};
578
+
579
+const jsons = ref({});
580
+const qq = (a, val) => {
581
+  let aa = '';
582
+  var url = 'sgsafe/Hiddendanger/qqId';
583
+  var param = {
584
+    params: val
585
+  };
586
+  proxy.$axios.post(url, param).then(response => {
587
+    if (response.data.code == 0) {
588
+
589
+      aa = response.data.data;
590
+      switch (a) {
591
+        case 'repairLL':
592
+          repairLL.value = response.data.data;
593
+          // console.log('repairLL',repairLL.value);
594
+          break;
595
+        case 'repairOO':
596
+          repairOO.value = response.data.data;
597
+          // console.log('repairOO',repairLL.value);
598
+          break;
599
+        case 'acceptLL':
600
+          acceptLL.value = response.data.data;
601
+          // console.log('acceptLL',repairLL.value);
602
+          break;
603
+        default:
604
+          break;
605
+      }
606
+      jsons.value = {
607
+        hdConfirm: repairLL.value,
608
+        hdConfirmO: repairOO.value,
609
+        hdCancel: acceptLL.value
610
+      };
611
+
612
+      // 处理函数
613
+      function processValue(value) {
614
+        // 将逗号替换为分号
615
+        const replacedValue = value.replace(/,/g, ';');
616
+        // 分割值
617
+        const parts = replacedValue.split(';');
618
+        // 每个部分前加上 U_
619
+        const processedParts = parts.map(part => `U_${part.trim()}`);
620
+        // 重新组合
621
+        return processedParts.join(';');
622
+      }
623
+
624
+      // 处理整个对象
625
+      const processedData = {};
626
+      for (const key in jsons.value) {
627
+        if (jsons.value.hasOwnProperty(key)) {
628
+          processedData[key] = processValue(jsons.value[key]);
629
+        }
630
+      }
631
+
632
+      console.log('对象', toRaw(processedData));
633
+
634
+      let b = {
635
+        acceptL: processedData.hdConfirm,
636
+        acceptO: processedData.hdConfirmO,
637
+        id: form.value.id
638
+      };
639
+
640
+      if (form.value.hdSelect === '即查即改') {
641
+        b = {
642
+          hdFxr: processedData.hdCancel,
643
+          id: form.value.id
644
+        };
645
+      }
646
+
647
+      if (form.value.hdSelect === '正常登记') {
648
+        b = {
649
+          // hdConfirm: processedData.hdConfirm,
650
+          // hdConfirmO: processedData.hdConfirmO,
651
+          id: form.value.id
652
+        };
653
+      }
654
+
655
+      const aaa = JSON.stringify(toRaw(b));
656
+      sessionStorage.setItem('variables', aaa);
657
+      console.log('aaa', aaa);
658
+    } else {
659
+      showToast({
660
+        type: 'fail',
661
+        message: '操作失败!' + response.data.msg
662
+      });
663
+    }
664
+  });
665
+  return aa;
666
+};
667
+
668
+const reback = () => {
669
+  // 返回逻辑
670
+};
671
+
672
+const deleteRow = (row) => {
673
+  selectedRows.value = [row];
674
+  handleDelete(row);
675
+};
676
+
677
+const deleteRowa = (row) => {
678
+  deleteRow(row);
679
+};
680
+
681
+const bm = (val) => {
682
+  // 部门选择逻辑
683
+};
684
+
685
+//提交审批流程
686
+import { workflowSubmit, workflowCancel } from '@/tools/workflow.js';
687
+
688
+const flowId = ref('');
689
+flowId.value = 'hazardManagementFlowId';
690
+
691
+
692
+const getStatusClass = (status) => {
693
+  switch (status) {
694
+    case '1':
695
+      return 'status-rectifying';
696
+    case '2':
697
+      return 'status-closed';
698
+    default:
699
+      return 'status-analyzing';
700
+  }
701
+};
702
+
703
+/**
704
+ * 按钮实现swipe-cell滑动
705
+ */
706
+const openStatus = ref([])
707
+const swipeCellRefs = ref([])
708
+const getSwipeCellRef = (el, index) => {
709
+  if (el) {
710
+    swipeCellRefs.value[index] = el;
711
+  }
712
+}
713
+const openSwipe = (idx) => {
714
+  openStatus.value = new Array(resultData.value.length).fill(true);
715
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
716
+    openStatus.value[idx] = false
717
+    swipeCellRefs.value[idx].open('right')
718
+  }
719
+  document.addEventListener('click', handleDocumentClick)
720
+}
721
+/**
722
+ * 当点击滑动单元格时,开始监听点击事件
723
+ */
724
+const handleDocumentClick = (event) => {
725
+  openStatus.value = new Array(resultData.value.length).fill(true);
726
+}
727
+
728
+const closeSwipe = (idx) => {
729
+  if (idx >= 0 && idx < swipeCellRefs.value.length) {
730
+    openStatus.value[idx] = true
731
+    swipeCellRefs.value[idx].close()
732
+  }
733
+}
734
+
735
+</script>
736
+
737
+<style scoped>
738
+.h5-container {
739
+  width: 100%;
740
+  padding: 5px;
741
+  box-sizing: border-box;
742
+}
743
+
744
+.status-pending {
745
+  background-color: #fff3cd;
746
+  color: #856404;
747
+  padding: 2px 4px;
748
+  border-radius: 4px;
749
+}
750
+
751
+.status-registered {
752
+  background-color: #d1ecf1;
753
+  color: #0c5460;
754
+  padding: 2px 4px;
755
+  border-radius: 4px;
756
+}
757
+
758
+.status-analyzing {
759
+  background-color: #fff8e1;
760
+  color: #ff8f00;
761
+  padding: 2px 4px;
762
+  border-radius: 4px;
763
+}
764
+
765
+.status-rectifying {
766
+  background-color: #e8f5e9;
767
+  color: #2e7d32;
768
+  padding: 2px 4px;
769
+  border-radius: 4px;
770
+}
771
+
772
+.status-accepting {
773
+  background-color: #e3f2fd;
774
+  color: #1565c0;
775
+  padding: 2px 4px;
776
+  border-radius: 4px;
777
+}
778
+
779
+.status-closed {
780
+  background-color: #f8bbd0;
781
+  color: #b71c1c;
782
+  padding: 2px 4px;
783
+  border-radius: 4px;
784
+}
785
+
786
+.status-finished {
787
+  background-color: #e8eaf6;
788
+  color: #311b92;
789
+  padding: 2px 4px;
790
+  border-radius: 4px;
791
+}
792
+
793
+.status-unknown {
794
+  background-color: #efebe9;
795
+  color: #424242;
796
+  padding: 2px 4px;
797
+  border-radius: 4px;
798
+}
799
+
800
+.cell-title {
801
+  display: -webkit-box;
802
+  /* 旧版弹性盒子模型 */
803
+  -webkit-box-orient: vertical;
804
+  /* 内容垂直排列 */
805
+  -webkit-line-clamp: 2;
806
+  /* 限制显示行数 */
807
+  overflow: hidden;
808
+  /* 超出隐藏 */
809
+  text-overflow: ellipsis;
810
+  /* 省略号 */
811
+  line-height: 1.5;
812
+  /* 可选:设置行高 */
813
+  max-height: calc(1.5em * 2);
814
+  /* 可选:根据行高限制最大高度 */
815
+  font-size: 16px;
816
+  font-weight: bold;
817
+  color: #333;
818
+  /* 字号 */
819
+}
820
+
821
+.swipe-cell-default {
822
+  display: flex;
823
+  background-color: #ffffff;
824
+  justify-content: center;
825
+  align-items: center;
826
+}
827
+
828
+.swipe-cell-default-icon {
829
+  width: 60px;
830
+  display: flex;
831
+  justify-content: center;
832
+}
833
+
834
+.delete-button {
835
+  height: 100%;
836
+  border: none;
837
+  color: #ff0000;
838
+  background-image: url('@/assets/img/del.png');
839
+  background-size: auto 100%;
840
+  background-repeat: no-repeat;
841
+}
842
+
843
+.red-rounded-box-wide {
844
+  width: 80px;           /* 可容纳3个字 */
845
+  height: 36px;          /* 高度适中 */
846
+  border: 2px solid #51e74c;
847
+  border-radius: 18px;   /* 圆角 = 高度 / 2 → 完美胶囊形 */
848
+  display: flex;
849
+  align-items: center;
850
+  justify-content: center;
851
+  font-size: 14px;
852
+  color: #07c160;
853
+  background-color: transparent;
854
+  box-sizing: border-box;
855
+}
856
+
857
+.submit-button {
858
+  height: 100%;
859
+  border: none;
860
+  color: #07c160;
861
+  background-image: url('@/assets/img/sub.png');
862
+  background-size: auto 100%;
863
+  background-repeat: no-repeat;
864
+}
865
+
866
+.subsuccess {
867
+  height: 100%;
868
+  border: none;
869
+  color: #07c160;
870
+  background-image: url('@/assets/img/sub1.png');
871
+  background-size: auto 100%;
872
+  background-repeat: no-repeat;
873
+}
874
+</style>

+ 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>

+ 2
- 4
src/view/dati/examCheck/addPeo.vue ファイルの表示

@@ -1,9 +1,7 @@
1 1
 <template>
2 2
   <div class="h5-container">
3
-    <van-nav-bar title="添加人员" @click-left="onClickLeft" @click-right="handleDepartmentLeaderName">
4
-      <template #right>
5
-        <van-icon name="add" size="25" color="#000" />
6
-      </template>
3
+    <van-nav-bar title="查看结果" @click-left="onClickLeft" @click-right="handleDepartmentLeaderName">
4
+
7 5
     </van-nav-bar>
8 6
 
9 7
     <!-- 项目列表 -->

+ 727
- 0
src/view/dati/examCheck/fcbkdatistart.vue ファイルの表示

@@ -0,0 +1,727 @@
1
+<template>
2
+  <van-sticky>
3
+    <van-nav-bar>
4
+      <!-- <template #left>
5
+        <van-icon name="arrow-left" size="18" @click="goBack" />
6
+      </template> -->
7
+      <template #title> 答题考试 </template>
8
+      <template #right>
9
+        <!-- 提交按钮 -->
10
+        <van-button @click="checkBeforeSubmit" round color="linear-gradient(to right, #18FFFF, #304FFE)"
11
+                    style="height: 30px; width: 80px">交卷</van-button>
12
+      </template>
13
+    </van-nav-bar>
14
+  </van-sticky>
15
+  <van-overlay :show="overlayloading">
16
+    <div class="wrapper">
17
+      <van-loading color="#0094ff"> 加载中... </van-loading>
18
+    </div>
19
+  </van-overlay>
20
+
21
+  <div class="quiz-page">
22
+    <div class="question-number">
23
+      {{ activeIndex + 1 }}/{{ questions.length }}
24
+    </div>
25
+    <div v-if="questions.length > 0" class="question-content">
26
+      <!-- 题干 -->
27
+      <p class="kong">
28
+        <img
29
+          :src="getQuestionTypeImage(currentQuestion.category)"
30
+          class="question-type-img"
31
+          alt=""
32
+        />
33
+        {{ currentQuestion.stem }}
34
+      </p>
35
+
36
+
37
+      <!-- 单选题 -->
38
+      <div v-if="currentQuestion.category === '单选'">
39
+        <van-radio-group v-model="userAnswers[currentQuestion.id]">
40
+          <van-radio :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-radio>
41
+          <van-radio :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-radio>
42
+          <van-radio :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-radio>
43
+          <van-radio :name="'D'" class="kong" v-if="currentQuestion.optionD">D.
44
+            {{ currentQuestion.optionD }}</van-radio>
45
+          <van-radio :name="'E'" class="kong" v-if="currentQuestion.optionE">E.
46
+            {{ currentQuestion.optionE }}</van-radio>
47
+        </van-radio-group>
48
+      </div>
49
+
50
+      <!-- 多选题 -->
51
+      <div v-if="currentQuestion.category === '多选'">
52
+        <van-checkbox-group v-model="userAnswers[currentQuestion.id]" shape="square">
53
+          <van-checkbox :name="'A'" class="kong">A.{{ currentQuestion.optionA }}</van-checkbox>
54
+          <van-checkbox :name="'B'" class="kong">B.{{ currentQuestion.optionB }}</van-checkbox>
55
+          <van-checkbox :name="'C'" class="kong">C.{{ currentQuestion.optionC }}</van-checkbox>
56
+          <van-checkbox :name="'D'" class="kong"
57
+                        v-if="currentQuestion.optionD">D.{{ currentQuestion.optionD }}</van-checkbox>
58
+          <van-checkbox :name="'E'" class="kong"
59
+                        v-if="currentQuestion.optionE">E.{{ currentQuestion.optionE }}</van-checkbox>
60
+        </van-checkbox-group>
61
+      </div>
62
+
63
+      <!-- 判断题 -->
64
+      <div v-if="currentQuestion.category === '判断'">
65
+        <van-radio-group v-model="userAnswers[currentQuestion.id]">
66
+          <van-radio :name="'A'" class="kong">A.正确</van-radio>
67
+          <van-radio :name="'B'" class="kong">B.错误</van-radio>
68
+        </van-radio-group>
69
+      </div>
70
+    </div>
71
+
72
+    <!-- 底部固定栏 -->
73
+    <div class="footer">
74
+      <van-button @click="prevQuestion" :disabled="activeIndex === 0"
75
+                  style="height: 40px; width: 45%">上一题</van-button>
76
+      <van-button @click="nextQuestion" :disabled="activeIndex === questions.length - 1" style="
77
+          height: 40px;
78
+          width: 45%;
79
+          background-color: var(--van-radio-checked-icon-color);
80
+          border-color: var(--van-radio-checked-icon-color);
81
+          color: #fff;
82
+        ">下一题</van-button>
83
+    </div>
84
+
85
+    <!-- 提交前确认弹窗 -->
86
+    <van-dialog v-model:show="confirmSubmitDialog" title="确认交卷" show-cancel-button @confirm="submitForm">
87
+      <p :class="{ 'van-dialog__message': true, 'text-center': true }">
88
+        <span v-if="hasUnanswered">{{ unansweredText }}</span>
89
+        <span v-else>{{ completedText }}</span>
90
+      </p>
91
+    </van-dialog>
92
+
93
+    <!-- 结果弹窗 -->
94
+    <van-popup v-model:show="showResult" position="top" style="height: 100%">
95
+      <van-sticky>
96
+        <van-nav-bar title="答题结果" />
97
+      </van-sticky>
98
+      <div style="
99
+          margin-top: 10px;
100
+          margin-left: 20px;
101
+          margin-bottom: 20px;
102
+          font-weight: bold;
103
+        ">
104
+        本次得分:{{ totalScore }}
105
+        <!--取接口成绩-->
106
+      </div>
107
+      <van-divider />
108
+      <!-- 题干 -->
109
+      <div v-for="question in questions" :key="question.id" class="question">
110
+        <p>
111
+          <span v-if="question.category === '单选'">[单选]</span>
112
+          <span v-if="question.category === '多选'">[多选]</span>
113
+          <span v-if="question.category === '判断'">[判断]</span>
114
+          {{ question.stem }}
115
+        </p>
116
+        <!-- 显示提交答案 -->
117
+        <p>
118
+					<span :style="{
119
+              color: Array.isArray(userAnswers[question.id])
120
+                ? userAnswers[question.id].sort().join('') ===
121
+                  question.answer
122
+                  ? '#007aff'
123
+                  : 'red'
124
+                : userAnswers[question.id] === question.answer
125
+                ? '#007aff'
126
+                : 'red',
127
+            }">
128
+						提交答案:{{
129
+              Array.isArray(userAnswers[question.id])
130
+                ? userAnswers[question.id].sort().join("")
131
+                : userAnswers[question.id] || "未作答"
132
+            }}
133
+					</span>
134
+        </p>
135
+        <!-- 显示正确答案 -->
136
+        <p style="color: #007aff">正确答案:{{ question.answer }}</p>
137
+        <div v-if="question.category === '单选'" class="kong">
138
+          <div>A. {{ question.optionA }}</div>
139
+          <div>B. {{ question.optionB }}</div>
140
+          <div>C. {{ question.optionC }}</div>
141
+          <div v-if="question.optionD">D. {{ question.optionD }}</div>
142
+          <div v-if="question.optionE">E. {{ question.optionE }}</div>
143
+        </div>
144
+        <div v-if="question.category === '多选'" class="kong">
145
+          <div>A. {{ question.optionA }}</div>
146
+          <div>B. {{ question.optionB }}</div>
147
+          <div>C. {{ question.optionC }}</div>
148
+          <div v-if="question.optionD">D. {{ question.optionD }}</div>
149
+          <div v-if="question.optionE">E. {{ question.optionE }}</div>
150
+        </div>
151
+        <div v-if="question.category === '判断'" class="kong">
152
+          <div>A.正确</div>
153
+          <div>B.错误</div>
154
+        </div>
155
+
156
+        <!-- AI解析按钮和内容 -->
157
+        <div style="margin: 10px 0;">
158
+          <van-button
159
+            type="primary"
160
+            size="small"
161
+            :loading="analysisLoading[question.id]"
162
+            @click="generateAIAnalysis(question,true)"
163
+          >
164
+            {{ aiAnalysis[question.id] ? '重新解析' : 'AI解析' }}
165
+          </van-button>
166
+
167
+          <!-- 显示AI解析内容 -->
168
+          <div v-if="aiAnalysis[question.id]" class="ai-analysis-content">
169
+            <div v-html="renderAnalysis(aiAnalysis[question.id])"></div>
170
+          </div>
171
+        </div>
172
+
173
+        <van-divider />
174
+      </div>
175
+      <div style="margin-top: 20px; text-align: center; margin-bottom: 20px">
176
+        <van-button class="questionBtn" type="primary" @click="confirmResult">确定</van-button>
177
+      </div>
178
+    </van-popup>
179
+  </div>
180
+</template>
181
+
182
+<script setup>
183
+import {
184
+  onMounted,
185
+  ref,
186
+  getCurrentInstance,
187
+  computed
188
+} from "vue";
189
+import {
190
+  showConfirmDialog,
191
+  showSuccessToast,
192
+  showFailToast,
193
+  showDialog
194
+} from 'vant';
195
+import {
196
+  useRouter,
197
+  useRoute
198
+} from "vue-router";
199
+import {
200
+  examResult,
201
+  formNew,
202
+  myDefaultCourse,
203
+  saveScore,
204
+  sortData,
205
+} from "@/api/dati";
206
+const {
207
+  proxy
208
+} = getCurrentInstance()
209
+
210
+const router = useRouter();
211
+const route = useRoute();
212
+const query = route.query;
213
+
214
+// 1. 从本地存储获取当前用户的 userCode(字符串)
215
+const currentUserCode = localStorage.getItem('userCode');
216
+
217
+// 2. 包装成数组(即使为空也安全)
218
+const userCodeArray = currentUserCode ? [currentUserCode] : [];
219
+
220
+// 3. 按原来的方式生成 userDesc 字符串
221
+const userDesc = userCodeArray.join(',');
222
+const getExamContext = () => ({
223
+  id: route.query.examId || '',
224
+  testRole: route.query.testRole || '',
225
+  checkTime: route.query.checkTime || '',
226
+  checkName: route.query.checkName || '',
227
+  testType: route.query.testType || '',
228
+  addId: route.query.addId || '',
229
+  userDesc: userDesc,
230
+  userName:localStorage.getItem('userDesc')
231
+
232
+});
233
+
234
+const   userId = localStorage.getItem('userId');
235
+const questions = ref([]);
236
+
237
+const userAnswers = ref({});
238
+const activeIndex = ref(0);
239
+const totalScore = ref(0);
240
+const showResult = ref(false);
241
+
242
+const confirmSubmitDialog = ref(false);
243
+const hasUnanswered = ref(false);
244
+const unansweredText = "有题目未完成,是否确认交卷?";
245
+const completedText = "已完成所有题目,是否确认交卷?";
246
+const overlayloading = ref(false);
247
+// 在组件挂载时获取试卷
248
+onMounted(async () => {
249
+  overlayloading.value = true;
250
+  await saveChecUser();
251
+  await getForm();
252
+  overlayloading.value = false;
253
+});
254
+const handData = ref({});
255
+
256
+const saveChecUser = async () => {
257
+  const examContext = getExamContext();
258
+  console.log("接到的参数:",examContext);
259
+  var url = '/sgsafe/ExamHead/showsaveChecUser';
260
+  var param = {
261
+    params: JSON.stringify(examContext)
262
+  };
263
+  const res = await proxy.$axios.post(url, param);
264
+    if (res.data.code == '0') {
265
+    handData.value =  res.data.data;
266
+    } else {
267
+      showFailToast('操作失败!' + res.data.msg)
268
+    }
269
+
270
+
271
+}
272
+//获取试卷
273
+const getForm = async () => {
274
+  console.log("后端返回的参数为:",handData.value);
275
+  console.log("headId为",handData.value.id);
276
+  var url = '/sgsafe/ExamLine/showquery'
277
+  const query1 = ref({
278
+    headId:   handData.value.id
279
+  })
280
+  var param = {
281
+    params: JSON.stringify(query1.value)
282
+  }
283
+  try {
284
+    const res = await proxy.$axios.post(url, param);
285
+    if (res.data.code === 0) {
286
+      questions.value = res.data.data
287
+    } else {
288
+      console.log('操作失败!' + res.data.msg);
289
+    }
290
+  } catch (error) {
291
+    console.log('请求出错:', questions);
292
+  }
293
+};
294
+
295
+// 获取当前题目
296
+const currentQuestion = computed(() => {
297
+  return questions.value[activeIndex.value];
298
+});
299
+
300
+// 获取题目类型对应的图片路径
301
+import danxuan from '@/assets/img/dx.svg'
302
+import duoxuanImg from '@/assets/img/ksdx.svg'
303
+import panduanImg from '@/assets/img/kspd.svg'
304
+const getQuestionTypeImage = (category) => {
305
+  switch (category) {
306
+    case "单选": // 单选
307
+      return danxuan;
308
+    case "多选": // 多选
309
+      return duoxuanImg;
310
+    case "判断": // 判断
311
+      return panduanImg;
312
+    default:
313
+      return "";
314
+  }
315
+};
316
+
317
+//返回答题首页
318
+const goBack = () => {
319
+  router.push({
320
+    path: "/dailyproblem"
321
+  });
322
+};
323
+
324
+// 切换到下一题
325
+const nextQuestion = () => {
326
+  if (activeIndex.value < questions.value.length - 1) {
327
+    activeIndex.value++;
328
+  }
329
+};
330
+
331
+// 切换到上一题
332
+const prevQuestion = () => {
333
+  if (activeIndex.value > 0) {
334
+    activeIndex.value--;
335
+  }
336
+};
337
+
338
+
339
+const getUserAnswers = () => {
340
+  let useranswers = [];
341
+  questions.value.forEach((question) => {
342
+    const userAnswer = userAnswers.value[question.id]; // 获取用户的答案
343
+    let userAnswerString;
344
+    if (Array.isArray(userAnswer)) {
345
+      // 多选题,将数组转换为字符串
346
+      userAnswerString = userAnswer.sort().join(""); // 排序并转换为字符串,如 "ABC"
347
+    } else {
348
+      // 单选题,直接是字符串
349
+      userAnswerString = userAnswer || ""; // 如果未选择答案,则设为空字符串
350
+    }
351
+    // 将答案保存到 answers 数组中
352
+    useranswers.push({
353
+      id: question.id, // 题目 ID
354
+      userAnswer: userAnswerString, // 用户的答案
355
+    });
356
+  });
357
+  return useranswers;
358
+};
359
+
360
+//交卷
361
+const submitForm = async () => {
362
+  overlayloading.value = true;
363
+  try {
364
+    let answers = getUserAnswers();
365
+    //console.log('answers.value', answers)
366
+    var url = '/sgsafe/ExamLine/showappSaveMyScore'
367
+    var param = {
368
+      json: JSON.stringify(
369
+        answers
370
+      )
371
+    }
372
+    try {
373
+      const res = await proxy.$axios.post(url, param);
374
+      if (res.data.code === 0) {
375
+        showSuccessToast("保存成功")
376
+      } else {
377
+        console.log('操作失败!' + res.data.msg);
378
+      }
379
+    } catch (error) {
380
+      console.log('请求出错:', questions);
381
+    }
382
+    const courseId = handData.value.id;
383
+    const today = new Date();
384
+    const year = today.getFullYear();
385
+    const month = String(today.getMonth() + 1).padStart(2, '0');
386
+    const day = String(today.getDate()).padStart(2, '0');
387
+    const todayStr = `${year}-${month}-${day}`;// ✅ 来自 saveChecUser 返回
388
+    //开始判卷
389
+    var url2 = '/sgsafe/Package/showdoProc'
390
+    var param2 = {
391
+      procName: 'safeplat.sxsp_grade_exam_p',
392
+      param: JSON.stringify([courseId])
393
+    }
394
+    try {
395
+      const res2 = await proxy.$axios.post(url2, param2);
396
+      if (res2.data.code === 0) {
397
+        consle.log("courseId:" + courseId + "判卷完成!")
398
+      } else {
399
+        console.log('操作失败!' + res.data.msg);
400
+      }
401
+    } catch (error) {
402
+      console.log('请求出错:', questions);
403
+    }
404
+
405
+    overlayloading.value = false;
406
+
407
+    //查询本日答题次数和分数
408
+    var url3 = '/sgsafe/DailyExam/showappQueryMyScore'
409
+    const query3 = ref({
410
+      userId: userId,
411
+      examDate: todayStr,
412
+      headId: courseId,
413
+    })
414
+    var param3 = {
415
+      params: JSON.stringify(query3.value)
416
+    }
417
+    const res3 = await proxy.$axios.post(url3, param3);
418
+    if (res3.data.code === 0) {
419
+      console.log(res3.data)
420
+      if (res3.data.data.dailyExamList.length==0){
421
+        var url4='/sgsafe/ExamHead/showqueryByheadId'
422
+        var param4 = {
423
+          headId: courseId
424
+        }
425
+        const res4 = await proxy.$axios.post(url4, param4);
426
+        if (res4.data.code === 0) {
427
+          totalScore.value = res4.data.data.totalScore;
428
+        }
429
+        showConfirmDialog({
430
+          message: "判卷完成",
431
+          confirmButtonText: "查看本次答题结果",
432
+          cancelButtonText: "退出答题"
433
+        })
434
+          .then(() => {
435
+            showResult.value = true;
436
+          })
437
+          .catch(() => {
438
+
439
+            router.replace({
440
+              path: "/dailyproblem"
441
+            });
442
+          })
443
+      }else {
444
+        let times = res3.data.data.dailyExamList[0].examCounts;
445
+        totalScore.value = res3.data.data.headScore;
446
+        showConfirmDialog({
447
+          message: "判卷完成",
448
+          confirmButtonText: "查看本次答题结果",
449
+          cancelButtonText: times == "3" ? "退出答题" : "继续答题",
450
+        })
451
+          .then(() => {
452
+            showResult.value = true;
453
+          })
454
+          .catch(() => {
455
+            if (times == "3") {
456
+              router.replace({
457
+                path: "/dailyproblem"
458
+              });
459
+            } else {
460
+              router.push({
461
+                path: "/dailyproblem"
462
+              });
463
+            }
464
+          });
465
+      }
466
+
467
+
468
+    } else {
469
+      console.log('操作失败!' + res3.data.msg);
470
+    }
471
+
472
+  } catch (error) {
473
+    console.error("出错:", error);
474
+    showFailToast("交卷失败");
475
+  }
476
+};
477
+
478
+// 确认结果并返回
479
+const confirmResult = () => {
480
+  showResult.value = false;
481
+  // router.back();
482
+  router.back()
483
+};
484
+
485
+// 检查是否所有题目都已作答
486
+const checkBeforeSubmit = () => {
487
+  hasUnanswered.value = questions.value.some((question) => {
488
+    const userAnswer = userAnswers.value[question.id];
489
+    return (
490
+      !userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)
491
+    );
492
+  });
493
+  confirmSubmitDialog.value = true;
494
+};
495
+
496
+// AI解析功能
497
+// AI解析相关变量
498
+const aiAnalysis = ref({}); // 存储每道题的AI解析内容
499
+const analysisLoading = ref({}); // 存储每道题的解析加载状态
500
+import { fetchHuaweiResponse } from "@/tools/deepseek.js";
501
+// 动态导入依赖
502
+let marked, DOMPurify;
503
+
504
+const initMarkdownLibs = async () => {
505
+  try {
506
+    // 尝试导入marked
507
+    const markedModule = await import('marked');
508
+    marked = markedModule.marked || markedModule.default || markedModule;
509
+
510
+    // 尝试导入DOMPurify
511
+    const dompurifyModule = await import('dompurify');
512
+    DOMPurify = dompurifyModule.default || dompurifyModule;
513
+  } catch (error) {
514
+    console.warn('Markdown libraries not available, using plain text', error);
515
+    // 如果导入失败,使用基础功能
516
+    marked = {
517
+      parse: (text) => text
518
+    };
519
+    DOMPurify = {
520
+      sanitize: (html) => html
521
+    };
522
+  }
523
+};
524
+
525
+// 在组件挂载时初始化
526
+onMounted(() => {
527
+  initMarkdownLibs();
528
+});
529
+
530
+// 生成AI解析
531
+const generateAIAnalysis = async (question, force = false) => {
532
+  // 如果该题已有解析且不是强制重新生成,直接返回
533
+  if (aiAnalysis.value[question.id] && !force) {
534
+    return;
535
+  }
536
+
537
+  // 如果是重新解析,先清空之前的内容
538
+  if (force) {
539
+    aiAnalysis.value[question.id] = '';
540
+  }
541
+
542
+  // 确保依赖已加载
543
+  if (!marked || !DOMPurify) {
544
+    await initMarkdownLibs();
545
+  }
546
+
547
+  // 设置加载状态
548
+  analysisLoading.value[question.id] = true;
549
+
550
+  try {
551
+    // 构造提示词
552
+    let prompt = `请为以下题目提供详细解析:
553
+题目类型:${question.category}题干:${question.stem}`;
554
+
555
+    // 添加选项
556
+    if (question.optionA) prompt += `\nA. ${question.optionA}`;
557
+    if (question.optionB) prompt += `\nB. ${question.optionB}`;
558
+    if (question.optionC) prompt += `\nC. ${question.optionC}`;
559
+    if (question.optionD) prompt += `\nD. ${question.optionD}`;
560
+    if (question.optionE) prompt += `\nE. ${question.optionE}`;
561
+
562
+    prompt += `\n正确答案:${question.answer}`;
563
+
564
+    // 添加用户答案(如果已作答)
565
+    const userAnswer = userAnswers.value[question.id];
566
+    if (userAnswer) {
567
+      const userAnswerString = Array.isArray(userAnswer)
568
+        ? userAnswer.sort().join("")
569
+        : userAnswer;
570
+      prompt += `\n用户答案:${userAnswerString}`;
571
+    }
572
+
573
+    prompt += `\n\n请提供以下内容:
574
+1. 正确答案的解释
575
+2. 为什么其他选项不正确(如果用户答案错误)
576
+3. 相关知识点说明`;
577
+
578
+    // 构造消息对象
579
+    const messages = [
580
+      {
581
+        role: "user",
582
+        content: prompt
583
+      }
584
+    ];
585
+
586
+    // 调用AI接口
587
+    fetchHuaweiResponse(
588
+      messages,
589
+      (content, isThinking, isEnd) => {
590
+        // 实时更新解析内容
591
+        aiAnalysis.value[question.id] = content;
592
+
593
+        // 如果是最终结果,停止加载状态
594
+        if (isEnd) {
595
+          analysisLoading.value[question.id] = false;
596
+        }
597
+      },
598
+      null
599
+    );
600
+  } catch (error) {
601
+    console.error('AI解析生成失败:', error);
602
+    analysisLoading.value[question.id] = false;
603
+    aiAnalysis.value[question.id] = '解析生成失败';
604
+  }
605
+};
606
+
607
+// 解析内容转换为HTML
608
+const renderAnalysis = (content) => {
609
+  if (!content) return '';
610
+
611
+  try {
612
+    // 确保依赖已加载
613
+    if (!marked || !DOMPurify) {
614
+      return content.replace(/\n/g, '<br>');
615
+    }
616
+
617
+    const html = marked.parse ? marked.parse(content) : marked(content);
618
+    return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
619
+  } catch (error) {
620
+    console.error('Markdown解析错误:', error);
621
+    return content.replace(/\n/g, '<br>');
622
+  }
623
+};
624
+
625
+
626
+
627
+
628
+</script>
629
+
630
+<style scoped>
631
+.quiz-page {
632
+  padding: 20px;
633
+}
634
+
635
+.question-type-img {
636
+  width: 54px;
637
+  height: 20px;
638
+}
639
+
640
+.question {
641
+  margin-left: 20px;
642
+  margin-right: 20px;
643
+}
644
+
645
+.kong {
646
+  margin-bottom: 20px;
647
+}
648
+
649
+.footer {
650
+  position: fixed;
651
+  bottom: 0;
652
+  left: 0;
653
+  width: 100%;
654
+  background-color: #fff;
655
+  display: flex;
656
+  justify-content: space-around;
657
+  align-items: center;
658
+  margin-bottom: 10px;
659
+}
660
+
661
+.van-dialog__message {
662
+  text-align: center;
663
+}
664
+
665
+.questionBtn {
666
+  width: 40%;
667
+}
668
+
669
+/* 遮罩 */
670
+.wrapper {
671
+  display: flex;
672
+  align-items: center;
673
+  justify-content: center;
674
+  height: 100%;
675
+}
676
+
677
+.van-overlay {
678
+  z-index: 2;
679
+  background-color: rgba(0, 0, 0, 0.5);
680
+}
681
+
682
+.ai-analysis-content {
683
+  margin-top: 10px;
684
+  padding: 10px;
685
+  background-color: #f5f5f5;
686
+  border-radius: 4px;
687
+  font-size: 14px;
688
+  line-height: 1.6;
689
+}
690
+
691
+.ai-analysis-content :deep(h1),
692
+.ai-analysis-content :deep(h2),
693
+.ai-analysis-content :deep(h3) {
694
+  margin: 10px 0;
695
+  font-weight: bold;
696
+  font-size: 16px;
697
+}
698
+
699
+.ai-analysis-content :deep(p) {
700
+  margin: 8px 0;
701
+}
702
+
703
+.ai-analysis-content :deep(ul),
704
+.ai-analysis-content :deep ol {
705
+  padding-left: 20px;
706
+  margin: 8px 0;
707
+}
708
+
709
+.ai-analysis-content :deep(li) {
710
+  margin: 4px 0;
711
+}
712
+
713
+.ai-analysis-content :deep(code) {
714
+  padding: 2px 4px;
715
+  background-color: #e0e0e0;
716
+  border-radius: 3px;
717
+  font-family: monospace;
718
+}
719
+
720
+.ai-analysis-content :deep(pre) {
721
+  padding: 10px;
722
+  background-color: #e0e0e0;
723
+  border-radius: 4px;
724
+  overflow-x: auto;
725
+}
726
+
727
+</style>

+ 90
- 5
src/view/dati/examCheck/index.vue ファイルの表示

@@ -40,7 +40,9 @@
40 40
 
41 41
             <template #right>
42 42
               <van-button  square class="delete-button" text="删除" @click="handleDelete(item)" />
43
-              <van-button  square class="submit-button" text="添加人员" @click="goaddPeo(item)" />
43
+              <van-button  square class="submit-button" text="查看结果" @click="goaddPeo1(item)" />
44
+              <van-button square class="submit-button" text="二维码_" @click="goaddPeo(item)" />
45
+
44 46
             </template>
45 47
           </van-swipe-cell>
46 48
         </div>
@@ -55,7 +57,19 @@
55 57
       </template>
56 58
       <div style="padding: 30px;">确定要删除该项目吗?</div>
57 59
     </van-dialog>
58
-
60
+    <!-- 二维码弹窗 -->
61
+    <van-dialog
62
+      v-model:show="qrDialogVisible"
63
+      title="扫码添加人员"
64
+      show-cancel-button
65
+      close-on-popstate
66
+      @cancel="qrDialogVisible = false"
67
+    >
68
+      <div class="qr-dialog-content">
69
+        <img v-if="qrDataUrl" :src="qrDataUrl" alt="二维码" class="qr-code" />
70
+        <p class="qr-tip">请使用手机扫码 App 扫描上方二维码</p>
71
+      </div>
72
+    </van-dialog>
59 73
   </div>
60 74
 </template>
61 75
 
@@ -65,6 +79,15 @@ import { Dialog, showDialog, showSuccessToast, showToast, Toast } from 'vant';
65 79
 
66 80
 const { proxy } = getCurrentInstance();
67 81
 
82
+//生成二维码
83
+import QRCode from 'qrcode';
84
+
85
+// 新增状态
86
+const qrDialogVisible = ref(false);
87
+const qrDataUrl = ref('');
88
+const currentExamItem = ref(null); // 用于临时保存当前 item
89
+
90
+
68 91
 const onClickLeft = () => {
69 92
   history.back();
70 93
 };
@@ -124,7 +147,40 @@ const handAdd =  () => {
124 147
     } });
125 148
 
126 149
 };
127
-const goaddPeo = (item) => {
150
+import.meta.env.VITE_BASE_API
151
+const goaddPeo = async (item) => {
152
+  currentExamItem.value = item;
153
+
154
+  //
155
+  const baseUrl = window.location.origin + '/sgsafeh5/fcbkdatistart';
156
+
157
+  const url = new URL(baseUrl);
158
+  url.searchParams.set('examId', item.id);           // 考试ID
159
+  url.searchParams.set('testRole', item.testRole); //规则id
160
+  url.searchParams.set('checkTime', item.checkTime); //考试时间
161
+  url.searchParams.set('checkName', item.checkName || '');
162
+  url.searchParams.set('testType', item.testType || '');//考试类型
163
+
164
+   url.searchParams.set('addId', localStorage.getItem('userId') || '');
165
+
166
+  const qrContent = url.toString();
167
+
168
+  try {
169
+    qrDataUrl.value = await QRCode.toDataURL(qrContent, {
170
+      width: 240,
171
+      margin: 2,
172
+      color: {
173
+        dark: '#000000',
174
+        light: '#ffffff'
175
+      }
176
+    });
177
+    qrDialogVisible.value = true;
178
+  } catch (err) {
179
+    console.error('二维码生成失败:', err);
180
+    Toast('二维码生成失败,请重试');
181
+  }
182
+};
183
+const goaddPeo1 = (item) => {
128 184
   router.push({
129 185
     path: '/addPeo',
130 186
     query: {
@@ -132,7 +188,6 @@ const goaddPeo = (item) => {
132 188
     }
133 189
   })
134 190
 }
135
-
136 191
 const edits = (row) => {
137 192
   kz.value = true;
138 193
   form.value = { ...row };
@@ -835,8 +890,16 @@ const closeSwipe = (idx) => {
835 890
   background-image: url('@/assets/img/sub.png');
836 891
   background-size: auto 100%;
837 892
   background-repeat: no-repeat;
838
-}
839 893
 
894
+}
895
+.edit-button {
896
+  height: 100%;
897
+  border: none;
898
+  color: #F9CC9D;
899
+  background-image: url('@/assets/img/edit.png');
900
+  background-size: auto 100%;
901
+  background-repeat: no-repeat;
902
+}
840 903
 .subsuccess {
841 904
   height: 100%;
842 905
   border: none;
@@ -845,4 +908,26 @@ const closeSwipe = (idx) => {
845 908
   background-size: auto 100%;
846 909
   background-repeat: no-repeat;
847 910
 }
911
+.qr-dialog-content {
912
+  display: flex;
913
+  flex-direction: column;
914
+  align-items: center;
915
+  padding: 20px 0;
916
+}
917
+
918
+.qr-code {
919
+  width: 240px;
920
+  height: 240px;
921
+  background: #fff;
922
+  padding: 8px;
923
+  border-radius: 8px;
924
+  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
925
+}
926
+
927
+.qr-tip {
928
+  margin-top: 12px;
929
+  font-size: 13px;
930
+  color: #666;
931
+  text-align: center;
932
+}
848 933
 </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: '',

+ 118
- 114
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 = {};
@@ -303,7 +304,7 @@ const actions=ref([])
303 304
       questionIds.value = response.data.data; // 先赋值给 questionIds.value
304 305
 
305 306
 
306
-      // ✅ 关键:使用 questionIds.value.map
307
+
307 308
       actions.value = questionIds.value.map(item => ({
308 309
         name: item.ruleName,
309 310
         value: item.id
@@ -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>

+ 0
- 1
src/view/knowledge/managerList.vue ファイルの表示

@@ -216,7 +216,6 @@ const actions=ref([])
216 216
       questionIds.value = response.data.data; // 先赋值给 questionIds.value
217 217
 
218 218
 
219
-      // ✅ 关键:使用 questionIds.value.map
220 219
       actions.value = questionIds.value.map(item => ({
221 220
         name: item.ruleName,
222 221
         value: item.id

+ 242
- 0
src/view/login/Register.vue ファイルの表示

@@ -0,0 +1,242 @@
1
+<template>
2
+  <div class="login-container">
3
+    <div class="login-content">
4
+      <div class="login-box">
5
+        <div class="login-header">
6
+          <img src="../../assets/logo.png" alt="logo" class="logo" />
7
+          <h2>请填写您的信息</h2>
8
+          <p class="subtitle">用于记录考试参与情况</p>
9
+        </div>
10
+
11
+        <van-form @submit="onSubmit" class="login-form">
12
+          <van-cell-group inset>
13
+            <van-field
14
+              v-model="formData.userCode"
15
+              name="userCode"
16
+              placeholder="请输入您的工号"
17
+              :rules="[{ required: true, message: '请输入工号' }]"
18
+            >
19
+              <template #left-icon>
20
+                <van-icon name="idcard-o" />
21
+              </template>
22
+            </van-field>
23
+            <van-field
24
+              v-model="formData.userName"
25
+              name="userName"
26
+              placeholder="请输入您的姓名"
27
+              :rules="[{ required: true, message: '请输入姓名' }]"
28
+            >
29
+              <template #left-icon>
30
+                <van-icon name="contact" />
31
+              </template>
32
+            </van-field>
33
+          </van-cell-group>
34
+          <div class="submit-btn">
35
+            <van-button
36
+              round
37
+              block
38
+              type="primary"
39
+              native-type="submit"
40
+              :loading="isLoading"
41
+            >
42
+              确认进入
43
+            </van-button>
44
+          </div>
45
+        </van-form>
46
+      </div>
47
+    </div>
48
+  </div>
49
+</template>
50
+
51
+<script setup lang="ts">
52
+import { ref } from 'vue'
53
+import { useRouter, useRoute } from 'vue-router'
54
+import { showToast } from 'vant'
55
+import axios from '@/axios'
56
+const router = useRouter()
57
+const route = useRoute()
58
+const isLoading = ref(false)
59
+
60
+const formData = ref({
61
+  userCode: '',
62
+  userName: ''
63
+})
64
+
65
+async function onSubmit() {
66
+  if (!formData.value.userCode.trim() || !formData.value.userName.trim()) {
67
+    showToast('请完整填写工号和姓名')
68
+    return
69
+  }
70
+
71
+  try {
72
+    isLoading.value = true
73
+   /* const userId=ref('C19810FBCBD111B2B2FA58EA818C71F9')
74
+
75
+    var url = 'framework/SysLogin/queryPublicKeyToken'
76
+    // var url = 'framework/SysLogin/queryPublicKey'
77
+    var param = {
78
+      param: userId.value
79
+    }
80
+    await axios.post(url, param).then(response => {
81
+
82
+      localStorage.setItem('publicKey', response.data.data.publicKey)
83
+      localStorage.setItem('userId', response.data.data.id)
84
+      localStorage.setItem('userCode', formData.value.userCode.trim())
85
+      localStorage.setItem('userName', response.data.data.userName)
86
+      localStorage.setItem('userDesc', formData.value.userName.trim())
87
+      localStorage.setItem('userType', response.data.data.userType)
88
+      localStorage.setItem('token', response.data.data.token)
89
+      localStorage.setItem('belongId', response.data.data.belongId)
90
+    })*/
91
+
92
+    localStorage.setItem('userDesc', formData.value.userName.trim())
93
+    localStorage.setItem('userCode', formData.value.userCode.trim())
94
+    showToast({
95
+      type: 'success',
96
+      message: '信息提交成功'
97
+    })
98
+
99
+    // 跳回原扫码页面(/fcbkdatistart),保留原始 query 参数
100
+    // 注意:route.query 中应包含 examId, testRole 等参数(如果从该页跳转而来)
101
+    // 如果不是从 fcbkdatistart 进来的,可默认跳首页或提示
102
+
103
+    const targetPath = '/fcbkdatistart'
104
+    const originalQuery = route.query // 如果是从 fcbkdatistart 跳过来的,query 会保留
105
+    console.log("携带参数:",originalQuery);
106
+
107
+    // 更健壮的方式:检查是否有必要参数,否则跳首页
108
+    if (originalQuery.examId) {
109
+
110
+      await router.replace({ path: targetPath, query: originalQuery })
111
+    } else {
112
+      await router.push('/Home1')
113
+    }
114
+  } catch (error) {
115
+    console.error('游客信息提交失败:', error)
116
+    showToast('提交失败,请重试')
117
+  } finally {
118
+    isLoading.value = false
119
+  }
120
+}
121
+</script>
122
+
123
+<style lang="scss" scoped>
124
+/* 样式保持不变,仅微调标题 */
125
+.login-container {
126
+  height: 100vh;
127
+  width: 100vw;
128
+  display: flex;
129
+  align-items: center;
130
+  justify-content: center;
131
+  background: linear-gradient(135deg, #1890ff 0%, #1d39c4 100%);
132
+  position: relative;
133
+  overflow: hidden;
134
+
135
+  &::before {
136
+    content: '';
137
+    position: absolute;
138
+    width: 200%;
139
+    height: 200%;
140
+    top: -50%;
141
+    left: -50%;
142
+    background: url('@/assets/login-bg.svg') repeat;
143
+    opacity: 0.1;
144
+    animation: move 10s linear infinite;
145
+  }
146
+}
147
+
148
+.login-content {
149
+  position: relative;
150
+  z-index: 1;
151
+  width: 100%;
152
+  padding: 20px;
153
+}
154
+
155
+.login-box {
156
+  width: 100%;
157
+  max-width: 400px;
158
+  margin: 0 auto;
159
+  padding: 30px 24px;
160
+  background: rgba(255, 255, 255, 0.9);
161
+  backdrop-filter: blur(10px);
162
+  border-radius: 16px;
163
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
164
+
165
+  .login-header {
166
+    text-align: center;
167
+    margin-bottom: 32px;
168
+
169
+    .logo {
170
+      width: 64px;
171
+      height: 64px;
172
+      margin-bottom: 16px;
173
+    }
174
+
175
+    h2 {
176
+      font-size: 24px;
177
+      color: #1a1a1a;
178
+      margin: 0 0 8px;
179
+      font-weight: 600;
180
+    }
181
+
182
+    .subtitle {
183
+      font-size: 14px;
184
+      color: #666;
185
+      margin: 0;
186
+    }
187
+  }
188
+
189
+  :deep(.van-cell-group) {
190
+    background: transparent;
191
+
192
+    .van-field {
193
+      background: rgba(255, 255, 255, 0.8);
194
+      border-radius: 8px;
195
+      margin-bottom: 16px;
196
+
197
+      &:last-child {
198
+        margin-bottom: 0;
199
+      }
200
+
201
+      .van-field__left-icon {
202
+        margin-right: 8px;
203
+        color: #666;
204
+      }
205
+    }
206
+  }
207
+
208
+  .submit-btn {
209
+    margin-top: 24px;
210
+
211
+    .van-button {
212
+      height: 44px;
213
+      font-size: 16px;
214
+      background: linear-gradient(135deg, #1890ff 0%, #1d39c4 100%);
215
+      border: none;
216
+
217
+      &--loading {
218
+        opacity: 0.8;
219
+      }
220
+    }
221
+  }
222
+}
223
+
224
+@keyframes move {
225
+  0% {
226
+    transform: translate(0, 0);
227
+  }
228
+  100% {
229
+    transform: translate(-50%, -50%);
230
+  }
231
+}
232
+
233
+@media screen and (max-width: 480px) {
234
+  .login-box {
235
+    padding: 24px 16px;
236
+
237
+    .login-header h2 {
238
+      font-size: 20px;
239
+    }
240
+  }
241
+}
242
+</style>

+ 0
- 1
src/view/moneySafe/safeMoneyManagementList.vue ファイルの表示

@@ -288,7 +288,6 @@ const actions=ref([])
288 288
       questionIds.value = response.data.data; // 先赋值给 questionIds.value
289 289
 
290 290
 
291
-      // ✅ 关键:使用 questionIds.value.map
292 291
       actions.value = questionIds.value.map(item => ({
293 292
         name: item.ruleName,
294 293
         value: item.id

+ 81
- 4
src/view/projectManage/projectConstructionOperation/projectConstructionOperation_detail/indexLook.vue ファイルの表示

@@ -227,6 +227,7 @@
227 227
           readonly
228 228
           label="风险告知人签字"
229 229
           label-width="200"
230
+          required
230 231
         ></van-field>
231 232
       </span>
232 233
       <!--      <van-button v-if="nextStepFlag===3" type="primary" @click="onClickSignatureFxgzr">点击签名</van-button>-->
@@ -242,6 +243,7 @@
242 243
            readonly
243 244
            label="其他作业人员签字"
244 245
            label-width="200"
246
+           required
245 247
          ></van-field>
246 248
       </span>
247 249
       <!--      <van-button v-if="nextStepFlag===4" type="primary" @click="onClickSignatureQtzyr">点击签名</van-button>-->
@@ -338,6 +340,7 @@
338 340
         readonly
339 341
         label="是否穿戴好劳保护品(拍照)"
340 342
         label-width="200"
343
+        required
341 344
       ></van-field>
342 345
       <AttachmentS3Image :f-id="form.protectClothing+'protectClothin'" />
343 346
 </span>
@@ -376,6 +379,7 @@
376 379
         readonly
377 380
         label="作业开始照片(拍照)"
378 381
         label-width="200"
382
+        required
379 383
       ></van-field>
380 384
       <AttachmentS3Image :f-id="form.startPhoto+'startPhoto'" />
381 385
 </span>
@@ -386,6 +390,7 @@
386 390
           readonly
387 391
           label="作业人员签退"
388 392
           label-width="200"
393
+          required
389 394
         ></van-field>
390 395
         <!--        <van-button v-if="nextStepFlag===7" type="primary" @click="onClickSignatureZyryqt">点击签名</van-button>-->
391 396
         <!--      <HandwrittenSignature v-if="onClickSignatureZyryqtFlag==='true'" :f-id="form.otherSignatureEnd+'ldseend'" @resetQMFlag="resetFlagFlase"></HandwrittenSignature>-->
@@ -397,6 +402,7 @@
397 402
   readonly
398 403
   label="作业结束照片(拍照)"
399 404
   label-width="200"
405
+  required
400 406
 ></van-field>
401 407
       <AttachmentS3Image :f-id="form.endPhoto" />
402 408
       </span>
@@ -438,6 +444,7 @@
438 444
           readonly
439 445
           label="作业负责人签名"
440 446
           label-width="200"
447
+          required
441 448
         ></van-field>
442 449
         <!--        <van-button v-if="nextStepFlag===10" type="primary" @click="onClickSignatureZyfzrqt">点击签名</van-button>-->
443 450
         <!--      <HandwrittenSignature v-if="onClickSignatureZyfzrqtFlag==='true'" :f-id="form.leaderSignatureEnd+'zyfzrqme'" @resetQMFlag="resetFlagFlase"></HandwrittenSignature>-->
@@ -610,26 +617,79 @@ const baocun = async () => {
610 617
     form.value.workStatus = '作业结束';
611 618
     form.value.realEnd = new Date();
612 619
   }
620
+  /**添加必填项验证**/
621
+  // 验证风险告知人签字
622
+  if (form.value.stepNumber === 3) {
623
+    const fxgzrCount = await getSizeByFid(form.value.leaderSignatureStart + 'fxgzrqzs');
624
+    if (fxgzrCount === 0) {
625
+      showToast({
626
+        type: 'fail',
627
+        message: '请完成风险告知人签字!'
628
+      });
629
+      return;
630
+    }
631
+  }
632
+  // 验证其他作业人员签字
633
+  if (form.value.stepNumber === 4) {
634
+    const qtzyrCount = await getSizeByFid(form.value.otherSignatureStart + 'qtzyrqms');
635
+    if (qtzyrCount === 0) {
636
+      showToast({
637
+        type: 'fail',
638
+        message: '请完成其他作业人员签字!'
639
+      });
640
+      return;
641
+    }
642
+  }
643
+  // 验证劳保护品拍照
644
+  if (form.value.stepNumber === 5) {
645
+    const protectCount = await getSizeByFid(form.value.protectClothing + 'protectClothin');
646
+    if (protectCount === 0) {
647
+      showToast({
648
+        type: 'fail',
649
+        message: '请拍摄劳保护品照片!'
650
+      });
651
+      return;
652
+    }
653
+  }
654
+  // 验证作业开始拍照
655
+  if (form.value.stepNumber === 6) {
656
+    const startPhotoCount = await getSizeByFid(form.value.startPhoto + 'startPhoto');
657
+    if (startPhotoCount === 0) {
658
+      showToast({
659
+        type: 'fail',
660
+        message: '请拍摄作业开始照片!'
661
+      });
662
+      return;
663
+    }
664
+  }
613 665
   /**添加人员确认判断**/
614 666
   if (form.value.stepNumber===7) {
615
-    let start = await getSizeByFid(form.value.otherSignatureStart+'qtzyrqms')
667
+    // 验证作业人员签退
616 668
     let end = await getSizeByFid(form.value.otherSignature+'ldseend')
669
+    if(end===0){
670
+      showToast({
671
+        type: 'fail',
672
+        message: '请完成作业人员签退!'
673
+      });
674
+      return
675
+    }
676
+    // 验证作业结束照片
617 677
     let flag = await getSizeByFid(form.value.endPhoto)
618 678
     if(flag===0){
619 679
       showToast({
620 680
         type: 'fail',
621
-        message: '请拍摄相关照片!'
681
+        message: '请拍摄作业结束照片!'
622 682
       });
623
-      console.log('无所畏惧');
624 683
       return
625 684
     }
685
+    // 验证签退人数是否匹配
686
+    let start = await getSizeByFid(form.value.otherSignatureStart+'qtzyrqms')
626 687
     console.log('start',start,'end',end);
627 688
     if(start>end){
628 689
       showToast({
629 690
         type: 'fail',
630 691
         message: '请确认签退人员是否全部签退!'
631 692
       });
632
-      console.log('无所畏惧1');
633 693
       return
634 694
     }
635 695
   }
@@ -644,6 +704,23 @@ const baocun = async () => {
644 704
     }
645 705
   }
646 706
   if (form.value.stepNumber===10) {
707
+    // 验证作业负责人签退(只能一个人)
708
+    const leaderSignOutCount = await getSizeByFid(form.value.leaderSignatureEnd + 'zyfzrqme');
709
+    if (leaderSignOutCount === 0) {
710
+      showToast({
711
+        type: 'fail',
712
+        message: '请完成作业负责人签退!'
713
+      });
714
+      return;
715
+    }
716
+    if (leaderSignOutCount > 1) {
717
+      showToast({
718
+        type: 'fail',
719
+        message: '作业负责人只能填写一个人!'
720
+      });
721
+      return;
722
+    }
723
+    // 验证签退人数是否匹配
647 724
     if(await getSizeByFid(form.value.leaderSignatureStart+'fxgzrqzs')>await getSizeByFid(form.value.leaderSignatureEnd+'zyfzrqme')){
648 725
       showToast({
649 726
         type: 'fail',

+ 656
- 5
src/view/safeCheck/safeCheck_edit/index.vue ファイルの表示

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
   <div class="h5-container">
3
-    <van-nav-bar title="隐患登记" @click-left="onClickLeft">
3
+    <van-nav-bar title="检查任务编辑" @click-left="onClickLeft">
4 4
     </van-nav-bar>
5 5
     <van-form @submit="baocun" ref="formRef">
6 6
         <van-field
@@ -91,7 +91,45 @@
91 91
           maxlength="1000"
92 92
           show-word-limit
93 93
           rows="4"
94
-        />
94
+        >
95
+          <template #right-icon>
96
+            <van-icon 
97
+              name="volume-o" 
98
+              size="20" 
99
+              :color="isRecording ? '#ee0a24' : '#1989fa'"
100
+              @click="toggleVoiceInput"
101
+              style="margin-right: 8px; cursor: pointer;"
102
+            />
103
+          </template>
104
+        </van-field>
105
+
106
+        <!-- AI判断按钮 -->
107
+        <div style="margin: 10px 0; padding: 0 16px;">
108
+          <van-button 
109
+            type="primary" 
110
+            size="small" 
111
+            block
112
+            :loading="aiJudging"
113
+            @click="handleAIJudge"
114
+            :disabled="!form.checkResult || form.checkResult.trim() === ''"
115
+          >
116
+            <van-icon name="chat-o" style="margin-right: 5px;" />
117
+            {{ aiJudging ? 'AI判断中...' : 'AI研判' }}
118
+          </van-button>
119
+        </div>
120
+
121
+        <!-- AI判断结果显示 -->
122
+        <van-field
123
+          v-if="aiJudgeResult"
124
+          border
125
+          readonly
126
+          label="AI判断结果"
127
+          :colon="true"
128
+        >
129
+          <template #input>
130
+            <div class="ai-judge-result" v-html="renderedJudgeResult"></div>
131
+          </template>
132
+        </van-field>
95 133
 
96 134
         <van-field
97 135
           border
@@ -145,10 +183,69 @@
145 183
 
146 184
   <van-dialog v-model:show="showDialogVisible" title="删除文件" show-cancel-button
147 185
               confirm-button-color="#ee0124" message="确定删除该文件吗?" @confirm="onDelete" />
186
+
187
+  <!-- 语音识别弹窗 -->
188
+  <van-dialog
189
+    v-model:show="showVoiceDialog"
190
+    title="语音输入"
191
+    :show-cancel-button="false"
192
+    :show-confirm-button="false"
193
+    :close-on-click-overlay="true"
194
+    width="90%"
195
+  >
196
+    <div style="padding: 20px; text-align: center;">
197
+      <van-icon 
198
+        name="volume-o" 
199
+        size="60" 
200
+        :color="isRecording ? '#ee0a24' : '#1989fa'"
201
+        :class="{ 'pulsing': isRecording }"
202
+      />
203
+      <div style="margin-top: 20px; font-size: 16px; color: #323233;">
204
+        {{ isRecording ? '正在录音,请说话...' : voiceErrorMessage || '点击开始录音' }}
205
+      </div>
206
+      <!-- 错误信息显示 -->
207
+      <div v-if="voiceErrorMessage && !isRecording" style="margin-top: 15px; padding: 10px; background: #fff7e6; border-radius: 4px; border-left: 3px solid #ff9800;">
208
+        <div style="font-size: 14px; color: #ff9800; text-align: left;">
209
+          {{ voiceErrorMessage }}
210
+        </div>
211
+      </div>
212
+      <!-- 识别结果显示 -->
213
+      <div v-if="recognizedText" style="margin-top: 15px; padding: 10px; background: #f7f8fa; border-radius: 4px; text-align: left; max-height: 150px; overflow-y: auto;">
214
+        <div style="font-size: 14px; color: #666; margin-bottom: 5px;">识别结果:</div>
215
+        <div style="font-size: 14px; color: #323233; word-break: break-all;">{{ recognizedText }}</div>
216
+      </div>
217
+      <div style="margin-top: 20px; display: flex; justify-content: center; gap: 10px;">
218
+        <van-button 
219
+          v-if="!isRecording" 
220
+          type="primary" 
221
+          size="small" 
222
+          @click="startRecognition"
223
+        >
224
+          开始录音
225
+        </van-button>
226
+        <van-button 
227
+          v-else 
228
+          type="danger" 
229
+          size="small" 
230
+          @click="stopRecognition"
231
+        >
232
+          停止录音
233
+        </van-button>
234
+        <van-button 
235
+          v-if="recognizedText" 
236
+          type="primary" 
237
+          size="small" 
238
+          @click="confirmVoiceText"
239
+        >
240
+          确认使用
241
+        </van-button>
242
+      </div>
243
+    </div>
244
+  </van-dialog>
148 245
 </template>
149 246
 
150 247
 <script setup>
151
-import { ref, reactive, onMounted, getCurrentInstance, nextTick } from 'vue';
248
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, onUnmounted, computed } from 'vue';
152 249
 import { closeToast, Dialog, showFailToast, showLoadingToast, showSuccessToast, showToast } from 'vant';
153 250
 
154 251
 const { proxy } = getCurrentInstance();
@@ -166,6 +263,7 @@ const form = ref({
166 263
   notes: '',
167 264
   ifFlag: '',
168 265
   checkResult: '',
266
+  aiJudge: '', // AI判断结果字段
169 267
   hdId: '',
170 268
   hdSubmitTime: ''
171 269
 });
@@ -183,7 +281,13 @@ onMounted(async () => {
183 281
 
184 282
     if (response.data.code === 0) {
185 283
       form.value = response.data.data;
186
-      console.log('查询结果', response.data.data);
284
+      // 如果有 AI 判断结果,同步到显示变量
285
+      if (form.value.aiJudge) {
286
+        aiJudgeResult.value = form.value.aiJudge;
287
+      } else {
288
+        // 如果没有AI判断结果,清空显示
289
+        aiJudgeResult.value = '';
290
+      }
187 291
     } else {
188 292
       showToast({
189 293
         message: '操作失败!' + response.data.msg
@@ -236,14 +340,20 @@ onMounted(async () => {
236 340
     onClickLeft();
237 341
   };
238 342
   const baocun = async () => {
343
+    // 强制同步 AI 判断结果到 form(确保数据不丢失)
344
+    if (aiJudgeResult.value) {
345
+      form.value.aiJudge = aiJudgeResult.value;
346
+    }
347
+    
239 348
     var url = '/sgsafe/CheckResultItem/save';
240 349
     var param = {
241 350
       json: JSON.stringify(form.value)
242 351
     };
352
+    
243 353
     proxy.$axios.post(url, param).then(response => {
244 354
       if (response.data.code === 0) {
245 355
         const data = response.data.data;
246
-        console.log('插入时返回的数据', data)
356
+        console.log('保存成功,返回的数据:', data);
247 357
         onClickLeft()
248 358
       } else {
249 359
         showToast({
@@ -356,7 +466,417 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
356 466
       }
357 467
     });
358 468
   };
469
+
470
+  /***********************语音识别功能******************************/
471
+  // 语音识别相关状态
472
+  const isRecording = ref(false);
473
+  const showVoiceDialog = ref(false);
474
+  const recognizedText = ref('');
475
+  const voiceErrorMessage = ref('');
476
+  let recognition = null;
477
+
478
+  // 检查浏览器是否支持语音识别
479
+  function checkSpeechRecognitionSupport() {
480
+    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
481
+    if (!SpeechRecognition) {
482
+      showToast({
483
+        type: 'fail',
484
+        message: '您的浏览器不支持语音识别功能'
485
+      });
486
+      return false;
487
+    }
488
+    return true;
489
+  }
490
+
491
+  // 初始化语音识别
492
+  function initSpeechRecognition() {
493
+    if (!checkSpeechRecognitionSupport()) {
494
+      return;
495
+    }
496
+
497
+    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
498
+    recognition = new SpeechRecognition();
499
+    
500
+    // 设置语言为中文
501
+    recognition.lang = 'zh-CN';
502
+    // 连续识别
503
+    recognition.continuous = true;
504
+    // 返回临时结果
505
+    recognition.interimResults = true;
506
+
507
+    // 识别开始
508
+    recognition.onstart = () => {
509
+      isRecording.value = true;
510
+      recognizedText.value = '';
511
+      voiceErrorMessage.value = '';
512
+    };
513
+
514
+    // 识别结果
515
+    recognition.onresult = (event) => {
516
+      let interimTranscript = '';
517
+      let finalTranscript = '';
518
+
519
+      for (let i = event.resultIndex; i < event.results.length; i++) {
520
+        const transcript = event.results[i][0].transcript;
521
+        if (event.results[i].isFinal) {
522
+          finalTranscript += transcript;
523
+        } else {
524
+          interimTranscript += transcript;
525
+        }
526
+      }
527
+
528
+      recognizedText.value = finalTranscript || interimTranscript;
529
+    };
530
+
531
+    // 识别错误
532
+    recognition.onerror = (event) => {
533
+      console.error('语音识别错误:', event.error);
534
+      isRecording.value = false;
535
+      
536
+      let errorMessage = '语音识别出错';
537
+      switch (event.error) {
538
+        case 'no-speech':
539
+          errorMessage = '未检测到语音,请重试';
540
+          break;
541
+        case 'audio-capture':
542
+          errorMessage = '无法访问麦克风,请检查权限';
543
+          break;
544
+        case 'not-allowed':
545
+          errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许';
546
+          break;
547
+        case 'network':
548
+          errorMessage = '网络错误,请检查网络连接';
549
+          break;
550
+        case 'aborted':
551
+          errorMessage = '语音识别已中断';
552
+          break;
553
+        case 'service-not-allowed':
554
+          errorMessage = '语音识别服务不可用';
555
+          break;
556
+        default:
557
+          errorMessage = `语音识别出错: ${event.error}`;
558
+      }
559
+      
560
+      voiceErrorMessage.value = errorMessage;
561
+      
562
+      showToast({
563
+        type: 'fail',
564
+        message: errorMessage
565
+      });
566
+    };
567
+
568
+    // 识别结束
569
+    recognition.onend = () => {
570
+      isRecording.value = false;
571
+    };
572
+  }
573
+
574
+  // 切换语音输入
575
+  function toggleVoiceInput() {
576
+    if (!checkSpeechRecognitionSupport()) {
577
+      return;
578
+    }
579
+
580
+    if (!recognition) {
581
+      initSpeechRecognition();
582
+    }
583
+
584
+    showVoiceDialog.value = true;
585
+    recognizedText.value = '';
586
+    voiceErrorMessage.value = '';
587
+  }
588
+
589
+  // 开始识别
590
+  function startRecognition() {
591
+    if (!recognition) {
592
+      initSpeechRecognition();
593
+    }
594
+
595
+    // 清除之前的错误信息
596
+    voiceErrorMessage.value = '';
597
+    recognizedText.value = '';
598
+
599
+    try {
600
+      recognition.start();
601
+    } catch (error) {
602
+      console.error('启动语音识别失败:', error);
603
+      voiceErrorMessage.value = '启动语音识别失败,请重试';
604
+      showToast({
605
+        type: 'fail',
606
+        message: '启动语音识别失败,请重试'
607
+      });
608
+    }
609
+  }
610
+
611
+  // 停止识别
612
+  function stopRecognition() {
613
+    if (recognition && isRecording.value) {
614
+      recognition.stop();
615
+    }
616
+  }
617
+
618
+  // 确认使用识别的文本
619
+  function confirmVoiceText() {
620
+    if (recognizedText.value) {
621
+      // 如果已有内容,追加;否则直接设置
622
+      if (form.value.checkResult) {
623
+        form.value.checkResult += recognizedText.value;
624
+      } else {
625
+        form.value.checkResult = recognizedText.value;
626
+      }
627
+      recognizedText.value = '';
628
+      voiceErrorMessage.value = '';
629
+      showVoiceDialog.value = false;
630
+      showToast({
631
+        type: 'success',
632
+        message: '已添加语音识别内容'
633
+      });
634
+    }
635
+  }
636
+
637
+  // 组件卸载时清理
638
+  onUnmounted(() => {
639
+    if (recognition && isRecording.value) {
640
+      recognition.stop();
641
+    }
642
+  });
643
+
644
+  /***********************AI判断法律法规功能******************************/
645
+  // AI判断相关状态
646
+  const aiJudging = ref(false);
647
+  const aiJudgeResult = ref('');
359 648
   
649
+  // Markdown 渲染库
650
+  let marked = null;
651
+  let DOMPurify = null;
652
+  
653
+  // 初始化 Markdown 库
654
+  async function initMarkdownLibs() {
655
+    try {
656
+      // 动态导入 marked
657
+      const markedModule = await import('marked');
658
+      marked = markedModule.marked || markedModule.default || markedModule;
659
+      
660
+      // 动态导入 DOMPurify
661
+      const dompurifyModule = await import('dompurify');
662
+      DOMPurify = dompurifyModule.default || dompurifyModule;
663
+    } catch (error) {
664
+      console.warn('Markdown libraries not available, using plain text', error);
665
+      marked = {
666
+        parse: (text) => text
667
+      };
668
+      DOMPurify = {
669
+        sanitize: (html) => html
670
+      };
671
+    }
672
+  }
673
+  
674
+  // 初始化 Markdown 库
675
+  initMarkdownLibs();
676
+  
677
+  // 渲染后的判断结果
678
+  const renderedJudgeResult = computed(() => {
679
+    if (!aiJudgeResult.value) return '';
680
+    
681
+    try {
682
+      if (!marked || !DOMPurify) {
683
+        // 如果库还没加载,先返回纯文本,但格式化一下
684
+        return formatPlainText(aiJudgeResult.value);
685
+      }
686
+      
687
+      // 使用 marked 解析 markdown
688
+      const html = marked.parse ? marked.parse(aiJudgeResult.value) : marked(aiJudgeResult.value);
689
+      // 使用 DOMPurify 清理 HTML
690
+      return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
691
+    } catch (error) {
692
+      console.error('Markdown解析错误:', error);
693
+      return formatPlainText(aiJudgeResult.value);
694
+    }
695
+  });
696
+  
697
+  // 格式化纯文本(当 Markdown 库不可用时)
698
+  function formatPlainText(text) {
699
+    if (!text) return '';
700
+    
701
+    // 将 Markdown 格式转换为 HTML
702
+    let formatted = text
703
+      // 标题(按顺序处理,从多级到单级)
704
+      .replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
705
+      .replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
706
+      .replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
707
+      .replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
708
+      // 粗体(支持 **text** 和 **text** 格式)
709
+      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
710
+      // 斜体
711
+      .replace(/\*(.+?)\*/g, '<em>$1</em>')
712
+      // 列表项(数字列表)
713
+      .replace(/^\d+\.\s+(.+)$/gm, '<p class="list-item">$1</p>')
714
+      // 列表项(无序列表)
715
+      .replace(/^[-*]\s+(.+)$/gm, '<p class="list-item">• $1</p>')
716
+      // 处理特殊格式:条款号、具体内容等
717
+      .replace(/\*\*条款号\*\*:\s*(.+)/g, '<p class="article-num"><strong>条款号:</strong>$1</p>')
718
+      .replace(/\*\*具体内容\*\*:\s*(.+)/g, '<p class="article-content"><strong>具体内容:</strong>$1</p>')
719
+      // 多个连续换行转换为段落分隔
720
+      .replace(/\n\n+/g, '</p><p>')
721
+      // 单个换行转换为换行符
722
+      .replace(/\n/g, '<br/>');
723
+    
724
+    // 确保每个段落都有正确的标签
725
+    formatted = formatted
726
+      .split('</p><p>')
727
+      .map(para => {
728
+        para = para.trim();
729
+        if (!para) return '';
730
+        if (para.startsWith('<h') || para.startsWith('<p') || para.startsWith('<ul') || para.startsWith('<ol')) {
731
+          return para;
732
+        }
733
+        return '<p>' + para + '</p>';
734
+      })
735
+      .filter(para => para)
736
+      .join('');
737
+    
738
+    // 包装整个内容
739
+    if (!formatted.startsWith('<h') && !formatted.startsWith('<p') && !formatted.startsWith('<ul')) {
740
+      formatted = '<p>' + formatted + '</p>';
741
+    }
742
+    
743
+    return formatted;
744
+  }
745
+
746
+  // 处理AI判断
747
+  async function handleAIJudge() {
748
+    if (!form.value.checkResult || form.value.checkResult.trim() === '') {
749
+      showToast({
750
+        type: 'fail',
751
+        message: '请先输入检查结果'
752
+      });
753
+      return;
754
+    }
755
+
756
+    aiJudging.value = true;
757
+    aiJudgeResult.value = '';
758
+
759
+    try {
760
+      const prompt = buildJudgePrompt(form.value.checkResult);
761
+      const messages = [{ role: 'user', content: prompt }];
762
+
763
+      await callAIStream(messages, (content, isEnd) => {
764
+        aiJudgeResult.value = content;
765
+        // 实时同步到 form.aiJudge
766
+        form.value.aiJudge = content;
767
+        
768
+        if (isEnd) {
769
+          aiJudging.value = false;
770
+          // 确保最终结果保存到 form
771
+          form.value.aiJudge = content;
772
+          showToast({
773
+            type: 'success',
774
+            message: 'AI判断完成'
775
+          });
776
+        }
777
+      });
778
+    } catch (error) {
779
+      console.error('AI判断失败:', error);
780
+      aiJudging.value = false;
781
+      showToast({
782
+        type: 'fail',
783
+        message: 'AI判断失败,请重试'
784
+      });
785
+    }
786
+  }
787
+
788
+  // 构造判断提示词
789
+  function buildJudgePrompt(checkResult) {
790
+    const prompt = `请根据以下检查结果内容,判断违反了哪一条法律法规,并详细说明:
791
+
792
+检查结果内容:
793
+${checkResult}
794
+
795
+请提供以下内容:
796
+1. 明确指出违反了哪些法律法规(包括法律名称、条款号、具体条款内容)
797
+2. 说明为什么该检查结果违反了这些法律法规
798
+3. 如果可能,提供相关的处罚依据或整改建议
799
+
800
+请用清晰、专业的语言进行回答,确保法律法规名称和条款号准确无误。`;
801
+    
802
+    return prompt;
803
+  }
804
+
805
+  // 调用AI流式接口
806
+  async function callAIStream(messageHistory, callback) {
807
+    try {
808
+      const url = `${import.meta.env.VITE_BASE_API}/sgsafe/deepseek/chat-stream`;
809
+      
810
+      const response = await fetch(url, {
811
+        method: 'POST',
812
+        headers: {
813
+          'Content-Type': 'application/json',
814
+          'token': localStorage.getItem('token') || '',
815
+          'userId': localStorage.getItem('userId') || ''
816
+        },
817
+        body: JSON.stringify({ messages: messageHistory })
818
+      });
819
+      
820
+      if (!response.ok) {
821
+        throw new Error(`HTTP error! status: ${response.status}`);
822
+      }
823
+      
824
+      if (!response.body) {
825
+        throw new Error('ReadableStream not supported');
826
+      }
827
+      
828
+      const reader = response.body.getReader();
829
+      const decoder = new TextDecoder('utf-8');
830
+      let buffer = '';
831
+      let fullResponse = '';
832
+      
833
+      while (true) {
834
+        const { done, value } = await reader.read();
835
+        if (done) {
836
+          callback(fullResponse, true);
837
+          break;
838
+        }
839
+        
840
+        const chunk = decoder.decode(value, { stream: true });
841
+        buffer += chunk;
842
+        
843
+        const lines = buffer.split('\n');
844
+        buffer = lines.pop();
845
+        
846
+        for (const line of lines) {
847
+          if (line.trim() === '' || !line.startsWith('data:')) continue;
848
+          
849
+          const data = line.slice(5).trim();
850
+          if (data === '[DONE]') {
851
+            callback(fullResponse, true);
852
+            return;
853
+          }
854
+          
855
+          try {
856
+            const jsonData = JSON.parse(data);
857
+            if (jsonData.choices?.[0]?.delta) {
858
+              let content = '';
859
+              if (jsonData.choices[0].delta.reasoning_content) {
860
+                content += jsonData.choices[0].delta.reasoning_content;
861
+              }
862
+              if (jsonData.choices[0].delta.content) {
863
+                content += jsonData.choices[0].delta.content;
864
+              }
865
+              if (content) {
866
+                fullResponse += content;
867
+                callback(fullResponse, false);
868
+              }
869
+            }
870
+          } catch (e) {
871
+            console.error('JSON解析错误:', e);
872
+          }
873
+        }
874
+      }
875
+    } catch (error) {
876
+      console.error('AI调用失败:', error);
877
+      throw error;
878
+    }
879
+  }
360 880
 
361 881
 </script>
362 882
 
@@ -369,4 +889,135 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
369 889
   padding: 10px;
370 890
 }
371 891
 
892
+.pulsing {
893
+  animation: pulse 1.5s ease-in-out infinite;
894
+}
895
+
896
+@keyframes pulse {
897
+  0%, 100% {
898
+    transform: scale(1);
899
+    opacity: 1;
900
+  }
901
+  50% {
902
+    transform: scale(1.2);
903
+    opacity: 0.7;
904
+  }
905
+}
906
+
907
+/* AI判断结果样式 */
908
+.ai-judge-result {
909
+  padding: 15px 0;
910
+  color: #323233;
911
+  line-height: 1.8;
912
+  font-size: 14px;
913
+  word-break: break-word;
914
+}
915
+
916
+.ai-judge-result :deep(h1) {
917
+  font-size: 18px;
918
+  font-weight: 600;
919
+  color: #303133;
920
+  margin: 15px 0 10px 0;
921
+  padding-bottom: 8px;
922
+  border-bottom: 2px solid #e4e7ed;
923
+}
924
+
925
+.ai-judge-result :deep(h2) {
926
+  font-size: 16px;
927
+  font-weight: 600;
928
+  color: #303133;
929
+  margin: 15px 0 10px 0;
930
+  padding-bottom: 6px;
931
+  border-bottom: 1px solid #e4e7ed;
932
+}
933
+
934
+.ai-judge-result :deep(h3) {
935
+  font-size: 15px;
936
+  font-weight: 600;
937
+  color: #409eff;
938
+  margin: 12px 0 8px 0;
939
+}
940
+
941
+.ai-judge-result :deep(p) {
942
+  margin: 10px 0;
943
+  line-height: 1.8;
944
+  color: #606266;
945
+}
946
+
947
+.ai-judge-result :deep(p.list-item) {
948
+  margin: 6px 0;
949
+  padding-left: 8px;
950
+  position: relative;
951
+}
952
+
953
+.ai-judge-result :deep(p.article-num) {
954
+  margin: 8px 0;
955
+  padding: 8px 12px;
956
+  background: #f0f9ff;
957
+  border-left: 3px solid #409eff;
958
+  border-radius: 4px;
959
+}
960
+
961
+.ai-judge-result :deep(p.article-content) {
962
+  margin: 8px 0;
963
+  padding: 10px 12px;
964
+  background: #fafafa;
965
+  border-radius: 4px;
966
+  line-height: 1.8;
967
+}
968
+
969
+.ai-judge-result :deep(strong) {
970
+  font-weight: 600;
971
+  color: #303133;
972
+}
973
+
974
+.ai-judge-result :deep(ul),
975
+.ai-judge-result :deep(ol) {
976
+  margin: 10px 0;
977
+  padding-left: 25px;
978
+}
979
+
980
+.ai-judge-result :deep(li) {
981
+  margin: 6px 0;
982
+  line-height: 1.8;
983
+  color: #606266;
984
+}
985
+
986
+.ai-judge-result :deep(blockquote) {
987
+  margin: 10px 0;
988
+  padding: 10px 15px;
989
+  background: #f5f7fa;
990
+  border-left: 4px solid #409eff;
991
+  border-radius: 4px;
992
+}
993
+
994
+.ai-judge-result :deep(code) {
995
+  background: #f5f7fa;
996
+  padding: 2px 6px;
997
+  border-radius: 3px;
998
+  font-family: 'Courier New', monospace;
999
+  font-size: 13px;
1000
+  color: #e6a23c;
1001
+}
1002
+
1003
+.ai-judge-result :deep(pre) {
1004
+  background: #f5f7fa;
1005
+  padding: 12px;
1006
+  border-radius: 4px;
1007
+  overflow-x: auto;
1008
+  margin: 10px 0;
1009
+}
1010
+
1011
+.ai-judge-result :deep(pre code) {
1012
+  background: transparent;
1013
+  padding: 0;
1014
+  color: #303133;
1015
+}
1016
+
1017
+.ai-judge-result :deep(hr) {
1018
+  border: none;
1019
+  border-top: 1px solid #e4e7ed;
1020
+  margin: 15px 0;
1021
+}
1022
+
372 1023
 </style>

読み込み中…
キャンセル
保存