From c94791b7fde443ac3b26910715bed364e799bbec Mon Sep 17 00:00:00 2001 From: spring <2396852758@qq.com> Date: 星期一, 01 九月 2025 16:17:46 +0800 Subject: [PATCH] Merge branch 'dev-juli' into dev --- src/views/personnelManagement/scheduling/index.vue | 634 ++++++++++++++++++++ src/views/personnelManagement/analytics/index.vue | 698 ++++++++++++++++++++++ src/views/personnelManagement/selfService/index.vue | 525 ++++++++++++++++ 3 files changed, 1,857 insertions(+), 0 deletions(-) diff --git a/src/views/personnelManagement/analytics/index.vue b/src/views/personnelManagement/analytics/index.vue new file mode 100644 index 0000000..06b868b --- /dev/null +++ b/src/views/personnelManagement/analytics/index.vue @@ -0,0 +1,698 @@ +<template> + <div class="app-container analytics-container"> + + <!-- 鍏抽敭鎸囨爣鍗$墖 --> + <el-row :gutter="20" class="metrics-cards"> + <el-col :span="6" v-for="(item, index) in keyMetrics" :key="index"> + <el-card class="metric-card" :class="item.type"> + <div class="card-content"> + <div class="card-icon"> + <el-icon :size="32"> + <component :is="item.icon" /> + </el-icon> + </div> + <div class="card-info"> + <div class="card-number"> + <el-skeleton-item v-if="loading" variant="text" style="width: 60px; height: 32px;" /> + <span v-else>{{ item.value }}{{ item.unit }}</span> + </div> + <div class="card-label">{{ item.label }}</div> + <div class="card-trend" :class="item.trend > 0 ? 'positive' : 'negative'"> + <el-icon> + <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" /> + </el-icon> + {{ Math.abs(item.trend) }}% + </div> + </div> + </div> + </el-card> + </el-col> + </el-row> + + <!-- 鍥捐〃鍖哄煙 --> + <el-row :gutter="20" class="charts-section"> + <!-- 鍛樺伐娴佸姩鐜囪秼鍔垮浘 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>鍛樺伐娴佸姩鐜囪秼鍔�</span> + <el-tag type="info">杩�12涓湀</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="turnoverChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + + <!-- 閮ㄩ棬浜哄憳鍒嗗竷 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>閮ㄩ棬浜哄憳鍒嗗竷</span> + <el-tag type="success">褰撳墠鐘舵��</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="departmentChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + </el-row> + + <!-- 绗簩琛屽浘琛� --> + <el-row :gutter="20" class="charts-section"> + <!-- 缂栧埗杈炬垚鐜� --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>缂栧埗杈炬垚鐜�</span> + <el-tag type="warning">鍚勯儴闂ㄥ姣�</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="staffingChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + + <!-- 鍛樺伐娴佸け鍘熷洜鍒嗘瀽 --> + <el-col :span="12"> + <el-card class="chart-card"> + <template #header> + <div class="card-header"> + <span>鍛樺伐娴佸け鍘熷洜鍒嗘瀽</span> + <el-tag type="danger">骞村害缁熻</el-tag> + </div> + </template> + <div class="chart-container"> + <div ref="attritionChartRef" class="chart"></div> + </div> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script setup> +import { ref, reactive, onMounted, onUnmounted } from 'vue' +import { ElMessage } from 'element-plus' +import { + Refresh, + User, + TrendCharts, + DataAnalysis, + PieChart, + ArrowUp, + ArrowDown +} from '@element-plus/icons-vue' +import * as echarts from 'echarts' + +// 鍝嶅簲寮忔暟鎹� +const loading = ref(false) +const autoRefreshEnabled = ref(true) +const autoRefreshInterval = ref(null) + +// 鍥捐〃寮曠敤 +const turnoverChartRef = ref(null) +const departmentChartRef = ref(null) +const staffingChartRef = ref(null) +const attritionChartRef = ref(null) + +// 鍥捐〃瀹炰緥 +let turnoverChart = null +let departmentChart = null +let staffingChart = null +let attritionChart = null + +// 鑷姩鏇存柊闂撮殧锛�10鍒嗛挓锛� +const AUTO_REFRESH_INTERVAL = 10 * 60 * 1000 + +// 鍏抽敭鎸囨爣鏁版嵁 +const keyMetrics = ref([ + { + label: '鍛樺伐娴佸姩鐜�', + value: 0, + unit: '%', + icon: 'TrendCharts', + type: 'primary', + trend: 0 + }, + { + label: '鍛樺伐娴佸け鐜�', + value: 0, + unit: '%', + icon: 'User', + type: 'danger', + trend: 0 + }, + { + label: '缂栧埗杈炬垚鐜�', + value: 0, + unit: '%', + icon: 'DataAnalysis', + type: 'success', + trend: 0 + }, + { + label: '鍦ㄨ亴鍛樺伐鏁�', + value: 0, + unit: '浜�', + icon: 'PieChart', + type: 'warning', + trend: 0 + } +]) + +// 閮ㄩ棬鏁版嵁 +const departmentData = ref([]) + +// 鍚姩鑷姩鍒锋柊 +const startAutoRefresh = () => { + if (autoRefreshInterval.value) { + clearInterval(autoRefreshInterval.value) + } + if (autoRefreshEnabled.value) { + autoRefreshInterval.value = setInterval(() => { + refreshData() + }, AUTO_REFRESH_INTERVAL) + } +} + +// 鍋滄鑷姩鍒锋柊 +const stopAutoRefresh = () => { + if (autoRefreshInterval.value) { + clearInterval(autoRefreshInterval.value) + autoRefreshInterval.value = null + } +} + +// 鍒囨崲鑷姩鍒锋柊鐘舵�� +const toggleAutoRefresh = (value) => { + if (value) { + startAutoRefresh() + } else { + stopAutoRefresh() + } +} + +// 鐢熸垚妯℃嫙鏁版嵁 +const generateMockData = () => { + // 鐢熸垚鍏抽敭鎸囨爣鏁版嵁 + keyMetrics.value[0].value = (Math.random() * 5 + 2).toFixed(1) + keyMetrics.value[0].trend = (Math.random() * 3 - 1.5).toFixed(1) + + keyMetrics.value[1].value = (Math.random() * 3 + 1).toFixed(1) + keyMetrics.value[1].trend = (Math.random() * 2 - 1).toFixed(1) + + keyMetrics.value[2].value = (Math.random() * 15 + 85).toFixed(1) + keyMetrics.value[2].trend = (Math.random() * 3 - 1.5).toFixed(1) + + keyMetrics.value[3].value = Math.floor(Math.random() * 50 + 200) + keyMetrics.value[3].trend = (Math.random() * 2 - 1).toFixed(1) + + // 鐢熸垚閮ㄩ棬鏁版嵁 + const departments = ['鎶�鏈儴', '閿�鍞儴', '浜轰簨閮�', '璐㈠姟閮�', '鐢熶骇閮�', '甯傚満閮�'] + departmentData.value = departments.map(dept => ({ + department: dept, + currentStaff: Math.floor(Math.random() * 30 + 20), + plannedStaff: Math.floor(Math.random() * 10 + 35), + staffingRate: Math.floor(Math.random() * 20 + 80), + turnoverRate: (Math.random() * 4 + 1).toFixed(1), + attritionRate: (Math.random() * 2 + 0.5).toFixed(1), + newHires: Math.floor(Math.random() * 5 + 1), + resignations: Math.floor(Math.random() * 3 + 1), + status: Math.random() > 0.7 ? '寮傚父' : '姝e父' + })) +} + +// 鍒锋柊鏁版嵁 +const refreshData = async () => { + loading.value = true + try { + // 妯℃嫙API璋冪敤寤惰繜 + await new Promise(resolve => setTimeout(resolve, 500)) + + generateMockData() + renderAllCharts() + + if (!autoRefreshEnabled.value) { + ElMessage.success('鏁版嵁鍒锋柊鎴愬姛') + } + } catch (error) { + console.error('鍒锋柊鏁版嵁澶辫触:', error) + ElMessage.error('鍒锋柊鏁版嵁澶辫触') + } finally { + loading.value = false + } +} + +// 鍒濆鍖栧浘琛� +const initCharts = () => { + setTimeout(() => { + if (turnoverChartRef.value) { + turnoverChart = echarts.init(turnoverChartRef.value) + } + if (departmentChartRef.value) { + departmentChart = echarts.init(departmentChartRef.value) + } + if (staffingChartRef.value) { + staffingChart = echarts.init(staffingChartRef.value) + } + if (attritionChartRef.value) { + attritionChart = echarts.init(attritionChartRef.value) + } + + renderAllCharts() + }, 300) +} + +// 娓叉煋鎵�鏈夊浘琛� +const renderAllCharts = () => { + renderTurnoverChart() + renderDepartmentChart() + renderStaffingChart() + renderAttritionChart() +} + +// 娓叉煋鍛樺伐娴佸姩鐜囪秼鍔垮浘 +const renderTurnoverChart = () => { + if (!turnoverChart) return + + const months = ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�'] + const turnoverData = months.map(() => (Math.random() * 5 + 2).toFixed(1)) + const attritionData = months.map(() => (Math.random() * 3 + 1).toFixed(1)) + + const option = { + title: { + text: '鍛樺伐娴佸姩鐜囪秼鍔�', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'cross' } + }, + legend: { + data: ['娴佸姩鐜�', '娴佸け鐜�'], + bottom: 10 + }, + grid: { + left: '3%', + right: '4%', + bottom: '15%', + top: '15%', + containLabel: true + }, + xAxis: { + type: 'category', + data: months, + boundaryGap: false + }, + yAxis: { + type: 'value', + axisLabel: { formatter: '{value}%' } + }, + series: [ + { + name: '娴佸姩鐜�', + type: 'line', + data: turnoverData, + smooth: true, + lineStyle: { color: '#409EFF' }, + itemStyle: { color: '#409EFF' } + }, + { + name: '娴佸け鐜�', + type: 'line', + data: attritionData, + smooth: true, + lineStyle: { color: '#F56C6C' }, + itemStyle: { color: '#F56C6C' } + } + ] + } + + turnoverChart.setOption(option) +} + +// 娓叉煋閮ㄩ棬浜哄憳鍒嗗竷鍥� +const renderDepartmentChart = () => { + if (!departmentChart) return + + const data = departmentData.value.map(item => ({ + name: item.department, + value: item.currentStaff + })) + + const option = { + title: { + text: '閮ㄩ棬浜哄憳鍒嗗竷', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c}浜� ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'middle' + }, + series: [ + { + name: '浜哄憳鏁伴噺', + type: 'pie', + radius: ['40%', '70%'], + center: ['60%', '50%'], + data: data, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + } + + departmentChart.setOption(option) +} + +// 娓叉煋缂栧埗杈炬垚鐜囧浘 +const renderStaffingChart = () => { + if (!staffingChart) return + + const departments = departmentData.value.map(item => item.department) + const rates = departmentData.value.map(item => item.staffingRate) + + const option = { + title: { + text: '缂栧埗杈炬垚鐜�', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' } + }, + grid: { + left: '3%', + right: '4%', + bottom: '15%', + top: '15%', + containLabel: true + }, + xAxis: { + type: 'category', + data: departments, + axisLabel: { rotate: 45 } + }, + yAxis: { + type: 'value', + axisLabel: { formatter: '{value}%' }, + max: 100 + }, + series: [ + { + name: '杈炬垚鐜�', + type: 'bar', + data: rates, + itemStyle: { + color: function(params) { + const value = params.value + if (value >= 90) return '#67C23A' + if (value >= 80) return '#E6A23C' + return '#F56C6C' + } + } + } + ] + } + + staffingChart.setOption(option) +} + +// 娓叉煋鍛樺伐娴佸け鍘熷洜鍒嗘瀽鍥� +const renderAttritionChart = () => { + if (!attritionChart) return + + const reasons = ['钖祫寰呴亣', '鑱屼笟鍙戝睍', '宸ヤ綔鐜', '涓汉鍘熷洜', '鍏朵粬'] + const data = reasons.map(() => Math.floor(Math.random() * 20 + 5)) + + const option = { + title: { + text: '鍛樺伐娴佸け鍘熷洜鍒嗘瀽', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'normal' } + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c}浜� ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'middle' + }, + series: [ + { + name: '娴佸け浜烘暟', + type: 'pie', + radius: '50%', + center: ['60%', '50%'], + data: reasons.map((reason, index) => ({ + name: reason, + value: data[index] + })), + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + } + ] + } + + attritionChart.setOption(option) +} + +// 鐢熷懡鍛ㄦ湡 +onMounted(() => { + generateMockData() + initCharts() + startAutoRefresh() +}) + +onUnmounted(() => { + stopAutoRefresh() +}) +</script> + +<style scoped> +.analytics-container { + padding: 20px; + background-color: #f5f7fa; + min-height: 100vh; +} + +.page-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.page-header h2 { + color: white; + margin-bottom: 10px; + font-size: 28px; + font-weight: 600; +} + +.page-header p { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + margin: 0 0 15px 0; +} + +.header-controls { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; +} + +.refresh-btn { + margin-left: 20px; +} + +.metrics-cards { + margin-bottom: 30px; +} + +.metric-card { + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + border: none; + overflow: hidden; +} + +.metric-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.metric-card.primary { + border-left: 4px solid #409EFF; + background: linear-gradient(135deg, #409EFF 0%, #36a3f7 100%); +} + +.metric-card.danger { + border-left: 4px solid #F56C6C; + background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%); +} + +.metric-card.success { + border-left: 4px solid #67C23A; + background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); +} + +.metric-card.warning { + border-left: 4px solid #E6A23C; + background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%); +} + +.card-content { + display: flex; + align-items: center; + padding: 20px; +} + +.card-icon { + margin-right: 20px; + color: white; +} + +.card-info { + flex: 1; +} + +.card-number { + font-size: 32px; + font-weight: 600; + color: white; + margin-bottom: 5px; +} + +.card-label { + font-size: 14px; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 5px; +} + +.card-trend { + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} + +.card-trend.positive { + color: #67C23A; +} + +.card-trend.negative { + color: #F56C6C; +} + +.charts-section { + margin-bottom: 30px; +} + +.chart-card { + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: none; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: #303133; + padding: 15px 20px; + border-bottom: 1px solid #ebeef5; +} + +.chart-container { + height: 350px; + padding: 20px; +} + +.chart { + width: 100%; + height: 100%; +} + +/* 鍝嶅簲寮忚璁� */ +@media (max-width: 768px) { + .analytics-container { + padding: 10px; + } + + .page-header { + padding: 15px; + } + + .page-header h2 { + font-size: 24px; + } + + .header-controls { + flex-direction: column; + gap: 15px; + } + + .refresh-btn { + margin-left: 0; + } + + .metrics-cards .el-col { + margin-bottom: 15px; + } + + .charts-section .el-col { + margin-bottom: 20px; + } + + .chart-container { + height: 300px; + } +} + +@media (max-width: 480px) { + .page-header h2 { + font-size: 20px; + } + + .card-number { + font-size: 24px; + } + + .chart-container { + height: 250px; + } +} +</style> diff --git a/src/views/personnelManagement/scheduling/index.vue b/src/views/personnelManagement/scheduling/index.vue new file mode 100644 index 0000000..8a7174d --- /dev/null +++ b/src/views/personnelManagement/scheduling/index.vue @@ -0,0 +1,634 @@ +<template> + <div class="app-container scheduling-container"> + <!-- 绛涢�夊尯鍩� --> + <div class="filter-section"> + <el-form :inline="true" :model="filterForm" class="filter-form"> + <el-form-item label="鍛樺伐濮撳悕锛�"> + <el-input + v-model="filterForm.employeeName" + placeholder="璇疯緭鍏ュ憳宸ュ鍚�" + clearable + style="width: 150px" + /> + </el-form-item> + <el-form-item label="鐝绫诲瀷锛�"> + <el-select v-model="filterForm.shiftType" placeholder="璇烽�夋嫨鐝" clearable style="width: 120px"> + <el-option label="鏃╃彮" value="鏃╃彮" /> + <el-option label="涓彮" value="涓彮" /> + <el-option label="鏅氱彮" value="鏅氱彮" /> + <el-option label="澶滅彮" value="澶滅彮" /> + </el-select> + </el-form-item> + <el-form-item label="鏃ユ湡鑼冨洿锛�"> + <el-date-picker + v-model="filterForm.dateRange" + type="daterange" + range-separator="鑷�" + start-placeholder="寮�濮嬫棩鏈�" + end-placeholder="缁撴潫鏃ユ湡" + format="YYYY-MM-DD" + value-format="YYYY-MM-DD" + style="width: 250px" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" @click="handleFilter"> + <el-icon><Search /></el-icon> + 绛涢�� + </el-button> + <el-button @click="resetFilter"> + <el-icon><Refresh /></el-icon> + 閲嶇疆 + </el-button> + <el-button type="primary" @click="openScheduleDialog('add')"> + <el-icon><Plus /></el-icon> + 鏂板鎺掔彮 + </el-button> + </el-form-item> + </el-form> + </div> + + <!-- 鎺掔彮琛ㄦ牸 --> + <div class="table-section"> + <el-table + :data="filteredScheduleList" + border + stripe + style="width: 100%" + height="calc(100vh - 18.5em)" + @selection-change="handleSelectionChange" + > + <el-table-column type="selection" width="55" /> + <el-table-column prop="employeeName" label="鍛樺伐濮撳悕" width="120" /> + <el-table-column prop="employeeId" label="鍛樺伐宸ュ彿" width="100" /> + <el-table-column prop="department" label="閮ㄩ棬" width="120" /> + <el-table-column prop="shiftType" label="鐝绫诲瀷" width="100"> + <template #default="scope"> + <el-tag :type="getShiftTagType(scope.row.shiftType)"> + {{ scope.row.shiftType }} + </el-tag> + </template> + </el-table-column> + <el-table-column prop="workDate" label="宸ヤ綔鏃ユ湡" width="120" /> + <el-table-column prop="startTime" label="寮�濮嬫椂闂�" width="100" /> + <el-table-column prop="endTime" label="缁撴潫鏃堕棿" width="100" /> + <el-table-column prop="workHours" label="宸ヤ綔鏃堕暱" width="100"> + <template #default="scope"> + {{ scope.row.workHours }}灏忔椂 + </template> + </el-table-column> + <el-table-column prop="status" label="鐘舵��" width="100"> + <template #default="scope"> + <el-tag :type="getStatusTagType(scope.row.status)"> + {{ scope.row.status }} + </el-tag> + </template> + </el-table-column> + <el-table-column prop="remark" label="澶囨敞" min-width="150" /> + <el-table-column label="鎿嶄綔" width="200" fixed="right"> + <template #default="scope"> + <el-button + type="primary" + size="small" + @click="openScheduleDialog('edit', scope.row)" + > + 缂栬緫 + </el-button> + <el-button + type="danger" + size="small" + @click="handleDelete(scope.row)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + </div> + + <!-- 鎵归噺鎿嶄綔 --> + <div class="batch-actions" v-if="selectedRows.length > 0"> + <el-button + type="danger" + @click="handleBatchDelete" + :disabled="selectedRows.length === 0" + > + 鎵归噺鍒犻櫎 ({{ selectedRows.length }}) + </el-button> + </div> + + <!-- 鎺掔彮鏂板/缂栬緫瀵硅瘽妗� --> + <el-dialog + v-model="scheduleDialog" + :title="dialogType === 'add' ? '鏂板鎺掔彮' : '缂栬緫鎺掔彮'" + width="700px" + @close="closeScheduleDialog" + > + <el-form + :model="scheduleForm" + :rules="scheduleRules" + ref="scheduleFormRef" + label-width="120px" + > + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="鍛樺伐濮撳悕锛�" prop="employeeName"> + <el-input v-model="scheduleForm.employeeName" placeholder="璇疯緭鍏ュ憳宸ュ鍚�" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="鍛樺伐宸ュ彿锛�" prop="employeeId"> + <el-input v-model="scheduleForm.employeeId" placeholder="璇疯緭鍏ュ憳宸ュ伐鍙�" /> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="閮ㄩ棬锛�" prop="department"> + <el-select v-model="scheduleForm.department" placeholder="璇烽�夋嫨閮ㄩ棬" style="width: 100%"> + <el-option label="鎶�鏈儴" value="鎶�鏈儴" /> + <el-option label="閿�鍞儴" value="閿�鍞儴" /> + <el-option label="浜轰簨閮�" value="浜轰簨閮�" /> + <el-option label="璐㈠姟閮�" value="璐㈠姟閮�" /> + <el-option label="鐢熶骇閮�" value="鐢熶骇閮�" /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="鐝绫诲瀷锛�" prop="shiftType"> + <el-select v-model="scheduleForm.shiftType" placeholder="璇烽�夋嫨鐝" style="width: 100%"> + <el-option label="鏃╃彮" value="鏃╃彮" /> + <el-option label="涓彮" value="涓彮" /> + <el-option label="鏅氱彮" value="鏅氱彮" /> + <el-option label="澶滅彮" value="澶滅彮" /> + </el-select> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="宸ヤ綔鏃ユ湡锛�" prop="workDate"> + <el-date-picker + v-model="scheduleForm.workDate" + type="date" + placeholder="閫夋嫨宸ヤ綔鏃ユ湡" + style="width: 100%" + format="YYYY-MM-DD" + value-format="YYYY-MM-DD" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="鐘舵�侊細" prop="status"> + <el-select v-model="scheduleForm.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%"> + <el-option label="宸插畨鎺�" value="宸插畨鎺�" /> + <el-option label="宸茬‘璁�" value="宸茬‘璁�" /> + <el-option label="宸插畬鎴�" value="宸插畬鎴�" /> + <el-option label="宸插彇娑�" value="宸插彇娑�" /> + </el-select> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="寮�濮嬫椂闂达細" prop="startTime"> + <el-time-picker + v-model="scheduleForm.startTime" + placeholder="閫夋嫨寮�濮嬫椂闂�" + style="width: 100%" + format="HH:mm" + value-format="HH:mm" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="缁撴潫鏃堕棿锛�" prop="endTime"> + <el-time-picker + v-model="scheduleForm.endTime" + placeholder="閫夋嫨缁撴潫鏃堕棿" + style="width: 100%" + format="HH:mm" + value-format="HH:mm" + /> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="24"> + <el-form-item label="澶囨敞锛�" prop="remark"> + <el-input + v-model="scheduleForm.remark" + type="textarea" + :rows="3" + placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" + /> + </el-form-item> + </el-col> + </el-row> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button type="primary" @click="submitScheduleForm">纭</el-button> + <el-button @click="closeScheduleDialog">鍙栨秷</el-button> + </div> + </template> + </el-dialog> + </div> +</template> + +<script setup> +import { ref, reactive, computed, onMounted } from 'vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import { Plus, Download, Search, Refresh } from '@element-plus/icons-vue' + +// 鍝嶅簲寮忔暟鎹� +const scheduleDialog = ref(false) +const dialogType = ref('add') +const selectedRows = ref([]) +const scheduleFormRef = ref() + +// 绛涢�夎〃鍗� +const filterForm = reactive({ + employeeName: '', + shiftType: '', + dateRange: [] +}) + +// 鎺掔彮琛ㄥ崟 +const scheduleForm = reactive({ + id: '', + employeeName: '', + employeeId: '', + department: '', + shiftType: '', + workDate: '', + startTime: '', + endTime: '', + workHours: 0, + status: '宸插畨鎺�', + remark: '' +}) + +// 琛ㄥ崟楠岃瘉瑙勫垯 +const scheduleRules = reactive({ + employeeName: [{ required: true, message: '璇疯緭鍏ュ憳宸ュ鍚�', trigger: 'blur' }], + employeeId: [{ required: true, message: '璇疯緭鍏ュ憳宸ュ伐鍙�', trigger: 'blur' }], + department: [{ required: true, message: '璇烽�夋嫨閮ㄩ棬', trigger: 'change' }], + shiftType: [{ required: true, message: '璇烽�夋嫨鐝绫诲瀷', trigger: 'change' }], + workDate: [{ required: true, message: '璇烽�夋嫨宸ヤ綔鏃ユ湡', trigger: 'change' }], + startTime: [{ required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change' }], + endTime: [{ required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change' }], + status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }] +}) + +// 妯℃嫙鎺掔彮鏁版嵁 +const scheduleList = ref([ + { + id: 1, + employeeName: '寮犳捣娲�', + employeeId: 'EMP001', + department: '鎶�鏈儴', + shiftType: '鏃╃彮', + workDate: '2024-01-15', + startTime: '08:00', + endTime: '17:00', + workHours: 9, + status: '宸插畨鎺�', + remark: '姝e父鎺掔彮' + }, + { + id: 2, + employeeName: '鏉庤秴', + employeeId: 'EMP002', + department: '閿�鍞儴', + shiftType: '涓彮', + workDate: '2024-01-15', + startTime: '14:00', + endTime: '22:00', + workHours: 8, + status: '宸茬‘璁�', + remark: '瀹㈡埛浼氳' + }, + { + id: 3, + employeeName: '鐜嬫澃', + employeeId: 'EMP003', + department: '鐢熶骇閮�', + shiftType: '鏅氱彮', + workDate: '2024-01-15', + startTime: '22:00', + endTime: '06:00', + workHours: 8, + status: '宸插畨鎺�', + remark: '澶滅彮鐢熶骇' + } +]) + +// 璁$畻灞炴�э細绛涢�夊悗鐨勬帓鐝垪琛� +const filteredScheduleList = computed(() => { + let result = scheduleList.value + + if (filterForm.employeeName) { + result = result.filter(item => + item.employeeName.includes(filterForm.employeeName) + ) + } + + if (filterForm.shiftType) { + result = result.filter(item => item.shiftType === filterForm.shiftType) + } + + if (filterForm.dateRange && filterForm.dateRange.length === 2) { + result = result.filter(item => { + const workDate = new Date(item.workDate) + const startDate = new Date(filterForm.dateRange[0]) + const endDate = new Date(filterForm.dateRange[1]) + return workDate >= startDate && workDate <= endDate + }) + } + + return result +}) + +// 鑾峰彇鐝鏍囩绫诲瀷 +const getShiftTagType = (shiftType) => { + const typeMap = { + '鏃╃彮': 'success', + '涓彮': 'warning', + '鏅氱彮': 'info', + '澶滅彮': 'danger' + } + return typeMap[shiftType] || 'info' +} + +// 鑾峰彇鐘舵�佹爣绛剧被鍨� +const getStatusTagType = (status) => { + const typeMap = { + '宸插畨鎺�': 'info', + '宸茬‘璁�': 'warning', + '宸插畬鎴�': 'success', + '宸插彇娑�': 'danger' + } + return typeMap[status] || 'info' +} + +// 绛涢�� +const handleFilter = () => { + // 绛涢�夐�昏緫宸插湪璁$畻灞炴�т腑瀹炵幇 +} + +// 閲嶇疆绛涢�� +const resetFilter = () => { + filterForm.employeeName = '' + filterForm.shiftType = '' + filterForm.dateRange = [] +} + +// 鎵撳紑鎺掔彮瀵硅瘽妗� +const openScheduleDialog = (type, data) => { + dialogType.value = type + scheduleDialog.value = true + + if (type === 'edit' && data) { + // 缂栬緫妯″紡锛屽鍒舵暟鎹� + Object.assign(scheduleForm, { ...data }) + } else { + // 鏂板妯″紡锛岄噸缃〃鍗� + Object.keys(scheduleForm).forEach(key => { + scheduleForm[key] = '' + }) + scheduleForm.status = '宸插畨鎺�' + scheduleForm.workDate = new Date().toISOString().split('T')[0] + } +} + +// 鍏抽棴鎺掔彮瀵硅瘽妗� +const closeScheduleDialog = () => { + scheduleFormRef.value?.resetFields() + scheduleDialog.value = false +} + +// 璁$畻宸ヤ綔鏃堕暱 +const calculateWorkHours = () => { + if (scheduleForm.startTime && scheduleForm.endTime) { + const start = new Date(`2000-01-01 ${scheduleForm.startTime}`) + const end = new Date(`2000-01-01 ${scheduleForm.endTime}`) + + if (end < start) { + // 璺ㄥぉ鐨勬儏鍐� + end.setDate(end.getDate() + 1) + } + + const diffMs = end - start + const diffHours = diffMs / (1000 * 60 * 60) + scheduleForm.workHours = Math.round(diffHours * 100) / 100 + } +} + +// 鎻愪氦鎺掔彮琛ㄥ崟 +const submitScheduleForm = () => { + scheduleFormRef.value.validate((valid) => { + if (valid) { + // 璁$畻宸ヤ綔鏃堕暱 + calculateWorkHours() + + if (dialogType.value === 'add') { + // 鏂板 + const newSchedule = { + ...scheduleForm, + id: Date.now() // 浣跨敤鏃堕棿鎴充綔涓轰复鏃禝D + } + scheduleList.value.push(newSchedule) + ElMessage.success('鏂板鎺掔彮鎴愬姛') + } else { + // 缂栬緫 + const index = scheduleList.value.findIndex(item => item.id === scheduleForm.id) + if (index !== -1) { + scheduleList.value[index] = { ...scheduleForm } + ElMessage.success('缂栬緫鎺掔彮鎴愬姛') + } + } + + closeScheduleDialog() + } + }) +} + +// 鍒犻櫎鎺掔彮 +const handleDelete = (row) => { + ElMessageBox.confirm( + `纭畾瑕佸垹闄� ${row.employeeName} 鐨勬帓鐝褰曞悧锛焋, + '鍒犻櫎鎻愮ず', + { + confirmButtonText: '纭', + cancelButtonText: '鍙栨秷', + type: 'warning' + } + ).then(() => { + const index = scheduleList.value.findIndex(item => item.id === row.id) + if (index !== -1) { + scheduleList.value.splice(index, 1) + ElMessage.success('鍒犻櫎鎴愬姛') + } + }).catch(() => { + ElMessage.info('宸插彇娑堝垹闄�') + }) +} + +// 鎵归噺鍒犻櫎 +const handleBatchDelete = () => { + if (selectedRows.value.length === 0) { + ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑璁板綍') + return + } + + ElMessageBox.confirm( + `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉℃帓鐝褰曞悧锛焋, + '鎵归噺鍒犻櫎鎻愮ず', + { + confirmButtonText: '纭', + cancelButtonText: '鍙栨秷', + type: 'warning' + } + ).then(() => { + const selectedIds = selectedRows.value.map(row => row.id) + scheduleList.value = scheduleList.value.filter(item => !selectedIds.includes(item.id)) + selectedRows.value = [] + ElMessage.success('鎵归噺鍒犻櫎鎴愬姛') + }).catch(() => { + ElMessage.info('宸插彇娑堝垹闄�') + }) +} + +// 閫夋嫨鍙樺寲浜嬩欢 +const handleSelectionChange = (selection) => { + selectedRows.value = selection +} + +// 鐩戝惉鏃堕棿鍙樺寲锛岃嚜鍔ㄨ绠楀伐浣滄椂闀� +const watchTimeChange = () => { + if (scheduleForm.startTime && scheduleForm.endTime) { + calculateWorkHours() + } +} + +// 鐢熷懡鍛ㄦ湡 +onMounted(() => { + // 椤甸潰鍒濆鍖� +}) +</script> + +<style scoped> +.scheduling-container { + padding: 20px; + background-color: #f5f7fa; + min-height: 100vh; +} + +.page-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.page-header h2 { + color: white; + margin-bottom: 10px; + font-size: 28px; + font-weight: 600; +} + +.page-header p { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + margin: 0 0 15px 0; +} + +.header-controls { + display: flex; + justify-content: center; + gap: 15px; +} + +.filter-section { + background: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.filter-form { + margin: 0; +} + +.table-section { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +.batch-actions { + background: white; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.dialog-footer { + text-align: right; +} + +:deep(.el-form-item__label) { + font-weight: 500; + color: #303133; +} + +:deep(.el-input__wrapper) { + box-shadow: 0 0 0 1px #dcdfe6 inset; +} + +:deep(.el-input__wrapper:hover) { + box-shadow: 0 0 0 1px #c0c4cc inset; +} + +:deep(.el-input__wrapper.is-focus) { + box-shadow: 0 0 0 1px #409eff inset; +} + +/* 鍝嶅簲寮忚璁� */ +@media (max-width: 768px) { + .scheduling-container { + padding: 10px; + } + + .page-header { + padding: 15px; + } + + .page-header h2 { + font-size: 24px; + } + + .header-controls { + flex-direction: column; + gap: 10px; + } +} + +@media (max-width: 768px) { + .filter-form .el-form-item { + margin-bottom: 10px; + } +} +</style> diff --git a/src/views/personnelManagement/selfService/index.vue b/src/views/personnelManagement/selfService/index.vue new file mode 100644 index 0000000..1f4fbea --- /dev/null +++ b/src/views/personnelManagement/selfService/index.vue @@ -0,0 +1,525 @@ +<template> + <div class="app-container self-service-container"> + + <!-- 鍔熻兘瀵艰埅鍗$墖 --> + <el-row :gutter="20" class="nav-cards"> + <el-col :span="6" v-for="(item, index) in navItems" :key="index"> + <el-card class="nav-card" @click="handleNavClick(item.type)"> + <div class="nav-content"> + <el-icon :size="40" class="nav-icon"> + <component :is="item.icon" /> + </el-icon> + <h3>{{ item.title }}</h3> + <p>{{ item.desc }}</p> + </div> + </el-card> + </el-col> + </el-row> + + <!-- 涓昏鍐呭鍖哄煙 --> + <div class="main-content"> + <!-- 鑰冨嫟璁板綍 --> + <el-card v-if="currentView === 'attendance'" class="content-card"> + <template #header> + <div class="card-header"> + <span>涓汉鑰冨嫟璁板綍</span> + <el-button type="primary" @click="addAttendanceRecord">鏂板璁板綍</el-button> + </div> + </template> + <el-table :data="attendanceData" style="width: 100%"> + <el-table-column prop="date" label="鏃ユ湡" /> + <el-table-column prop="checkIn" label="绛惧埌鏃堕棿" /> + <el-table-column prop="checkOut" label="绛鹃��鏃堕棿" /> + <el-table-column prop="workHours" label="宸ヤ綔鏃堕暱" width="100" /> + <el-table-column prop="status" label="鐘舵��" width="100"> + <template #default="scope"> + <el-tag :type="scope.row.status === '姝e父' ? 'success' : 'danger'"> + {{ scope.row.status }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="150"> + <template #default="scope"> + <el-button size="small" @click="editAttendanceRecord(scope.row)">缂栬緫</el-button> + <el-button size="small" type="danger" @click="deleteAttendanceRecord(scope.$index)">鍒犻櫎</el-button> + </template> + </el-table-column> + </el-table> + </el-card> + + <!-- 钖祫鍗� --> + <el-card v-if="currentView === 'salary'" class="content-card"> + <template #header> + <div class="card-header"> + <span>钖祫鍗曟煡璇�</span> + <el-date-picker v-model="salaryMonth" type="month" placeholder="閫夋嫨鏈堜唤" /> + </div> + </template> + <el-table :data="salaryData" style="width: 100%"> + <el-table-column prop="month" label="鏈堜唤" /> + <el-table-column prop="basicSalary" label="鍩烘湰宸ヨ祫" /> + <el-table-column prop="bonus" label="濂栭噾" /> + <el-table-column prop="deduction" label="鎵f" /> + <el-table-column prop="total" label="瀹炲彂宸ヨ祫" /> + <el-table-column prop="status" label="鐘舵��" > + <template #default="scope"> + <el-tag :type="scope.row.status === '宸插彂鏀�' ? 'success' : 'warning'"> + {{ scope.row.status }} + </el-tag> + </template> + </el-table-column> + </el-table> + </el-card> + + <!-- 鍋囨湡鐢宠 --> + <el-card v-if="currentView === 'leave'" class="content-card"> + <template #header> + <div class="card-header"> + <span>鍋囨湡鐢宠绠$悊</span> + <el-button type="primary" @click="showLeaveDialog = true">鐢宠鍋囨湡</el-button> + </div> + </template> + <el-table :data="leaveData" style="width: 100%"> + <el-table-column prop="type" label="鍋囨湡绫诲瀷" /> + <el-table-column prop="startDate" label="寮�濮嬫棩鏈�" /> + <el-table-column prop="endDate" label="缁撴潫鏃ユ湡" /> + <el-table-column prop="days" label="澶╂暟" width="80" /> + <el-table-column prop="reason" label="鐢宠鍘熷洜" /> + <el-table-column prop="status" label="瀹℃壒鐘舵��" width="100"> + <template #default="scope"> + <el-tag :type="getStatusType(scope.row.status)"> + {{ scope.row.status }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="150"> + <template #default="scope"> + <el-button size="small" @click="editLeaveRecord(scope.row)">缂栬緫</el-button> + <el-button size="small" type="danger" @click="deleteLeaveRecord(scope.$index)">鍒犻櫎</el-button> + </template> + </el-table-column> + </el-table> + </el-card> + + <!-- 涓汉淇℃伅 --> + <el-card v-if="currentView === 'profile'" class="content-card"> + <template #header> + <div class="card-header"> + <span>涓汉淇℃伅缁存姢</span> + <el-button type="primary" @click="editProfile = true">缂栬緫淇℃伅</el-button> + </div> + </template> + <el-descriptions :column="2" border> + <el-descriptions-item label="濮撳悕">{{ profile.name }}</el-descriptions-item> + <el-descriptions-item label="宸ュ彿">{{ profile.employeeId }}</el-descriptions-item> + <el-descriptions-item label="閮ㄩ棬">{{ profile.department }}</el-descriptions-item> + <el-descriptions-item label="鑱屼綅">{{ profile.position }}</el-descriptions-item> + <el-descriptions-item label="鍏ヨ亴鏃ユ湡">{{ profile.hireDate }}</el-descriptions-item> + <el-descriptions-item label="鑱旂郴鐢佃瘽">{{ profile.phone }}</el-descriptions-item> + <el-descriptions-item label="閭">{{ profile.email }}</el-descriptions-item> + <el-descriptions-item label="鍦板潃">{{ profile.address }}</el-descriptions-item> + </el-descriptions> + </el-card> + </div> + + <!-- 鍋囨湡鐢宠寮圭獥 --> + <el-dialog v-model="showLeaveDialog" title="鐢宠鍋囨湡" width="500px"> + <el-form :model="leaveForm" label-width="100px"> + <el-form-item label="鍋囨湡绫诲瀷"> + <el-select v-model="leaveForm.type" placeholder="璇烽�夋嫨鍋囨湡绫诲瀷"> + <el-option label="骞村亣" value="骞村亣" /> + <el-option label="鐥呭亣" value="鐥呭亣" /> + <el-option label="璋冧紤" value="璋冧紤" /> + <el-option label="浜嬪亣" value="浜嬪亣" /> + </el-select> + </el-form-item> + <el-form-item label="寮�濮嬫棩鏈�"> + <el-date-picker v-model="leaveForm.startDate" type="date" placeholder="閫夋嫨寮�濮嬫棩鏈�" /> + </el-form-item> + <el-form-item label="缁撴潫鏃ユ湡"> + <el-date-picker v-model="leaveForm.endDate" type="date" placeholder="閫夋嫨缁撴潫鏃ユ湡" /> + </el-form-item> + <el-form-item label="鐢宠鍘熷洜"> + <el-input v-model="leaveForm.reason" type="textarea" rows="3" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="showLeaveDialog = false">鍙栨秷</el-button> + <el-button type="primary" @click="submitLeaveApplication">鎻愪氦鐢宠</el-button> + </template> + </el-dialog> + + <!-- 鏂板鑰冨嫟璁板綍寮圭獥 --> + <el-dialog v-model="showAttendanceDialog" title="鏂板鑰冨嫟璁板綍" width="500px"> + <el-form :model="attendanceForm" :rules="attendanceRules" ref="attendanceFormRef" label-width="100px"> + <el-form-item label="鏃ユ湡" prop="date"> + <el-date-picker v-model="attendanceForm.date" type="date" placeholder="閫夋嫨鏃ユ湡" /> + </el-form-item> + <el-form-item label="绛惧埌鏃堕棿" prop="checkIn"> + <el-time-picker v-model="attendanceForm.checkIn" placeholder="閫夋嫨绛惧埌鏃堕棿" format="HH:mm" value-format="HH:mm" /> + </el-form-item> + <el-form-item label="绛鹃��鏃堕棿" prop="checkOut"> + <el-time-picker v-model="attendanceForm.checkOut" placeholder="閫夋嫨绛鹃��鏃堕棿" format="HH:mm" value-format="HH:mm" /> + </el-form-item> + <el-form-item label="鐘舵��" prop="status"> + <el-select v-model="attendanceForm.status" placeholder="璇烽�夋嫨鐘舵��"> + <el-option label="姝e父" value="姝e父" /> + <el-option label="杩熷埌" value="杩熷埌" /> + <el-option label="鏃╅��" value="鏃╅��" /> + <el-option label="缂哄嫟" value="缂哄嫟" /> + </el-select> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="showAttendanceDialog = false">鍙栨秷</el-button> + <el-button type="primary" @click="submitAttendance">鎻愪氦</el-button> + </template> + </el-dialog> + + <!-- 涓汉淇℃伅缂栬緫寮圭獥 --> + <el-dialog v-model="editProfile" title="缂栬緫涓汉淇℃伅" width="500px"> + <el-form :model="profileForm" label-width="100px"> + <el-form-item label="濮撳悕"> + <el-input v-model="profileForm.name" /> + </el-form-item> + <el-form-item label="鑱旂郴鐢佃瘽"> + <el-input v-model="profileForm.phone" /> + </el-form-item> + <el-form-item label="閭"> + <el-input v-model="profileForm.email" /> + </el-form-item> + <el-form-item label="鍦板潃"> + <el-input v-model="profileForm.address" type="textarea" rows="2" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="editProfile = false">鍙栨秷</el-button> + <el-button type="primary" @click="saveProfile">淇濆瓨</el-button> + </template> + </el-dialog> + </div> +</template> + +<script setup> +import { ref, reactive, watch } from 'vue' +import { ElMessage } from 'element-plus' +import { + Calendar, + Money, + Clock, + User +} from '@element-plus/icons-vue' + +// 褰撳墠瑙嗗浘 +const currentView = ref('attendance') + +// 瀵艰埅椤� +const navItems = [ + { type: 'attendance', title: '鑰冨嫟璁板綍', desc: '鏌ヨ涓汉鑰冨嫟淇℃伅', icon: 'Calendar' }, + { type: 'salary', title: '钖祫鍗�', desc: '鏌ョ湅钖祫鍙戞斁璁板綍', icon: 'Money' }, + { type: 'leave', title: '鍋囨湡鐢宠', desc: '鍦ㄧ嚎鐢宠鍚勭被鍋囨湡', icon: 'Clock' }, + { type: 'profile', title: '涓汉淇℃伅', desc: '缁存姢涓汉鍩烘湰淇℃伅', icon: 'User' } +] + +// 鑰冨嫟鏁版嵁 +const attendanceData = ref([ + { date: '2024-01-15', checkIn: '09:00', checkOut: '18:00', workHours: '9灏忔椂', status: '姝e父' }, + { date: '2024-01-16', checkIn: '08:55', checkOut: '18:05', workHours: '9灏忔椂10鍒�', status: '姝e父' }, + { date: '2024-01-17', checkIn: '09:15', checkOut: '18:00', workHours: '8灏忔椂45鍒�', status: '杩熷埌' } +]) + +// 钖祫鏁版嵁 +const salaryData = ref([ + { month: '2024-01', basicSalary: 8000, bonus: 1000, deduction: 200, total: 8800, status: '宸插彂鏀�' }, + { month: '2023-12', basicSalary: 8000, bonus: 800, deduction: 150, total: 8650, status: '宸插彂鏀�' } +]) + +// 鍋囨湡鏁版嵁 +const leaveData = ref([ + { type: '骞村亣', startDate: '2024-02-01', endDate: '2024-02-03', days: 3, reason: '鏄ヨ妭鍥炲', status: '宸查�氳繃' }, + { type: '鐥呭亣', startDate: '2024-01-20', endDate: '2024-01-21', days: 2, reason: '鎰熷啋鍙戠儳', status: '瀹℃壒涓�' } +]) + +// 涓汉淇℃伅 +const profile = ref({ + name: '寮犳捣娲�', + employeeId: 'EMP001', + department: '鎶�鏈儴', + position: '杞欢宸ョ▼甯�', + hireDate: '2023-03-01', + phone: '13800138000', + email: 'zhangsan@company.com', + address: '鍖椾含甯傛湞闃冲尯xxx琛楅亾xxx鍙�' +}) + +// 寮圭獥鎺у埗 +const showLeaveDialog = ref(false) +const editProfile = ref(false) +const salaryMonth = ref('') + +// 琛ㄥ崟鏁版嵁 +const leaveForm = reactive({ + type: '', + startDate: '', + endDate: '', + reason: '' +}) + +const profileForm = reactive({ + name: '', + phone: '', + email: '', + address: '' +}) + +// 鏂板鑰冨嫟璁板綍锛氬脊绐椾笌琛ㄥ崟 +const showAttendanceDialog = ref(false) +const attendanceFormRef = ref(null) +const attendanceForm = reactive({ + date: '', + checkIn: '', + checkOut: '', + status: '姝e父' +}) +const attendanceRules = { + date: [{ required: true, message: '璇烽�夋嫨鏃ユ湡', trigger: 'change' }], + checkIn: [{ required: true, message: '璇烽�夋嫨绛惧埌鏃堕棿', trigger: 'change' }], + checkOut: [{ required: true, message: '璇烽�夋嫨绛鹃��鏃堕棿', trigger: 'change' }], + status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }] +} + +// 澶勭悊瀵艰埅鐐瑰嚮 +const handleNavClick = (type) => { + currentView.value = type +} + +// 鑾峰彇鐘舵�佺被鍨� +const getStatusType = (status) => { + const types = { + '宸查�氳繃': 'success', + '瀹℃壒涓�': 'warning', + '宸叉嫆缁�': 'danger' + } + return types[status] || 'info' +} + +// 鏂板鑰冨嫟璁板綍锛堟墦寮�寮圭獥骞堕濉粯璁ゅ�硷級 +const addAttendanceRecord = () => { + attendanceForm.date = new Date().toISOString().split('T')[0] + attendanceForm.checkIn = '09:00' + attendanceForm.checkOut = '18:00' + attendanceForm.status = '姝e父' + showAttendanceDialog.value = true +} + +// 璁$畻宸ユ椂 +const computeWorkHours = (inStr, outStr) => { + const [inH, inM] = inStr.split(':').map(n => parseInt(n, 10)) + const [outH, outM] = outStr.split(':').map(n => parseInt(n, 10)) + const inMin = inH * 60 + inM + const outMin = outH * 60 + outM + const diff = Math.max(0, outMin - inMin) + const h = Math.floor(diff / 60) + const m = diff % 60 + return m === 0 ? `${h}灏忔椂` : `${h}灏忔椂${m}鍒哷 +} + +// 鎻愪氦鏂板鑰冨嫟璁板綍 +const submitAttendance = () => { + if (!attendanceFormRef.value) return + attendanceFormRef.value.validate((valid) => { + if (!valid) return + const workHours = computeWorkHours(attendanceForm.checkIn, attendanceForm.checkOut) + const newRecord = { + date: attendanceForm.date, + checkIn: attendanceForm.checkIn, + checkOut: attendanceForm.checkOut, + workHours, + status: attendanceForm.status + } + attendanceData.value.unshift(newRecord) + showAttendanceDialog.value = false + // 閲嶇疆琛ㄥ崟 + attendanceForm.date = '' + attendanceForm.checkIn = '' + attendanceForm.checkOut = '' + attendanceForm.status = '姝e父' + ElMessage.success('鑰冨嫟璁板綍娣诲姞鎴愬姛') + }) +} + +// 缂栬緫鑰冨嫟璁板綍 +const editAttendanceRecord = (row) => { + ElMessage.info('缂栬緫鍔熻兘寮�鍙戜腑...') +} + +// 鍒犻櫎鑰冨嫟璁板綍 +const deleteAttendanceRecord = (index) => { + attendanceData.value.splice(index, 1) + ElMessage.success('鑰冨嫟璁板綍鍒犻櫎鎴愬姛') +} + +// 缂栬緫鍋囨湡璁板綍 +const editLeaveRecord = (row) => { + ElMessage.info('缂栬緫鍔熻兘寮�鍙戜腑...') +} + +// 鍒犻櫎鍋囨湡璁板綍 +const deleteLeaveRecord = (index) => { + leaveData.value.splice(index, 1) + ElMessage.success('鍋囨湡璁板綍鍒犻櫎鎴愬姛') +} + +// 鎻愪氦鍋囨湡鐢宠 +const submitLeaveApplication = () => { + if (!leaveForm.type || !leaveForm.startDate || !leaveForm.endDate || !leaveForm.reason) { + ElMessage.warning('璇峰~鍐欏畬鏁翠俊鎭�') + return + } + + const newLeave = { + type: leaveForm.type, + startDate: leaveForm.startDate, + endDate: leaveForm.endDate, + days: 3, // 绠�鍗曡绠� + reason: leaveForm.reason, + status: '瀹℃壒涓�' + } + + leaveData.value.unshift(newLeave) + showLeaveDialog.value = false + + // 閲嶇疆琛ㄥ崟 + Object.keys(leaveForm).forEach(key => { + leaveForm[key] = '' + }) + + ElMessage.success('鍋囨湡鐢宠鎻愪氦鎴愬姛') +} + +// 淇濆瓨涓汉淇℃伅 +const saveProfile = () => { + Object.assign(profile.value, profileForm) + editProfile.value = false + ElMessage.success('涓汉淇℃伅淇濆瓨鎴愬姛') +} + +// 鍒濆鍖栦釜浜轰俊鎭〃鍗� +const initProfileForm = () => { + Object.assign(profileForm, { + name: profile.value.name, + phone: profile.value.phone, + email: profile.value.email, + address: profile.value.address + }) +} + +// 鐩戝惉缂栬緫涓汉淇℃伅寮圭獥 +watch(editProfile, (val) => { + if (val) { + initProfileForm() + } +}) +</script> + +<style scoped> +.self-service-container { + padding: 20px; + background-color: #f5f7fa; + min-height: 100vh; +} + +.page-header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.page-header h2 { + color: white; + margin-bottom: 10px; + font-size: 28px; + font-weight: 600; +} + +.page-header p { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + margin: 0; +} + +.nav-cards { + margin-bottom: 30px; +} + +.nav-card { + cursor: pointer; + transition: all 0.3s ease; + border-radius: 12px; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.nav-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.nav-content { + text-align: center; + padding: 20px; +} + +.nav-icon { + color: #409EFF; + margin-bottom: 15px; +} + +.nav-content h3 { + margin: 0 0 10px 0; + color: #303133; + font-size: 18px; +} + +.nav-content p { + margin: 0; + color: #909399; + font-size: 14px; +} + +.main-content { + margin-bottom: 30px; +} + +.content-card { + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: none; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: #303133; +} + +/* 鍝嶅簲寮忚璁� */ +@media (max-width: 768px) { + .self-service-container { + padding: 10px; + } + + .nav-cards .el-col { + margin-bottom: 15px; + } + + .page-header h2 { + font-size: 24px; + } +} +</style> -- Gitblit v1.9.3