| src/views/equipmentManagement/attendanceManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/personnelManagement/attendanceCheckin/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/views/equipmentManagement/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,403 @@ <template> <div class="app-container"> <!-- æå¡è¯ä»·æ¦è§ï¼æ¨¡æåå·¥ä¸ç»©è¯å --> <el-row :gutter="16" class="mb16"> <el-col :span="8"> <el-card shadow="never"> <div class="kpi-title">æ¬æå¹³åè¯å</div> <div class="kpi-value"> {{ overallAvgScore.toFixed(1) }} <span class="kpi-unit">å</span> </div> <el-rate v-model="overallAvgScore" disabled show-score score-template="{value} / 5" /> </el-card> </el-col> <el-col :span="8"> <el-card shadow="never"> <div class="kpi-title">å·²è¯ä»·ç»´ä¿®å·¥å</div> <div class="kpi-value"> {{ ratedCount }} <span class="kpi-unit">å</span> </div> </el-card> </el-col> <el-col :span="8"> <el-card shadow="never"> <div class="kpi-title">å¾ è¯ä»·ç»´ä¿®å·¥å</div> <div class="kpi-value kpi-warning"> {{ pendingCount }} <span class="kpi-unit">å</span> </div> </el-card> </el-col> </el-row> <!-- æ¥è¯¢æ¡ä»¶ï¼ç®¡çåæå·¥ç¨å¸ / å®¢æ· / æ¶é´è¿½æº¯è¯ä»· --> <div class="search_form"> <div> <span class="search_title">维修工ç¨å¸ï¼</span> <el-input v-model="searchForm.engineerName" placeholder="请è¾å ¥å·¥ç¨å¸å§å" style="width: 180px" clearable @keyup.enter.native="handleQuery" /> <span class="search_title ml10">客æ·åç§°ï¼</span> <el-input v-model="searchForm.customerName" placeholder="请è¾å ¥å®¢æ·åç§°" style="width: 180px" clearable @keyup.enter.native="handleQuery" /> <span class="search_title ml10">宿æ¶é´ï¼</span> <el-date-picker v-model="searchForm.dateRange" type="daterange" range-separator="è³" start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ" value-format="YYYY-MM-DD" format="YYYY-MM-DD" clearable /> <span class="search_title ml10">è¯ä»·ç¶æï¼</span> <el-select v-model="searchForm.status" placeholder="è¯·éæ©" style="width: 140px" clearable > <el-option label="å¾ è¯ä»·" value="pending" /> <el-option label="å·²è¯ä»·" value="rated" /> </el-select> <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> æç´¢ </el-button> <el-button @click="resetSearch">éç½®</el-button> </div> <div> <el-button icon="Download" @click="handleExport"> 导åºè¯ä»·ç»è®¡ </el-button> </div> </div> <!-- ç»´ä¿®è¯ä»·åè¡¨ï¼æ¨¡æâç»´ä¿®å®æå触åè¯ä»·âåºæ¯ --> <div class="table_list"> <el-table :data="tableData" border style="width: 100%" height="calc(100vh - 24em)" :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" > <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="orderNo" label="维修工åå·" width="160" show-overflow-tooltip /> <el-table-column prop="deviceName" label="设å¤åç§°" width="160" show-overflow-tooltip /> <el-table-column prop="customerName" label="客æ·åç§°" width="180" show-overflow-tooltip /> <el-table-column prop="engineerName" label="维修工ç¨å¸" width="120" /> <el-table-column prop="completeTime" label="ç»´ä¿®å®ææ¶é´" width="180" /> <el-table-column prop="score" label="æçº§è¯å" width="140" align="center"> <template #default="scope"> <el-rate v-if="scope.row.score" v-model="scope.row.score" disabled /> <span v-else>-</span> </template> </el-table-column> <el-table-column prop="status" label="è¯ä»·ç¶æ" width="100" align="center"> <template #default="scope"> <el-tag :type="scope.row.status === 'rated' ? 'success' : 'warning'" size="small" > {{ scope.row.status === 'rated' ? 'å·²è¯ä»·' : 'å¾ è¯ä»·' }} </el-tag> </template> </el-table-column> <el-table-column prop="feedback" label="客æ·åé¦" show-overflow-tooltip /> <el-table-column label="æä½" width="160" align="center" fixed="right"> <template #default="scope"> <el-button v-if="scope.row.status === 'pending'" type="primary" link size="small" @click="openEvaluate(scope.row)" > å»è¯ä»· </el-button> <el-button v-else type="primary" link size="small" @click="openEvaluate(scope.row)" > æ¥ç / ä¿®æ¹è¯ä»· </el-button> </template> </el-table-column> </el-table> </div> <!-- è¯ä»·å¼¹æ¡ï¼æ¨¡æâç»´ä¿®å®æåå¼¹åºå®¢æ·ç«¯è¯ä»·â --> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close > <div class="dialog-order-info" v-if="currentOrder"> <div>维修工åï¼{{ currentOrder.orderNo }}</div> <div>设å¤åç§°ï¼{{ currentOrder.deviceName }}</div> <div>维修工ç¨å¸ï¼{{ currentOrder.engineerName }}</div> </div> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" > <el-form-item label="æçº§è¯åï¼" prop="score"> <el-rate v-model="form.score" :max="5" /> </el-form-item> <el-form-item label="æååé¦ï¼" prop="feedback"> <el-input v-model="form.feedback" type="textarea" :rows="4" placeholder="请填åå¯¹æ¬æ¬¡ç»´ä¿®æå¡çè¯ä»·ï¼å¦ååºé度ãä¸ä¸ç¨åº¦ãæ²éä½éªç" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="dialogVisible = false">å æ¶</el-button> <el-button type="primary" @click="handleSubmit">æ 交 è¯ ä»·</el-button> </div> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, computed } from "vue"; import { ElMessage } from "element-plus"; // 模æç»´ä¿®å·¥å + 客æ·è¯ä»·æ°æ® const rawOrders = ref([ { id: 1, orderNo: "WX-2024-1201-001", deviceName: "ç©ºåæº A1 å·", customerName: "ååçµåç§ææéå ¬å¸", engineerName: "çå¸å ", completeTime: "2024-12-01 10:30:00", completeDate: "2024-12-01", status: "rated", score: 5, feedback: "ç»´ä¿®é常ä¸ä¸ï¼ååºé度快ï¼ç°åºè§£éä¹å¾æ¸ æ°ï¼æ»¡æã", }, { id: 2, orderNo: "WX-2024-1201-002", deviceName: "æ³¨å¡æº B3 å·", customerName: "åä¸ç²¾å¯å¶é æéå ¬å¸", engineerName: "æå¸å ", completeTime: "2024-12-01 15:20:00", completeDate: "2024-12-01", status: "rated", score: 4, feedback: "æ´ä½è¿ä¸éï¼å°±æ¯å°åºæ¶é´ç¨å¾®é¿äºä¸ç¹ï¼å¸æåé¢è½åå¿«ä¸äºã", }, { id: 3, orderNo: "WX-2024-1202-003", deviceName: "çæ¥æºå¨äºº C2 å·", customerName: "è¥¿åæ°è½æºç§æè¡ä»½", engineerName: "å¼ å¸å ", completeTime: "2024-12-02 11:05:00", completeDate: "2024-12-02", status: "pending", score: null, feedback: "", }, { id: 4, orderNo: "WX-2024-1203-005", deviceName: "æµè¯å° D1 å·", customerName: "åæ¹æ±½è½¦é¶é¨ä»¶æéå ¬å¸", engineerName: "çå¸å ", completeTime: "2024-12-03 09:50:00", completeDate: "2024-12-03", status: "pending", score: null, feedback: "", }, ]); // æ¥è¯¢è¡¨å const searchForm = reactive({ engineerName: "", customerName: "", dateRange: [], status: "", }); // åè¡¨æ°æ® const tableData = ref([...rawOrders.value]); // ç»è®¡ï¼æ´ä½è¯åãå·²è¯ä»· / å¾ è¯ä»·æ°é const ratedOrders = computed(() => rawOrders.value.filter((o) => o.status === "rated" && o.score) ); const overallAvgScore = computed(() => { if (!ratedOrders.value.length) return 0; const sum = ratedOrders.value.reduce((acc, cur) => acc + (cur.score || 0), 0); return sum / ratedOrders.value.length; }); const ratedCount = computed(() => ratedOrders.value.length); const pendingCount = computed( () => rawOrders.value.filter((o) => o.status === "pending").length ); // æ¥è¯¢ / éç½® const recomputeTable = () => { const list = rawOrders.value.filter((item) => { if ( searchForm.engineerName && !item.engineerName.includes(searchForm.engineerName.trim()) ) { return false; } if ( searchForm.customerName && !item.customerName.includes(searchForm.customerName.trim()) ) { return false; } if (searchForm.status && item.status !== searchForm.status) { return false; } if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) { const [start, end] = searchForm.dateRange; if (item.completeDate < start || item.completeDate > end) { return false; } } return true; }); tableData.value = list; }; const handleQuery = () => { recomputeTable(); }; const resetSearch = () => { searchForm.engineerName = ""; searchForm.customerName = ""; searchForm.dateRange = []; searchForm.status = ""; recomputeTable(); }; // 导åºï¼æ¼ç¤ºï¼ const handleExport = () => { ElMessage.success("å½å为æ¼ç¤ºé¡µé¢ï¼è¯ä»·å¯¼åºåè½æªå¯¹æ¥å®é æ¥å£"); }; // è¯ä»·å¼¹æ¡ const dialogVisible = ref(false); const dialogTitle = ref("ç»´ä¿®æå¡è¯ä»·"); const currentOrder = ref(null); const formRef = ref(null); const form = reactive({ score: 0, feedback: "", }); const rules = { score: [{ required: true, message: "è¯·éæ©æçº§è¯å", trigger: "change" }], feedback: [{ required: true, message: "请填åæååé¦", trigger: "blur" }], }; // æå¼è¯ä»·ï¼æ¨¡æâç»´ä¿®å®æç¡®è®¤åå¼¹åºè¯ä»·å¼¹æ¡â const openEvaluate = (row) => { currentOrder.value = row; dialogTitle.value = row.status === "pending" ? "ç»´ä¿®æå¡è¯ä»·" : "æ¥ç / ä¿®æ¹è¯ä»·"; form.score = row.score || 0; form.feedback = row.feedback || ""; dialogVisible.value = true; }; // æäº¤è¯ä»·ï¼åæ¥å°æ¬å°âåå·¥ä¸ç»©ç»è®¡â const handleSubmit = () => { if (!formRef.value) return; formRef.value.validate((valid) => { if (!valid || !currentOrder.value) return; const target = rawOrders.value.find((o) => o.id === currentOrder.value.id); if (target) { target.score = form.score; target.feedback = form.feedback; target.status = "rated"; } ElMessage.success("è¯ä»·æäº¤æåï¼å·²åæ¥è³åå·¥ä¸ç»©ç»è®¡"); dialogVisible.value = false; recomputeTable(); }); }; // åå§åå表 recomputeTable(); </script> <style scoped lang="scss"> .mb16 { margin-bottom: 16px; } .kpi-title { font-size: 13px; color: #909399; } .kpi-value { margin-top: 6px; font-size: 24px; font-weight: 600; color: #303133; } .kpi-unit { font-size: 12px; margin-left: 4px; color: #909399; } .kpi-warning { color: #e6a23c; } .dialog-order-info { margin-bottom: 12px; font-size: 13px; color: #606266; line-height: 1.8; } .dialog-footer { text-align: right; } </style> src/views/personnelManagement/attendanceCheckin/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,469 @@ <template> <div class="app-container"> <!-- åå·¥æå¡åº --> <el-card shadow="never" class="mb16"> <div class="attendance-header"> <div> <div class="title">æå¡ç¾å°</div> <div class="sub-title">æ¯æä¸é®æå¡ï¼èªå¨è®°å½ä¸ä¸çæ¶é´</div> </div> <div class="attendance-actions"> <div class="time-block"> <div class="label">å½åæ¶é´</div> <div class="value">{{ nowTime }}</div> </div> <el-button type="primary" size="large" @click="handleCheckInOut"> {{ checkInOutText }} </el-button> </div> </div> <el-descriptions border :column="4" class="mt10"> <el-descriptions-item label="åå·¥å§å"> {{ currentUser.name }} </el-descriptions-item> <el-descriptions-item label="å·¥å·"> {{ currentUser.no }} </el-descriptions-item> <el-descriptions-item label="æå±é¨é¨"> {{ currentUser.dept }} </el-descriptions-item> <el-descriptions-item label="仿¥ç¶æ"> <el-tag :type="todayStatusTag" size="small"> {{ todayStatusText }} </el-tag> </el-descriptions-item> <el-descriptions-item label="ä¸çæ¶é´"> {{ todayRecord?.checkInTime || '-' }} </el-descriptions-item> <el-descriptions-item label="ä¸çæ¶é´"> {{ todayRecord?.checkOutTime || '-' }} </el-descriptions-item> <el-descriptions-item label="å·¥æ¶(å°æ¶)"> {{ todayRecord?.workHours ?? '-' }} </el-descriptions-item> <el-descriptions-item label="å¼å¸¸æ è®°"> <span v-if="todayRecord?.status === 'normal'">-</span> <el-tag v-else type="danger" size="small"> {{ todayRecord?.statusText }} </el-tag> </el-descriptions-item> </el-descriptions> </el-card> <!-- æ¥è¯¢æ¡ä»¶ï¼ç®¡çåè夿¥æ¥ï¼ --> <div class="search_form"> <div> <span class="search_title">é¨é¨ï¼</span> <el-select v-model="searchForm.dept" placeholder="è¯·éæ©é¨é¨" style="width: 180px" clearable > <el-option v-for="item in deptOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> <span class="search_title ml10">æ¥æï¼</span> <el-date-picker v-model="searchForm.date" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©æ¥æ" clearable /> <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> æç´¢ </el-button> <el-button @click="resetSearch">éç½®</el-button> </div> <div> <el-button icon="Download" @click="handleExport"> 导åºè夿¥æ¥ </el-button> </div> </div> <!-- è夿¥æ¥è¡¨æ ¼ --> <div class="table_list"> <el-table :data="tableData" border style="width: 100%" height="calc(100vh - 24em)" :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" :row-class-name="rowClassName" > <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="date" label="æ¥æ" width="120" /> <el-table-column prop="dept" label="é¨é¨" width="140" /> <el-table-column prop="name" label="å§å" width="120" /> <el-table-column prop="no" label="å·¥å·" width="120" /> <el-table-column prop="checkInTime" label="ä¸çæ¶é´" width="140" /> <el-table-column prop="checkOutTime" label="ä¸çæ¶é´" width="140" /> <el-table-column prop="workHours" label="å·¥æ¶(å°æ¶)" width="110" align="center" /> <el-table-column prop="statusText" label="èå¤ç¶æ" width="120" align="center" > <template #default="scope"> <el-tag v-if="scope.row.status === 'normal'" type="success" size="small" > æ£å¸¸ </el-tag> <el-tag v-else type="danger" size="small" > {{ scope.row.statusText }} </el-tag> </template> </el-table-column> <el-table-column prop="remark" label="夿³¨" show-overflow-tooltip /> </el-table> </div> </div> </template> <script setup> import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; import { ElMessage } from "element-plus"; // 模æå½åç»å½åå·¥ const currentUser = reactive({ id: 1, name: "å¼ ä¸", no: "E10001", dept: "ç产ä¸é¨", }); // é¨é¨é项 const deptOptions = [ { label: "ç产ä¸é¨", value: "ç产ä¸é¨" }, { label: "ç产äºé¨", value: "ç产äºé¨" }, { label: "设å¤ç»´æ¤é¨", value: "设å¤ç»´æ¤é¨" }, { label: "è´¨æ£é¨", value: "è´¨æ£é¨" }, ]; // 模æèå¤åå§æ°æ® const rawAttendance = ref([ { id: 1, date: "2024-12-01", userId: 1, name: "å¼ ä¸", no: "E10001", dept: "ç产ä¸é¨", checkInTime: "08:58", checkOutTime: "18:10", workHours: 9.2, status: "normal", statusText: "æ£å¸¸", remark: "", }, { id: 2, date: "2024-12-01", userId: 2, name: "æå", no: "E10002", dept: "ç产ä¸é¨", checkInTime: "09:15", checkOutTime: "18:05", workHours: 8.8, status: "late", statusText: "è¿å°", remark: "å äº¤éæ¥å µè¿å°", }, { id: 3, date: "2024-12-01", userId: 3, name: "çäº", no: "E20001", dept: "设å¤ç»´æ¤é¨", checkInTime: "08:50", checkOutTime: "17:20", workHours: 8.5, status: "early", statusText: "æ©é", remark: "å¤åºå¤çç´§æ¥æ é", }, { id: 4, date: "2024-12-02", userId: 1, name: "å¼ ä¸", no: "E10001", dept: "ç产ä¸é¨", checkInTime: "08:45", checkOutTime: "18:30", workHours: 9.7, status: "normal", statusText: "æ£å¸¸", remark: "å ç0.5å°æ¶", }, ]); // æ¥è¯¢è¡¨å const searchForm = reactive({ dept: "", date: "", }); // è¡¨æ ¼æ°æ® const tableData = ref([]); // å½åæ¶é´å±ç¤º const nowTime = ref(""); let timer = null; const updateNowTime = () => { const now = new Date(); const Y = now.getFullYear(); const M = String(now.getMonth() + 1).padStart(2, "0"); const D = String(now.getDate()).padStart(2, "0"); const h = String(now.getHours()).padStart(2, "0"); const m = String(now.getMinutes()).padStart(2, "0"); const s = String(now.getSeconds()).padStart(2, "0"); nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`; }; // 仿¥æ¥æ const todayStr = computed(() => nowTime.value.slice(0, 10)); // 彿¥å½ååå·¥èå¤è®°å½ const todayRecord = computed(() => rawAttendance.value.find( (item) => item.userId === currentUser.id && item.date === todayStr.value ) ); // æå¡æé®ææ¬ const checkInOutText = computed(() => { if (!todayRecord.value || !todayRecord.value.checkInTime) { return "ä¸çæå¡"; } if (!todayRecord.value.checkOutTime) { return "ä¸çæå¡"; } return "仿¥å·²æå¡å®æ"; }); // 仿¥ç¶æå±ç¤º const todayStatusTag = computed(() => { if (!todayRecord.value) return "info"; if (todayRecord.value.status === "normal") return "success"; return "danger"; }); const todayStatusText = computed(() => { if (!todayRecord.value) return "æªæå¡"; return todayRecord.value.statusText || "æ£å¸¸"; }); // è¡æ ·å¼ï¼å¼å¸¸é«äº® const rowClassName = ({ row }) => { if (row.status === "late" || row.status === "early") { return "row-abnormal"; } return ""; }; // æ¥è¯¢ const recomputeTable = () => { const list = rawAttendance.value.filter((item) => { if (searchForm.dept && item.dept !== searchForm.dept) { return false; } if (searchForm.date && item.date !== searchForm.date) { return false; } return true; }); tableData.value = list; }; const handleQuery = () => { recomputeTable(); }; const resetSearch = () => { searchForm.dept = ""; searchForm.date = ""; recomputeTable(); }; // 导åºï¼æ¼ç¤ºï¼ const handleExport = () => { ElMessage.success("å½å为æ¼ç¤ºé¡µé¢ï¼å¯¼åºåè½æªå¯¹æ¥å®é æ¥å£"); }; // æå¡é»è¾ï¼ä» å端模æï¼ const handleCheckInOut = () => { const [dateStr, timeStr] = nowTime.value.split(" "); if (!dateStr || !timeStr) return; // ä¸çæå¡ if (!todayRecord.value) { const newId = rawAttendance.value.length ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1 : 1; const status = timeStr > "09:00:00" ? "late" : "normal"; const statusText = status === "late" ? "è¿å°" : "æ£å¸¸"; rawAttendance.value.push({ id: newId, date: dateStr, userId: currentUser.id, name: currentUser.name, no: currentUser.no, dept: currentUser.dept, checkInTime: timeStr.slice(0, 5), checkOutTime: "", workHours: null, status, statusText, remark: "", }); ElMessage.success("ä¸çæå¡æå"); } else if (!todayRecord.value.checkOutTime) { // ä¸çæå¡ todayRecord.value.checkOutTime = timeStr.slice(0, 5); // ç®åæ 9:00-18:00 计ç®å·¥æ¶ const start = todayRecord.value.checkInTime || "09:00"; const [sh, sm] = start.split(":").map((v) => parseInt(v, 10)); const [eh, em] = todayRecord.value.checkOutTime .split(":") .map((v) => parseInt(v, 10)); const diff = (eh * 60 + em - (sh * 60 + sm)) / 60; todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1)); // æ©é夿ï¼18:00 å离å¼è§ä¸ºæ©éï¼åªç¤ºæï¼ if (timeStr < "18:00:00") { todayRecord.value.status = "early"; todayRecord.value.statusText = "æ©é"; } else if (todayRecord.value.status === "normal") { todayRecord.value.statusText = "æ£å¸¸"; } ElMessage.success("ä¸çæå¡æå"); } else { ElMessage.info("仿¥å·²å®æä¸ä¸çæå¡"); } recomputeTable(); }; onMounted(() => { updateNowTime(); timer = setInterval(updateNowTime, 1000); // é»è®¤å±ç¤ºå½å¤©æ°æ® const today = new Date(); const Y = today.getFullYear(); const M = String(today.getMonth() + 1).padStart(2, "0"); const D = String(today.getDate()).padStart(2, "0"); searchForm.date = `${Y}-${M}-${D}`; recomputeTable(); }); onBeforeUnmount(() => { if (timer) { clearInterval(timer); } }); </script> <style scoped lang="scss"> .mb16 { margin-bottom: 16px; } .attendance-header { display: flex; justify-content: space-between; align-items: center; } .attendance-header .title { font-size: 18px; font-weight: 600; margin-bottom: 4px; } .attendance-header .sub-title { font-size: 13px; color: #909399; } .attendance-actions { display: flex; align-items: center; gap: 16px; } .time-block { text-align: right; } .time-block .label { font-size: 12px; color: #909399; } .time-block .value { font-size: 18px; font-weight: 600; color: #333; } ::v-deep(.row-abnormal) { background-color: #fff5f5; } </style>