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