From 5d132317d0ded846812d5009a560be2fd5381478 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 30 七月 2026 15:51:05 +0800
Subject: [PATCH] fix: 安全培训考核签到修改
---
src/views/safeProduction/safetyTrainingAssessment/index.vue | 407 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 371 insertions(+), 36 deletions(-)
diff --git a/src/views/safeProduction/safetyTrainingAssessment/index.vue b/src/views/safeProduction/safetyTrainingAssessment/index.vue
index 98e5d31..9daa354 100644
--- a/src/views/safeProduction/safetyTrainingAssessment/index.vue
+++ b/src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -46,7 +46,21 @@
:tableLoading="tableLoading"
@pagination="pagination"
:total="page.total"
- :rowClassName="getRowClass"></PIMTable>
+ :rowClassName="getRowClass">
+ <template #courseCode="{ row }">
+ <el-button v-if="row.courseCode"
+ type="primary"
+ link
+ class="course-code-link"
+ @click.stop="openCourseDetail(row)">
+ {{ row.courseCode }}
+ </el-button>
+ <span v-else>-</span>
+ </template>
+ <template #signStatus="{ row }">
+ <span class="sign-summary-text">{{ formatSignSummary(row) }}</span>
+ </template>
+ </PIMTable>
</div>
<!-- 鏂板/缂栬緫鐭ヨ瘑寮圭獥 -->
<el-dialog v-model="dialogVisible"
@@ -115,8 +129,19 @@
<el-col :span="12">
<el-form-item label="鍙傚姞瀵硅薄"
prop="participants">
- <el-input v-model="form.participants"
- placeholder="璇疯緭鍏ュ弬鍔犲璞�" />
+ <el-select v-model="participantSelection"
+ placeholder="璇烽�夋嫨鍙傚姞瀵硅薄"
+ filterable
+ multiple
+ collapse-tags
+ collapse-tags-tooltip
+ clearable
+ style="width: 100%">
+ <el-option v-for="item in userOptions"
+ :key="item.userId"
+ :label="getUserLabel(item)"
+ :value="getUserLabel(item)" />
+ </el-select>
</el-form-item>
</el-col>
</el-row>
@@ -131,21 +156,20 @@
<el-col :span="12">
<el-form-item label="鍩硅璁插笀"
prop="trainingLecturer">
- <el-input v-model="form.trainingLecturer"
- placeholder="璇疯緭鍏ュ煿璁甯�" />
+ <el-select v-model="form.trainingLecturer"
+ placeholder="璇烽�夋嫨鍩硅璁插笀"
+ filterable
+ clearable
+ style="width: 100%">
+ <el-option v-for="item in userOptions"
+ :key="item.userId"
+ :label="getUserLabel(item)"
+ :value="getUserLabel(item)" />
+ </el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="璇剧▼瀛﹀垎"
- prop="projectCredits">
- <el-input v-model="form.projectCredits"
- type="number"
- min="0"
- placeholder="璇疯緭鍏ヨ绋嬪鍒�" />
- </el-form-item>
- </el-col>
<el-col :span="12">
<el-form-item label="鍩硅鏂瑰紡"
prop="trainingMode">
@@ -184,6 +208,96 @@
<el-button type="primary"
@click="submitForm">纭畾</el-button>
<el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 璇剧▼璇︽儏寮圭獥 -->
+ <el-dialog v-model="courseDetailVisible"
+ title="璇剧▼璇︽儏"
+ width="860px"
+ :close-on-click-modal="false">
+ <div v-loading="courseDetailLoading"
+ class="course-detail">
+ <el-descriptions border
+ :column="2"
+ class="course-detail-descriptions">
+ <el-descriptions-item label="璇剧▼缂栧彿">
+ {{ formatDetailValue(courseDetail.courseCode) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="getTrainingStateTagType(courseDetail.state)">
+ {{ getTrainingStateLabel(courseDetail.state) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鏃ユ湡">
+ {{ formatDetailValue(courseDetail.trainingDate) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鏃堕棿">
+ {{ formatTrainingTimeRange(courseDetail) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鐩爣">
+ {{ formatDetailValue(courseDetail.trainingObjectives) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鍐呭">
+ {{ formatDetailValue(courseDetail.trainingContent) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙傚姞瀵硅薄">
+ {{ formatDetailValue(courseDetail.participants) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅璁插笀">
+ {{ formatDetailValue(courseDetail.trainingLecturer) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鏂瑰紡">
+ {{ formatDetailValue(getTrainingModeLabel(courseDetail.trainingMode)) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鍦扮偣">
+ {{ formatDetailValue(courseDetail.placeTraining) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇炬椂">
+ {{ formatDetailValue(courseDetail.classHour) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="绛惧埌鎯呭喌">
+ {{ formatSignSummary(courseDetail) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="闄勪欢鍒楄〃">
+ <el-button type="primary"
+ link
+ :disabled="!courseDetail.id"
+ @click="openFileDialog(courseDetail)">
+ 鏌ョ湅闄勪欢
+ </el-button>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div class="classtitle course-sign-title">绛惧埌鎯呭喌</div>
+ <div class="course-sign-summary">
+ <el-tag type="success">宸茬鍒� {{ getSignedCount(courseDetail) }}</el-tag>
+ <el-tag type="info">鏈鍒� {{ getUnsignedCount(courseDetail) }}</el-tag>
+ </div>
+ <el-table :data="courseSignRows"
+ border
+ fit
+ stripe
+ empty-text="鏆傛棤鍙傚姞瀵硅薄"
+ class="course-sign-table">
+ <el-table-column prop="name"
+ label="濮撳悕"
+ min-width="140" />
+ <el-table-column prop="phonenumber"
+ label="鐢佃瘽鍙风爜"
+ min-width="140" />
+ <el-table-column label="绛惧埌鐘舵��"
+ width="120">
+ <template #default="scope">
+ <el-tag :type="scope.row.signed ? 'success' : 'info'">
+ {{ scope.row.signed ? "宸茬鍒�" : "鏈鍒�" }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="courseDetailVisible = false">鍏抽棴</el-button>
</span>
</template>
</el-dialog>
@@ -229,9 +343,6 @@
</el-descriptions-item>
<el-descriptions-item label="璇炬椂:">
{{ currentKnowledge.classHour }}
- </el-descriptions-item>
- <el-descriptions-item label="璇剧▼瀛﹀垎:">
- {{ currentKnowledge.projectCredits }}
</el-descriptions-item>
<el-descriptions-item label="鎶ュ悕浜烘暟:">
{{ currentKnowledge.nums }}
@@ -408,7 +519,7 @@
{ required: true, message: "璇疯緭鍏ュ煿璁唴瀹�", trigger: "blur" },
],
trainingLecturer: [
- { required: true, message: "璇疯緭鍏ュ煿璁甯�", trigger: "blur" },
+ { required: true, message: "璇烽�夋嫨鍩硅璁插笀", trigger: "change" },
],
classHour: [{ required: true, message: "璇疯緭鍏ヨ鏃�", trigger: "blur" }],
};
@@ -441,7 +552,6 @@
participants: "", // 鍙傚姞瀵硅薄
trainingContent: "", // 鍩硅鍐呭
trainingLecturer: "", // 鍩硅璁插笀
- projectCredits: "", // 椤圭洰瀛﹀垎
trainingMode: "", // 鍩硅鏂瑰紡
placeTraining: "", // 鍩硅鍦扮偣
classHour: "", // 璇炬椂
@@ -475,6 +585,60 @@
);
return item ? item.label : val;
};
+ const courseDetailVisible = ref(false);
+ const courseDetailLoading = ref(false);
+ const courseDetail = ref({});
+ const formatDetailValue = val =>
+ val === null || val === undefined || val === "" ? "-" : val;
+ const getTrainingStateLabel = val => {
+ const state = Number(val);
+ if (state === 0) return "鏈紑濮�";
+ if (state === 1) return "杩涜涓�";
+ if (state === 2) return "宸茬粨鏉�";
+ return "-";
+ };
+ const getTrainingStateTagType = val => {
+ const state = Number(val);
+ if (state === 0) return "info";
+ if (state === 1) return "success";
+ if (state === 2) return "warning";
+ return "info";
+ };
+ const formatTrainingTimeRange = row => {
+ const start = row?.openingTime || "";
+ const end = row?.endTime || "";
+ if (!start && !end) return "-";
+ return `${start || "-"} - ${end || "-"}`;
+ };
+ const normalizeCourseDetail = (data, row = {}) => ({
+ ...row,
+ ...data,
+ nums: data?.nums ?? row?.nums,
+ safeTrainingDetailsDtoList:
+ data?.safeTrainingDetailsDtoList ?? row?.safeTrainingDetailsDtoList,
+ });
+ const openCourseDetail = row => {
+ if (!row?.id) {
+ return;
+ }
+ courseDetail.value = normalizeCourseDetail(row);
+ courseDetailVisible.value = true;
+ courseDetailLoading.value = true;
+ safeTrainingGet({ id: row.id })
+ .then(res => {
+ if (res.code === 200) {
+ courseDetail.value = normalizeCourseDetail(res.data || {}, row);
+ } else {
+ proxy.$modal.msgError(res.msg || "鏌ヨ璇剧▼璇︽儏澶辫触");
+ }
+ })
+ .catch(err => {
+ proxy.$modal.msgError(err?.msg || "鏌ヨ璇剧▼璇︽儏澶辫触");
+ })
+ .finally(() => {
+ courseDetailLoading.value = false;
+ });
+ };
// 鍒囨崲tab鏌ヨ
const tabhandleQuery = val => {
searchForm.value.state = val.paneName;
@@ -492,11 +656,38 @@
]);
const fileList = ref([]);
- const getTrainingState = row => Number(row?.state);
+ const getTrainingState = row => {
+ if (row?.state === null || row?.state === undefined || row?.state === "") {
+ return undefined;
+ }
+ return Number(row.state);
+ };
const isNotStartedTraining = row => getTrainingState(row) === 0;
const isInProgressTraining = row => getTrainingState(row) === 1;
const isEndedTraining = row => getTrainingState(row) === 2;
const hasKnownTrainingState = row => [0, 1, 2].includes(getTrainingState(row));
+ const signedTrainingIds = ref(new Set());
+ const signingTrainingIds = ref(new Set());
+ const isSignedValue = val => val === true || val === 1 || val === "1";
+ const isCurrentUserSigned = row =>
+ isSignedValue(row?.signed) ||
+ isSignedValue(row?.currentUserSigned) ||
+ signedTrainingIds.value.has(row?.id);
+ const isSigningTraining = row => signingTrainingIds.value.has(row?.id);
+ const setSigningTraining = (id, signing) => {
+ const next = new Set(signingTrainingIds.value);
+ if (signing) {
+ next.add(id);
+ } else {
+ next.delete(id);
+ }
+ signingTrainingIds.value = next;
+ };
+ const markTrainingSigned = id => {
+ const next = new Set(signedTrainingIds.value);
+ next.add(id);
+ signedTrainingIds.value = next;
+ };
// 琛ㄦ牸鍒楅厤缃�
const tableColumn = ref([
@@ -504,7 +695,8 @@
label: "璇剧▼缂栧彿",
prop: "courseCode",
width: 150,
- showOverflowTooltip: true,
+ dataType: "slot",
+ slot: "courseCode",
},
{
label: "鍩硅鏃ユ湡",
@@ -550,12 +742,6 @@
showOverflowTooltip: true,
},
{
- label: "椤圭洰瀛﹀垎",
- prop: "projectCredits",
- width: 120,
- showOverflowTooltip: true,
- },
- {
label: "鍩硅鏂瑰紡",
prop: "trainingMode",
width: 120,
@@ -577,10 +763,11 @@
showOverflowTooltip: true,
},
{
- label: "鎶ュ悕浜烘暟",
+ label: "绛惧埌鎯呭喌",
prop: "nums",
- width: 120,
- showOverflowTooltip: true,
+ width: 170,
+ dataType: "slot",
+ slot: "signStatus",
},
{
dataType: "action",
@@ -592,10 +779,20 @@
{
name: "绛惧埌",
type: "text",
- show: isInProgressTraining,
+ show: row => isInProgressTraining(row) && !isCurrentUserSigned(row),
+ disabled: isSigningTraining,
+ loading: isSigningTraining,
clickFun: row => {
signIn(row);
},
+ },
+ {
+ name: "宸茬鍒�",
+ type: "text",
+ show: row => isInProgressTraining(row) && isCurrentUserSigned(row),
+ disabled: true,
+ color: "#909399",
+ clickFun: () => {},
},
{
name: "缂栬緫",
@@ -643,13 +840,97 @@
},
]);
const userList = ref([]);
+ const userOptions = computed(() => userList.value || []);
+ const getUserLabel = user => user?.nickName || user?.userName || "";
+ const parseUserNames = val => {
+ if (Array.isArray(val)) {
+ return val.filter(Boolean);
+ }
+ return String(val || "")
+ .split(/[,锛�;锛沒/)
+ .map(item => item.trim())
+ .filter(Boolean);
+ };
+ const getSignedDetailList = row =>
+ Array.isArray(row?.safeTrainingDetailsDtoList)
+ ? row.safeTrainingDetailsDtoList
+ : [];
+ const findUserByName = name =>
+ userList.value.find(user =>
+ [user?.nickName, user?.userName, getUserLabel(user)]
+ .filter(Boolean)
+ .includes(name)
+ );
+ const isSignedDetailMatched = (detail, name) =>
+ [detail?.nickName, detail?.userName].filter(Boolean).includes(name);
+ const getParticipantSignRows = row => {
+ const participantNames = parseUserNames(row?.participants);
+ const signedDetails = getSignedDetailList(row);
+ const usedSignedIndexes = new Set();
+ const rows = participantNames.map(name => {
+ const signedIndex = signedDetails.findIndex(
+ (detail, index) =>
+ !usedSignedIndexes.has(index) && isSignedDetailMatched(detail, name)
+ );
+ const signedDetail = signedIndex >= 0 ? signedDetails[signedIndex] : null;
+ const user = findUserByName(name);
+ if (signedIndex >= 0) {
+ usedSignedIndexes.add(signedIndex);
+ }
+ return {
+ name,
+ phonenumber: signedDetail?.phonenumber || user?.phonenumber || "-",
+ signed: !!signedDetail,
+ };
+ });
+ signedDetails.forEach((detail, index) => {
+ if (usedSignedIndexes.has(index)) {
+ return;
+ }
+ const name =
+ detail?.nickName ||
+ detail?.userName ||
+ (detail?.userId ? `鐢ㄦ埛${detail.userId}` : "");
+ if (!name) {
+ return;
+ }
+ rows.push({
+ name,
+ phonenumber: detail?.phonenumber || "-",
+ signed: true,
+ });
+ });
+ return rows;
+ };
+ const getSignedCount = row => {
+ if (Array.isArray(row?.safeTrainingDetailsDtoList)) {
+ return getParticipantSignRows(row).filter(item => item.signed).length;
+ }
+ return Number(row?.nums || 0);
+ };
+ const getUnsignedCount = row => {
+ const participantCount = parseUserNames(row?.participants).length;
+ if (!participantCount) {
+ return 0;
+ }
+ return Math.max(participantCount - getSignedCount(row), 0);
+ };
+ const formatSignSummary = row =>
+ `宸茬鍒� ${getSignedCount(row)} / 鏈鍒� ${getUnsignedCount(row)}`;
+ const courseSignRows = computed(() => getParticipantSignRows(courseDetail.value));
+ const participantSelection = computed({
+ get: () => parseUserNames(form.value.participants),
+ set: val => {
+ form.value.participants = (val || []).join(",");
+ },
+ });
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
getCurrentFactoryName();
getList();
startAutoRefresh();
userListNoPage().then(res => {
- userList.value = res.data;
+ userList.value = Array.isArray(res.data) ? res.data : [];
});
});
const endform = ref({
@@ -754,21 +1035,38 @@
};
const signIn = row => {
+ if (isCurrentUserSigned(row)) {
+ proxy.$modal.msgWarning("宸茬鍒帮紝璇峰嬁閲嶅绛惧埌");
+ return;
+ }
ElMessageBox.confirm("纭绛惧埌鍚楋紵", "鎻愮ず", {
confirmButtonText: "纭畾",
cancelButtonText: "鍙栨秷",
type: "warning",
}).then(() => {
+ setSigningTraining(row.id, true);
safeTrainingSign({
safeTrainingId: row.id,
userId: currentUserId.value,
}).then(res => {
if (res.code === 200) {
+ markTrainingSigned(row.id);
+ row.signed = true;
+ row.currentUserSigned = true;
proxy.$modal.msgSuccess("绛惧埌鎴愬姛");
getList();
} else {
+ if (res.msg?.includes("宸茬鍒�")) {
+ markTrainingSigned(row.id);
+ row.signed = true;
+ row.currentUserSigned = true;
+ }
proxy.$modal.msgError(res.msg || "绛惧埌澶辫触");
}
+ }).catch(err => {
+ proxy.$modal.msgError(err?.msg || "绛惧埌澶辫触");
+ }).finally(() => {
+ setSigningTraining(row.id, false);
});
});
};
@@ -986,8 +1284,13 @@
safeTrainingListPage({ ...page.value, ...searchForm.value })
.then(res => {
tableLoading.value = false;
- tableData.value = res.data.records;
- page.value.total = res.data.total;
+ const records = res.data?.records || [];
+ tableData.value = records.map(item => ({
+ ...item,
+ currentUserSigned:
+ isSignedValue(item.signed) || signedTrainingIds.value.has(item.id),
+ }));
+ page.value.total = res.data?.total || 0;
})
.catch(err => {
tableLoading.value = false;
@@ -1028,7 +1331,6 @@
participants: "", // 鍙傚姞瀵硅薄
trainingContent: "", // 鍩硅鍐呭
trainingLecturer: "", // 鍩硅璁插笀
- projectCredits: "", // 椤圭洰瀛﹀垎
trainingMode: "", // 鍩硅鏂瑰紡
placeTraining: "", // 鍩硅鍦扮偣
classHour: "", // 璇炬椂
@@ -1045,7 +1347,6 @@
participants: row.participants, // 鍙傚姞瀵硅薄
trainingContent: row.trainingContent, // 鍩硅鍐呭
trainingLecturer: row.trainingLecturer, // 鍩硅璁插笀
- projectCredits: row.projectCredits, // 椤圭洰瀛﹀垎
trainingMode: row.trainingMode, // 鍩硅鏂瑰紡
placeTraining: row.placeTraining, // 鍩硅鍦扮偣
classHour: row.classHour, // 璇炬椂
@@ -1304,6 +1605,40 @@
:deep(.danger-row td) {
color: #e95a66 !important;
}
+
+ .course-code-link {
+ padding: 0;
+ font-weight: 600;
+ }
+
+ .sign-summary-text {
+ color: #303133;
+ font-weight: 600;
+ }
+
+ .course-detail {
+ min-height: 220px;
+ padding: 4px 0;
+ }
+
+ .course-detail-descriptions {
+ width: 100%;
+ }
+
+ .course-sign-title {
+ margin-top: 22px;
+ }
+
+ .course-sign-summary {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 12px;
+ }
+
+ .course-sign-table {
+ width: 100%;
+ }
+
.classtitle {
font-size: 16px;
font-weight: 600;
--
Gitblit v1.9.3