12f01b616fad4acb9b380875c6335fd00aa21e08..f9a9009a9ead99c731eccfbf66ca943fb601b9e2
2026-02-26 gaoluyang
湟水峡 1.人员模块代码迁移
f9a900 对比 | 目录
2026-02-26 gaoluyang
湟水峡 1.人员模块代码迁移
2cf422 对比 | 目录
已添加12个文件
已修改13个文件
已删除5个文件
4805 ■■■■■ 文件已修改
src/api/personnelManagement/attendanceRules.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/monthlyStatistics.js 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/onboarding.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/personalAttendanceRecords.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/staffAnalytics.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/staffOnJob.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/ledger/Form.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/analytics/index.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 507 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/components/formDia.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 425 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/index.vue 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/RenewContract.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/Show.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/formDia.vue 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/formDia.vue 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/onboarding/components/formDia.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/onboarding/components/formDiaXJHT.vue 427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/onboarding/index.vue 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/payrollManagement/components/formDia.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/payrollManagement/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/scheduling/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/selfService/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/attendanceRules.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import request from "@/utils/request";
// èŽ·å–æ‰“å¡è§„åˆ™åˆ—è¡¨
export function getAttendanceRules(query) {
  return request({
    url: "/personalAttendanceLocationConfig/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæ‰“卡规则
export function addAttendanceRule(data) {
  return request({
    url: "/personalAttendanceLocationConfig/add",
    method: "post",
    data,
  });
}
// æ›´æ–°æ‰“卡规则
export function updateAttendanceRule(data) {
  return request({
    url: "/attendanceRules/update",
    method: "put",
    data,
  });
}
// åˆ é™¤æ‰“卡规则
export function deleteAttendanceRule(ids) {
  return request({
    url: `/personalAttendanceLocationConfig/del`,
    method: "delete",
    data: ids,
  });
}
// èŽ·å–å•ä¸ªæ‰“å¡è§„åˆ™è¯¦æƒ…
export function getAttendanceRuleDetail(id) {
  return request({
    url: `/attendanceRules/detail/${id}`,
    method: "get",
  });
}
src/api/personnelManagement/monthlyStatistics.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
import request from "@/utils/request";
// äººå‘˜è–ªèµ„台账列表
export function monthlyStatisticsListPage(query) {
  return request({
    url: "/compensationPerformance/listPage",
    method: "get",
    params: query,
  });
}
// äººå‘˜è–ªèµ„台账详情
export function monthlyStatisticsGet(id) {
  return request({
    url: "/monthlyStatistics/get",
    method: "get",
    params: { id },
  });
}
// æ–°å¢žäººå‘˜è–ªèµ„台账
export function monthlyStatisticsAdd(data) {
  return request({
    url: "/compensationPerformance/add",
    method: "post",
    data,
  });
}
// ç¼–辑人员薪资台账
export function monthlyStatisticsUpdate(data) {
  return request({
    url: "/compensationPerformance/update",
    method: "post",
    data,
  });
}
// åˆ é™¤äººå‘˜è–ªèµ„台账
export function monthlyStatisticsDelete(ids) {
  return request({
    url: "/compensationPerformance/delete",
    method: "delete",
    data: ids,
  });
}
// å¯¼å‡ºäººå‘˜è–ªèµ„台账
export function monthlyStatisticsExport(query) {
  return request({
    url: "/compensationPerformance/export",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
// äººå‘˜åˆ—表
export function staffOnJobList(query) {
  return request({
    url: "/staff/staffOnJob/list",
    method: "get",
    params: query,
  });
}
src/api/personnelManagement/onboarding.js
ÎļþÒÑɾ³ý
src/api/personnelManagement/personalAttendanceRecords.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import request from "@/utils/request.js";
export function createPersonalAttendanceRecord(params) {
    return request({
        url: "/personalAttendanceRecords",
        method: "post",
        data: params,
    });
}
export function findPersonalAttendanceRecords(query) {
    return request({
        url: "/personalAttendanceRecords/listPage",
        method: "get",
        params: query,
    });
}
export function findTodayPersonalAttendanceRecord(query) {
    return request({
        url: "/personalAttendanceRecords/today",
        method: "get",
        params: query,
    });
}
src/api/personnelManagement/staffAnalytics.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
import request from "@/utils/request.js";
// ç¦»èŒåŽŸå› åˆ†æž
export function findStaffLeaveReasonAnalysis() {
    return request({
        url: "/staff/analytics/reason",
        method: "get"
    });
}
// 12个月员工流动流失率分析
export function findStaffAnalysisMonthlyTurnoverRateFor12Months() {
    return request({
        url: "/staff/analytics/monthly_turnover_rate",
        method: "get"
    });
}
export function findStaffAnalysisTotalStatistic() {
    return request({
        url: "/staff/analytics/total_statistic",
        method: "get"
    });
}
src/api/personnelManagement/staffOnJob.js
@@ -17,6 +17,15 @@
    })
}
// æŸ¥è¯¢å‘˜å·¥å…¥èŒä¿¡æ¯
export function getStaffOnJobInfoByUserName(query) {
    return request({
        url: '/staff/staffOnJob/byUserName',
        method: 'get',
        params: query,
    })
}
// æ–°å¢žå‘˜å·¥
export function createStaffOnJob(params) {
    return request({
src/views/lavorissue/ledger/Form.vue
@@ -10,15 +10,15 @@
        <el-option :label="item.deptName" :value="item.deptId" v-for="(item,index) in productOptions" :key="deptId" />
      </el-select>
        </el-form-item>
        <el-form-item label="员工名称" prop="staffId">
          <el-select
            v-model="form.staffId"
            placeholder="请选择"
            clearable
          >
            <el-option :label="item.staffName" :value="item.id" v-for="(item,index) in personList" :key="id" />
          </el-select>
        </el-form-item>
<!--        <el-form-item label="员工名称" prop="staffId">-->
<!--          <el-select-->
<!--            v-model="form.staffId"-->
<!--            placeholder="请选择"-->
<!--            clearable-->
<!--          >-->
<!--            <el-option :label="item.staffName" :value="item.id" v-for="(item,index) in personList" :key="item.id" />-->
<!--          </el-select>-->
<!--        </el-form-item>-->
        <el-form-item label="劳保类型" prop="dictType">
          <el-select
              v-model="form.dictType"
@@ -73,7 +73,7 @@
import useFormData from "@/hooks/useFormData";
import {ref,onMounted} from "vue";
import useUserStore from "@/store/modules/user";
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
// import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import {deepCopySameProperties} from '@/utils/util'
const userStore = useUserStore();
import {
src/views/personnelManagement/analytics/index.vue
@@ -1,5 +1,5 @@
<template>
  <div class="app-container analytics-container">
  <div class="app-container analytics-container" v-loading="loading">
    <!-- å…³é”®æŒ‡æ ‡å¡ç‰‡ -->
    <el-row :gutter="20" class="metrics-cards">
@@ -11,18 +11,18 @@
                <component :is="item.icon" />
              </el-icon>
            </div>
              <div class="card-info">
            <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'" v-if="item.showTrend !== false">
                <el-icon>
                  <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" />
                </el-icon>
                {{ Math.abs(item.trend) }}%
              </div>
<!--              <div class="card-trend" :class="item.trend > 0 ? 'positive' : 'negative'" v-if="item.showTrend !== false">-->
<!--                <el-icon>-->
<!--                  <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" />-->
<!--                </el-icon>-->
<!--                {{ Math.abs(item.trend) }}%-->
<!--              </div>-->
            </div>
          </div>
        </el-card>
@@ -64,21 +64,6 @@
    <!-- ç¬¬äºŒè¡Œå›¾è¡¨ -->
    <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">
@@ -98,19 +83,15 @@
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, nextTick } 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'
import { staffOnJobListPage } from '@/api/personnelManagement/employeeRecord.js'
import {listDept} from "@/api/system/dept.js";
import {
  findStaffAnalysisMonthlyTurnoverRateFor12Months,
  findStaffLeaveReasonAnalysis,
  findStaffAnalysisTotalStatistic
} from "@/api/personnelManagement/staffAnalytics.js";
// å“åº”式数据
const loading = ref(false)
@@ -151,14 +132,6 @@
    trend: 0
  },
  {
    label: '编制达成率',
    value: 0,
    unit: '%',
    icon: 'DataAnalysis',
    type: 'success',
    trend: 0
  },
  {
    label: '在职员工数',
    value: 0,
    unit: '人',
@@ -171,16 +144,56 @@
// éƒ¨é—¨æ•°æ®
const departmentData = ref([])
// å‘˜å·¥æµå¤±åŽŸå› åˆ†æžæ•°æ®
const staffLeaveReasons = ref([])
// 12个月员工流动流失率分析数据
const turnoverRateStatistics = ref([])
// èŽ·å–åœ¨èŒå‘˜å·¥æ•°
const getStaffCount = async () => {
// èŽ·å–éƒ¨é—¨æ•°æ®
const getDepartmentData = async () => {
  try {
    const res = await staffOnJobListPage({ staffState: 1, current: 1, size: 1 })
    const res = await listDept()
    if (res && res.data) {
      keyMetrics.value[3].value = res.data.total || 0
      departmentData.value = res.data
    }
  } catch (error) {
    console.error('获取在职员工数失败:', error)
    console.error('获取部门数据失败:', error)
  }
}
const getStaffLeaveReasonAnalysis = async () => {
  try {
    const res = await findStaffLeaveReasonAnalysis()
    if (res && res.data) {
      staffLeaveReasons.value = res.data || []
    }
  } catch (error) {
    console.error('获取员工流失原因分析失败:', error)
  }
}
// ä¿®æ”¹ä¸ºè¿”回Promise的异步函数
const getMonthlyTurnoverRateFor12Months = async () => {
  try {
    const res = await findStaffAnalysisMonthlyTurnoverRateFor12Months()
    if (res && res.data) {
      turnoverRateStatistics.value = res.data || []
    }
  } catch (error) {
    console.error('获取12个月员工流动流失率分析数据失败:', error)
  }
}
const getStaffAnalysisTotalStatistic = async () => {
  try {
    const res = await findStaffAnalysisTotalStatistic()
    if (res && res.data) {
      keyMetrics.value[0].value = res.data.totalFlowRate || 0
      keyMetrics.value[1].value = res.data.totalTurnoverRate || 0
      keyMetrics.value[2].value = res.data.currentOnJobCount || 0
    }
  } catch (error) {
    console.error('获取员工分析总统计数据失败:', error)
  }
}
@@ -213,49 +226,28 @@
  }
}
// ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
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)
  // ç”Ÿæˆéƒ¨é—¨æ•°æ®
  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 ? '异常' : '正常'
  }))
}
// åˆ·æ–°æ•°æ®
// ä¿®æ”¹ä¸ºå¼‚步函数,确保数据加载完成后再渲染图表
const refreshData = async () => {
  loading.value = true
  try {
    // æ¨¡æ‹ŸAPI调用延迟
    await new Promise(resolve => setTimeout(resolve, 500))
    generateMockData()
    loading.value = true
    // ç­‰å¾…所有数据加载完成
    await Promise.all([
      getDepartmentData(),
      getStaffLeaveReasonAnalysis(),
      getMonthlyTurnoverRateFor12Months(),
      getStaffAnalysisTotalStatistic()
    ])
    await nextTick()
    renderAllCharts()
    if (!autoRefreshEnabled.value) {
      ElMessage.success('数据刷新成功')
    }
  } catch (error) {
    console.error('刷新数据失败:', error)
    ElMessage.error('刷新数据失败')
    console.error('数据刷新失败:', error)
    ElMessage.error('数据刷新失败')
  } finally {
    loading.value = false
  }
@@ -276,8 +268,9 @@
    if (attritionChartRef.value) {
      attritionChart = echarts.init(attritionChartRef.value)
    }
    renderAllCharts()
    // åˆå§‹åŒ–时也先加载数据再渲染图表
    refreshData()
  }, 300)
}
@@ -289,14 +282,15 @@
  renderAttritionChart()
}
// æ¸²æŸ“员工流动率趋势图
// ä¿®æ”¹ä¸ºä½¿ç”¨API返回的实际数据
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))
  // ä½¿ç”¨API返回的实际数据
  const months = turnoverRateStatistics.value.map(item => item.month)
  const turnoverData = turnoverRateStatistics.value.map(item => item.flowRate || 0)
  const attritionData = turnoverRateStatistics.value.map(item => item.turnoverRate || 0)
  const option = {
    title: {
      text: '员工流动率趋势',
@@ -346,19 +340,19 @@
      }
    ]
  }
  turnoverChart.setOption(option)
}
// æ¸²æŸ“部门人员分布图
const renderDepartmentChart = () => {
  if (!departmentChart) return
  const data = departmentData.value.map(item => ({
    name: item.department,
    value: item.currentStaff
    name: item.deptName,
    value: item.staffCount
  }))
  const option = {
    title: {
      text: '部门人员分布',
@@ -391,17 +385,17 @@
      }
    ]
  }
  departmentChart.setOption(option)
}
// æ¸²æŸ“编制达成率图
const renderStaffingChart = () => {
  if (!staffingChart) return
  const departments = departmentData.value.map(item => item.department)
  const departments = departmentData.value.map(item => item.deptName)
  const rates = departmentData.value.map(item => item.staffingRate)
  const option = {
    title: {
      text: '编制达成率',
@@ -445,17 +439,17 @@
      }
    ]
  }
  staffingChart.setOption(option)
}
// æ¸²æŸ“员工流失原因分析图
const renderAttritionChart = () => {
  if (!attritionChart) return
  const reasons = ['薪资待遇', '职业发展', '工作环境', '个人原因', '其他']
  const data = reasons.map(() => Math.floor(Math.random() * 20 + 5))
  const reasons = staffLeaveReasons.value.map(item => item.reasonText)
  const data = staffLeaveReasons.value.map(item => item.count)
  const option = {
    title: {
      text: '员工流失原因分析',
@@ -491,14 +485,12 @@
      }
    ]
  }
  attritionChart.setOption(option)
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  generateMockData()
  getStaffCount()
  initCharts()
  startAutoRefresh()
})
@@ -663,32 +655,32 @@
  .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;
  }
@@ -698,11 +690,11 @@
  .page-header h2 {
    font-size: 20px;
  }
  .card-number {
    font-size: 24px;
  }
  .chart-container {
    height: 250px;
  }
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,496 @@
<template>
  <el-dialog v-model="dialogVisible"
             :title="dialogTitle"
             width="700px"
             :close-on-click-modal="false">
    <el-form ref="formRef"
             :model="form"
             :rules="rules"
             label-width="120px"
             class="mt8">
      <!-- éƒ¨é—¨é€‰æ‹© -->
      <el-form-item label="部门"
                    prop="sysDeptId">
        <el-tree-select v-model="form.sysDeptId"
                        :data="deptOptions"
                        :props="{ value: 'id', label: 'label', children: 'children' }"
                        value-key="id"
                        placeholder="请选择部门"
                        check-strictly
                        style="width: 100%"
                        :disabled="['edit', 'view'].includes(operationType)" />
      </el-form-item>
      <!-- åœ°ç‚¹ä¿¡æ¯ -->
      <!-- <el-form-item label="地点名称"
                    prop="locationName">
        <el-input v-model="form.locationName"
                  placeholder="请输入地点名称"
                  :disabled="operationType === 'view'" />
      </el-form-item> -->
      <!-- æ‰“卡范围 -->
      <el-form-item label="打卡范围(m)"
                    prop="radius">
        <el-input-number v-model="form.radius"
                         :min="10"
                         :max="1000"
                         :step="10"
                         placeholder="请输入打卡范围"
                         :disabled="operationType === 'view'" />
      </el-form-item>
      <!-- é«˜å¾·åœ°å›¾é€‰æ‹© -->
      <el-form-item label="打卡位置"
                    prop="longitude">
        <div class="map-container">
          <div class="map-header"
               style="margin-bottom: 10px">
            <!-- <el-button @click="getCurrentLocation">
              <el-icon>
                <Position />
              </el-icon>
              å½“前位置
            </el-button> -->
            <!-- <span style="margin-left: 10px; color: #909399;font-size: 12px;">点击地图选择位置</span> -->
          </div>
          <div id="map-container"
               class="map"
               ref="mapContainer"></div>
          <div class="coordinates-info mt10">
            <el-input v-model="form.longitude"
                      readonly
                      placeholder="经度"
                      style="width: 140px; margin-right: 10px" />
            <el-input v-model="form.latitude"
                      readonly
                      placeholder="纬度"
                      style="width: 140px; margin-right: 10px" />
            <!-- <el-input v-model="form.locationName"
                      placeholder="地点名称"
                      style="width: calc(100% - 290px)" /> -->
          </div>
        </div>
      </el-form-item>
      <el-form-item label="地点名称"
                    prop="locationName">
        <el-input v-model="form.locationName"
                  :disabled="operationType === 'view'"
                  placeholder="请输入地点名称" />
      </el-form-item>
      <!-- ä¸Šä¸‹ç­æ—¶é—´ -->
      <el-form-item label="上班时间"
                    prop="startAt">
        <el-time-picker v-model="form.startAt"
                        format="HH:mm"
                        value-format="HH:mm"
                        placeholder="请选择上班时间"
                        :disabled="operationType === 'view'" />
      </el-form-item>
      <el-form-item label="下班时间"
                    prop="endAt">
        <el-time-picker v-model="form.endAt"
                        format="HH:mm"
                        value-format="HH:mm"
                        :picker-options="{
      minTime: form.startAt
    }"
                        placeholder="请选择下班时间"
                        :disabled="operationType === 'view'" />
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="submitForm"
                   v-if="operationType !== 'view'">
          ç¡®å®š
        </el-button>
      </span>
    </template>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, computed, watch, onMounted, nextTick } from "vue";
  import { ElMessage } from "element-plus";
  import { Position } from "@element-plus/icons-vue";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { addAttendanceRule } from "@/api/personnelManagement/attendanceRules.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    operationType: {
      type: String,
      default: "add",
    },
    row: {
      type: Object,
      default: () => ({}),
    },
  });
  // const pickerOptions = ref({ minTime: form.value.startAt });
  const emit = defineEmits(["update:modelValue", "close"]);
  const dialogVisible = computed({
    get: () => props.modelValue,
    set: val => emit("update:modelValue", val),
  });
  const dialogTitle = computed(() => {
    if (props.operationType === "add") return "新增打卡规则";
    if (props.operationType === "edit") return "编辑打卡规则";
    return "查看打卡规则";
  });
  // è¡¨å•数据
  const formRef = ref();
  const form = reactive({
    id: "",
    sysDeptId: "",
    locationName: "",
    longitude: "",
    latitude: "",
    radius: 100,
    startAt: "09:00",
    endAt: "18:00",
  });
  // è¡¨å•验证规则
  const rules = {
    sysDeptId: [{ required: true, message: "请选择部门", trigger: "change" }],
    locationName: [
      { required: true, message: "请输入地点名称", trigger: "blur" },
    ],
    longitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
    latitude: [{ required: true, message: "请选择打卡位置", trigger: "blur" }],
    radius: [{ required: true, message: "请输入打卡范围", trigger: "blur" }],
    startAt: [{ required: true, message: "请选择上班时间", trigger: "change" }],
    endAt: [
      { required: true, message: "请选择下班时间", trigger: "change" },
      {
        validator: (rule, value, callback) => {
          if (form.startAt && value) {
            const startParts = form.startAt.split(":");
            const endParts = value.split(":");
            const startTime =
              parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
            const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
            if (endTime <= startTime) {
              callback(new Error("下班时间不能早于上班时间"));
            } else {
              callback();
            }
          } else {
            callback();
          }
        },
        trigger: "change",
      },
    ],
  };
  // éƒ¨é—¨é€‰é¡¹
  const deptOptions = ref([]);
  // åœ°å›¾ç›¸å…³
  const mapContainer = ref(null);
  let map = null;
  let marker = null;
  let circle = null;
  // èŽ·å–éƒ¨é—¨åˆ—è¡¨
  const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
      deptOptions.value = filterDisabledDept(
        JSON.parse(JSON.stringify(response.data))
      );
    });
  };
  // è¿‡æ»¤ç¦ç”¨çš„部门
  const filterDisabledDept = deptList => {
    return deptList.filter(dept => {
      if (dept.disabled) {
        return false;
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      }
      return true;
    });
  };
  // åˆå§‹åŒ–地图
  const initMap = () => {
    nextTick(() => {
      if (window.AMap && mapContainer.value) {
        // åˆå§‹åŒ–地图
        map = new window.AMap.Map(mapContainer.value, {
          zoom: 16,
          center: [116.397428, 39.90923], // é»˜è®¤åŒ—京
        });
        // æ·»åŠ æŽ§ä»¶
        window.AMap.plugin(["AMap.ToolBar", "AMap.Scale"], function () {
          map.addControl(new window.AMap.ToolBar());
          map.addControl(new window.AMap.Scale());
        });
        // æ·»åŠ æ ‡è®°
        marker = new window.AMap.Marker({
          position: [116.397428, 39.90923],
          draggable: true,
          cursor: "move",
          title: "拖拽定位",
        });
        map.add(marker);
        // æ·»åŠ åœ†å½¢èŒƒå›´
        circle = new window.AMap.Circle({
          center: [116.397428, 39.90923],
          radius: form.radius,
          strokeColor: "#3366FF",
          strokeOpacity: 0.8,
          strokeWeight: 2,
          fillColor: "#3366FF",
          fillOpacity: 0.2,
        });
        map.add(circle);
        // ç›‘听标记拖拽
        marker.on("dragend", e => {
          const position = e.lnglat;
          const lng = position.getLng();
          const lat = position.getLat();
          form.longitude = lng;
          form.latitude = lat;
          updateCircle(position);
        });
        // ç›‘听标记拖拽开始
        marker.on("dragstart", () => {
          map.setDefaultCursor("move");
        });
        // ç›‘听标记拖拽结束
        marker.on("dragend", () => {
          map.setDefaultCursor("default");
        });
        // ç›‘听地图点击
        map.on("click", e => {
          const position = e.lnglat;
          const lng = position.getLng();
          const lat = position.getLat();
          form.longitude = lng;
          form.latitude = lat;
          updateMarker(position);
          updateCircle(position);
        });
        // å°è¯•获取当前位置并设置为地图中心
        if (navigator.geolocation && !form.longitude && !form.latitude) {
          navigator.geolocation.getCurrentPosition(
            position => {
              console.log("获取到当前位置:", position);
              const { longitude, latitude } = position.coords;
              const currentPosition = [longitude, latitude];
              map.setCenter(currentPosition);
              updateMarker(currentPosition);
              updateCircle(currentPosition);
              form.longitude = longitude;
              form.latitude = latitude;
            },
            error => {
              console.log("获取位置失败,使用默认位置");
            }
          );
        } else if (form.longitude && form.latitude) {
          // å¦‚果有数据,设置到地图
          const position = [form.longitude, form.latitude];
          map.setCenter(position);
          updateMarker(position);
          updateCircle(position);
        }
      }
    });
  };
  // æ›´æ–°æ ‡è®°ä½ç½®
  const updateMarker = position => {
    if (marker) {
      marker.setPosition(position);
    }
  };
  // æ›´æ–°åœ†å½¢èŒƒå›´
  const updateCircle = position => {
    if (circle) {
      circle.setCenter(position);
      circle.setRadius(form.radius);
    }
  };
  // èŽ·å–å½“å‰ä½ç½®
  const getCurrentLocation = () => {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        position => {
          const { longitude, latitude } = position.coords;
          form.longitude = longitude;
          form.latitude = latitude;
          if (map) {
            map.setCenter([longitude, latitude]);
            updateMarker([longitude, latitude]);
            updateCircle([longitude, latitude]);
          }
          // é€†åœ°ç†ç¼–码获取地址
          if (window.AMap) {
            // åŠ è½½Geocoder插件
            window.AMap.plugin("AMap.Geocoder", function () {
              const geocoder = new window.AMap.Geocoder();
              geocoder.getAddress([longitude, latitude], (status, result) => {
                if (status === "complete" && result.regeocode) {
                  form.locationName = result.regeocode.formattedAddress;
                }
              });
            });
          }
        },
        error => {
          ElMessage.error("获取位置失败,请手动选择");
        }
      );
    } else {
      ElMessage.error("浏览器不支持地理定位");
    }
  };
  // ç›‘听半径变化
  watch(
    () => form.radius,
    newValue => {
      if (circle) {
        circle.setRadius(newValue);
      }
    }
  );
  // ç›‘听上班时间变化,触发下班时间校验
  watch(
    () => form.startAt,
    () => {
      if (formRef.value && form.endAt) {
        formRef.value.validateField("endAt");
      }
    }
  );
  // ç›‘听弹窗显示
  watch(
    () => dialogVisible.value,
    newValue => {
      if (newValue) {
        // é‡ç½®è¡¨å•
        Object.assign(form, {
          id: "",
          sysDeptId: "",
          locationName: "",
          longitude: "",
          latitude: "",
          radius: 100,
          startAt: "09:00",
          endAt: "18:00",
        });
        // å¦‚果是编辑或查看,填充数据
        if (props.operationType !== "add" && props.row.id) {
          // å¤„理时间格式,确保是HH:mm格式
          const rowData = { ...props.row };
          if (rowData.startAt && rowData.startAt.includes(":")) {
            rowData.startAt = rowData.startAt.split(":").slice(0, 2).join(":");
          }
          if (rowData.endAt && rowData.endAt.includes(":")) {
            rowData.endAt = rowData.endAt.split(":").slice(0, 2).join(":");
          }
          Object.assign(form, rowData);
        }
        // åˆå§‹åŒ–地图
        setTimeout(() => {
          initMap();
        }, 100);
      }
    }
  );
  // æäº¤è¡¨å•
  const submitForm = () => {
    formRef.value.validate(valid => {
      if (valid) {
        const submitData = {
          ...form,
          // è½¬æ¢æ—¶é—´æ ¼å¼ï¼Œç¡®ä¿åªä¿ç•™æ—¶åˆ†éƒ¨åˆ†
          startAt: form.startAt
            ? `${form.startAt.split(":").slice(0, 2).join(":")}`
            : null,
          endAt: form.endAt
            ? `${form.endAt.split(":").slice(0, 2).join(":")}`
            : null,
        };
        if (props.operationType === "add") {
          addAttendanceRule(submitData).then(() => {
            ElMessage.success("新增成功");
            emit("close");
          });
        } else if (props.operationType === "edit") {
          addAttendanceRule(submitData).then(() => {
            ElMessage.success("更新成功");
            emit("close");
          });
        }
      }
    });
  };
  // åˆå§‹åŒ–
  onMounted(() => {
    fetchDeptOptions();
  });
</script>
<style scoped lang="scss">
  .map-container {
    width: 100%;
  }
  .map {
    width: 100%;
    height: 400px;
    border: 1px solid #e4e7ed;
  }
  .coordinates-info {
    display: flex;
    gap: 10px;
  }
  .coordinates-display {
    padding: 10px;
    background-color: #f5f7fa;
    border-radius: 4px;
  }
  .mt10 {
    margin-top: 10px;
  }
  .mt8 {
    margin-top: 8px;
  }
</style>
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,293 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜å’Œæ“ä½œæŒ‰é’® -->
    <div class="page-header">
      <div class="title">打卡规则配置</div>
      <div class="actions">
        <el-button type="primary"
                   @click="openForm('add')">
          <el-icon>
            <Plus />
          </el-icon>
          æ–°å¢žè§„则
        </el-button>
      </div>
    </div>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <!-- <el-form :model="searchForm"
             :inline="true"
             class="search-form mb16">
      <el-form-item label="部门:"
                    prop="countId">
        <el-tree-select v-model="searchForm.countId"
                        :data="deptOptions"
                        :props="{ value: 'id', label: 'label', children: 'children' }"
                        value-key="id"
                        placeholder="请选择部门"
                        check-strictly
                        style="width: 200px" />
      </el-form-item>
      <el-form-item label="地点:"
                    prop="locationName">
        <el-input v-model="searchForm.locationName"
                  placeholder="请输入地点名称"
                  clearable
                  style="width: 200px" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="fetchData">
          <el-icon>
            <Search />
          </el-icon>
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">
          <el-icon>
            <Refresh />
          </el-icon>
          é‡ç½®
        </el-button>
      </el-form-item>
    </el-form> -->
    <!-- è§„则列表 -->
    <el-card shadow="never"
             class="mb16">
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                style="width: 100%"
                row-key="id">
        <el-table-column type="index"
                         label="序号"
                         width="60"
                         align="center" />
        <el-table-column label="部门">
          <template #default="scope">
            {{ getDeptNameById(scope.row.sysDeptId) }}
          </template>
        </el-table-column>
        <el-table-column prop="locationName"
                         label="地点名称" />
        <el-table-column prop="longitude"
                         label="经度" />
        <el-table-column prop="latitude"
                         label="纬度" />
        <el-table-column prop="radius"
                         label="打卡范围(m)" />
        <el-table-column prop="startAt"
                         label="上班时间">
          <template #default="scope">
            {{ scope.row.startAt }}
          </template>
        </el-table-column>
        <el-table-column prop="endAt"
                         label="下班时间">
          <template #default="scope">
            {{ scope.row.endAt }}
          </template>
        </el-table-column>
        <el-table-column label="操作"
                         width="180"
                         fixed="right"
                         align="center">
          <template #default="scope">
            <el-button type="primary"
                       size="small"
                       link
                       @click="openForm('edit', scope.row)">编辑</el-button>
            <el-button type="danger"
                       size="small"
                       link
                       @click="handleDelete(scope.row.id)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination :total="page.total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="paginationChange"
                  class="mt10" />
    </el-card>
    <!-- æ–°å¢ž/编辑规则弹窗 -->
    <rule-form ref="ruleFormRef"
               v-model="dialogVisible"
               :operation-type="operationType"
               :row="currentRow"
               @close="dialogVisible = false; fetchData()" />
  </div>
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Plus, Edit, Delete, Search, Refresh } from "@element-plus/icons-vue";
  import Pagination from "@/components/Pagination/index.vue";
  import RuleForm from "./components/form.vue";
  import { deptTreeSelect } from "@/api/system/user.js";
  import {
    getAttendanceRules,
    deleteAttendanceRule,
  } from "@/api/personnelManagement/attendanceRules.js";
  const { proxy } = getCurrentInstance();
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  const tableLoading = ref(false);
  // åˆ†é¡µå‚æ•°
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // æŸ¥è¯¢è¡¨å•
  const searchForm = reactive({
    countId: "",
    locationName: "",
  });
  // éƒ¨é—¨é€‰é¡¹
  const deptOptions = ref([]);
  // å¼¹çª—控制
  const dialogVisible = ref(false);
  const operationType = ref("add");
  const currentRow = ref({});
  const ruleFormRef = ref();
  // æ ¼å¼åŒ–æ—¶é—´
  const formatTime = timestamp => {
    if (!timestamp) return "";
    const date = new Date(timestamp);
    return `${String(date.getHours()).padStart(2, "0")}:${String(
      date.getMinutes()
    ).padStart(2, "0")}`;
  };
  // èŽ·å–éƒ¨é—¨åˆ—è¡¨
  const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
      deptOptions.value = filterDisabledDept(
        JSON.parse(JSON.stringify(response.data))
      );
    });
  };
  // è¿‡æ»¤ç¦ç”¨çš„部门
  const filterDisabledDept = deptList => {
    return deptList.filter(dept => {
      if (dept.disabled) {
        return false;
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      }
      return true;
    });
  };
  // æ ¹æ®éƒ¨é—¨ID查找部门名称
  const getDeptNameById = (deptId, deptList = deptOptions.value) => {
    for (const dept of deptList) {
      if (dept.id === deptId) {
        return dept.label;
      }
      if (dept.children && dept.children.length) {
        const name = getDeptNameById(deptId, dept.children);
        if (name) {
          return name;
        }
      }
    }
    return "";
  };
  // æŸ¥è¯¢è§„则列表
  const fetchData = () => {
    tableLoading.value = true;
    getAttendanceRules({ ...page, ...searchForm })
      .then(res => {
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // åˆ†é¡µå˜æ›´
  const paginationChange = pagination => {
    page.current = pagination.page;
    page.size = pagination.limit;
    fetchData();
  };
  // é‡ç½®æœç´¢
  const resetSearch = () => {
    searchForm.countId = "";
    searchForm.locationName = "";
    fetchData();
  };
  // æ‰“开表单
  const openForm = (type, row = {}) => {
    operationType.value = type;
    currentRow.value = row;
    dialogVisible.value = true;
  };
  // åˆ é™¤è§„则
  const handleDelete = id => {
    ElMessageBox.confirm("确定要删除这条规则吗?", "删除确认", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        deleteAttendanceRule([id]).then(() => {
          ElMessage.success("删除成功");
          fetchData();
        });
      })
      .catch(() => {
        // å–消删除
      });
  };
  // åˆå§‹åŒ–
  onMounted(() => {
    fetchDeptOptions();
    fetchData();
  });
</script>
<style scoped lang="scss">
  .page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    .title {
      font-size: 18px;
      font-weight: 600;
    }
    .actions {
      display: flex;
      gap: 10px;
    }
  }
  .mb16 {
    margin-bottom: 16px;
  }
  .mt10 {
    margin-top: 10px;
  }
</style>
src/views/personnelManagement/attendanceCheckin/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,507 @@
<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"
                     :disabled="todayRecord.workEndAt">
            {{ checkInOutText }}
          </el-button>
        </div>
      </div>
      <el-descriptions border
                       :column="4"
                       class="mt10">
        <el-descriptions-item label="员工姓名">
          {{ todayRecord.staffName }}
        </el-descriptions-item>
        <el-descriptions-item label="工号">
          {{ todayRecord.staffNo }}
        </el-descriptions-item>
        <el-descriptions-item label="所属部门">
          {{ todayRecord.deptName }}
        </el-descriptions-item>
        <el-descriptions-item label="今日状态">
          <el-tag :type="todayStatusTag"
                  size="small">
            {{ todayStatusText }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="上班时间">
          {{ todayRecord?.workStartAt || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="下班时间">
          {{ todayRecord?.workEndAt || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="工时(小时)">
          {{ todayRecord?.workHours ?? '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="异常标记">
          <span v-if="!todayRecord.id || todayRecord?.status === 0">-</span>
          <el-tag v-else
                  type="danger"
                  size="small">
            {{ todayRecord?.status ? getStatusText(todayRecord.status) : '-' }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
    </el-card> -->
    <div class="attendance-operation">
      <!-- æŸ¥è¯¢æ¡ä»¶ï¼ˆç®¡ç†å‘˜è€ƒå‹¤æ—¥æŠ¥ï¼‰ -->
      <el-form :model="searchForm"
               :inline="true"
               class="search-form">
        <el-form-item label="部门:"
                      prop="deptId">
          <el-tree-select v-model="searchForm.deptId"
                          :data="deptOptions"
                          :props="{ value: 'id', label: 'label', children: 'children' }"
                          value-key="id"
                          placeholder="请选择部门"
                          check-strictly
                          style="width: 200px" />
        </el-form-item>
        <el-form-item label="日期:"
                      prop="date">
          <el-date-picker v-model="searchForm.date"
                          type="date"
                          value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD"
                          placeholder="请选择日期"
                          clearable />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="fetchData">
            <el-icon>
              <Search />
            </el-icon>
            æœç´¢
          </el-button>
          <el-button @click="resetSearch">
            <el-icon>
              <Refresh />
            </el-icon>
            é‡ç½®
          </el-button>
        </el-form-item>
      </el-form>
      <el-button icon="Download"
                 @click="handleExport">
        å¯¼å‡ºè€ƒå‹¤æ—¥æŠ¥
      </el-button>
    </div>
    <!-- è€ƒå‹¤æ—¥æŠ¥è¡¨æ ¼ -->
    <div class="table_list">
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                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="deptName"
                         label="部门"
                         width="140" />
        <el-table-column prop="staffName"
                         label="姓名"
                         width="120" />
        <el-table-column prop="staffNo"
                         label="工号"
                         width="120" />
        <el-table-column prop="workStartAt"
                         label="上班时间"
                         width="140" />
        <el-table-column prop="workEndAt"
                         label="下班时间"
                         width="140" />
        <el-table-column prop="workHours"
                         label="工时(小时)"
                         align="center" />
        <el-table-column prop="status"
                         label="考勤状态"
                         align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.status === 0"
                    type="success"
                    size="small">
              æ­£å¸¸
            </el-tag>
            <el-tag v-else
                    type="danger"
                    size="small">
              <!-- {{ scope.row.status === 1 ? '迟到' : scope.row.status === 2 ? '早退' : '迟到、早退' }} -->
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="remark"
                         label="备注"
                         show-overflow-tooltip />
      </el-table>
      <pagination :total="page.total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="paginationChange" />
    </div>
  </div>
</template>
<script setup>
  import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    createPersonalAttendanceRecord,
    findPersonalAttendanceRecords,
    findTodayPersonalAttendanceRecord,
  } from "@/api/personnelManagement/personalAttendanceRecords.js";
  import Pagination from "@/components/Pagination/index.vue";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { Refresh, Search } from "@element-plus/icons-vue";
  const { proxy } = getCurrentInstance();
  const tableLoading = ref(false);
  // åˆ†é¡µå‚æ•°
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // ä»Šæ—¥æ•°æ®
  const todayRecord = ref({});
  // éƒ¨é—¨é€‰é¡¹
  const deptOptions = ref([]);
  // æŸ¥è¯¢è¡¨å•
  const searchForm = reactive({
    deptId: "",
    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 checkInOutText = computed(() => {
    if (!todayRecord.value || !todayRecord.value.workStartAt) {
      return "上班打卡";
    }
    if (!todayRecord.value.workEndAt) {
      return "下班打卡";
    }
    return "今日已打卡完成";
  });
  // ä»Šæ—¥çŠ¶æ€å±•ç¤º
  const todayStatusTag = computed(() => {
    if (!todayRecord.value.id) return "info";
    if (todayRecord.value.status === 0) return "success";
    return "danger";
  });
  const getStatusText = status => {
    switch (status) {
      case 0:
        return "正常";
      case 1:
        return "迟到";
      case 2:
        return "早退";
      case 3:
        return "迟到、早退";
      case 4:
        return "缺勤";
    }
  };
  const todayStatusText = computed(() => {
    if (!todayRecord.value.id) return "未打卡";
    switch (todayRecord.value.status) {
      case 0:
        return "正常";
      case 1:
        return "迟到";
      case 2:
        return "早退";
      case 3:
        return "迟到、早退";
      case 4:
        return "缺勤";
    }
  });
  // è¡Œæ ·å¼ï¼šå¼‚常高亮
  const rowClassName = ({ row }) => {
    if (row.status === 1 || row.status === 2) {
      return "row-abnormal";
    }
    return "";
  };
  // æŸ¥è¯¢éƒ¨é—¨åˆ—表
  const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
      deptOptions.value = filterDisabledDept(
        JSON.parse(JSON.stringify(response.data))
      );
    });
  };
  /** è¿‡æ»¤ç¦ç”¨çš„部门 */
  function filterDisabledDept(deptList) {
    return deptList.filter(dept => {
      if (dept.disabled) {
        return false;
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      }
      return true;
    });
  }
  // æŸ¥è¯¢
  const fetchData = () => {
    tableLoading.value = true;
    findPersonalAttendanceRecords({ ...page, ...searchForm })
      .then(res => {
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // æŸ¥è¯¢ä»Šæ—¥æ‰“卡信息
  const fetchTodayData = () => {
    // findTodayPersonalAttendanceRecord({}).then(res => {
    //   todayRecord.value = res.data;
    // });
  };
  const paginationChange = pagination => {
    page.current = pagination.page;
    page.size = pagination.limit;
    fetchData();
  };
  const resetSearch = () => {
    searchForm.deptId = "";
    searchForm.date = "";
    fetchData();
  };
  const handleExport = () => {
    ElMessageBox.confirm("是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/personalAttendanceRecords/export", {}, "考勤记录.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // èŽ·å–å½“å‰ä½ç½®
  const getCurrentLocation = () => {
    return new Promise((resolve, reject) => {
      if (!navigator.geolocation) {
        reject(new Error("浏览器不支持地理定位"));
        return;
      }
      // æ£€æŸ¥æ˜¯å¦ä½¿ç”¨HTTPS
      const isSecureContext =
        window.isSecureContext || window.location.protocol === "https:";
      console.log(
        "当前协议:",
        window.location.protocol,
        "是否安全上下文:",
        isSecureContext
      );
      if (!isSecureContext) {
        console.warn("当前不是HTTPS协议,地理位置API可能受限");
      }
      navigator.geolocation.getCurrentPosition(
        position => {
          const { longitude, latitude } = position.coords;
          console.log("获取位置成功:", longitude, latitude);
          resolve({ longitude, latitude });
        },
        error => {
          console.log("获取位置失败:", error);
          let errorMessage = "获取位置失败";
          // æ ¹æ®é”™è¯¯ç±»åž‹æä¾›æ›´å…·ä½“的提示
          switch (error.code) {
            case error.PERMISSION_DENIED:
              errorMessage =
                "用户拒绝了位置权限请求,请在浏览器设置中允许位置访问";
              break;
            case error.POSITION_UNAVAILABLE:
              errorMessage = "位置信息不可用,请检查设备定位功能";
              break;
            case error.TIMEOUT:
              errorMessage = "获取位置超时,请重试";
              break;
            case error.UNKNOWN_ERROR:
              errorMessage = "获取位置时发生未知错误";
              break;
            default:
              errorMessage = `获取位置失败: ${error.message}`;
          }
          // æ£€æŸ¥æ˜¯å¦æ˜¯HTTPS问题
          if (error.code === error.PERMISSION_DENIED && !isSecureContext) {
            errorMessage += "(注意:生产环境需要使用HTTPS协议才能获取位置)";
          }
          reject(new Error(errorMessage));
        },
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 0,
        }
      );
    });
  };
  // æ‰“卡
  const handleCheckInOut = () => {
    getCurrentLocation()
      .then(location => {
        console.log("位置成功");
        createPersonalAttendanceRecord(location).then(res => {
          fetchData();
          fetchTodayData();
          ElMessage.success("打卡成功!");
        });
      })
      .catch(error => {
        // èŽ·å–ä½ç½®å¤±è´¥æ—¶ï¼Œä»å…è®¸æ‰“å¡
        ElMessage.warning("获取位置失败,将使用默认位置打卡");
        createPersonalAttendanceRecord({}).then(res => {
          fetchData();
          fetchTodayData();
          ElMessage.success("打卡成功!");
        });
      });
  };
  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}`;
    fetchData();
    fetchTodayData();
    fetchDeptOptions();
  });
  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;
  }
  .attendance-operation {
    display: flex;
    justify-content: space-between;
  }
</style>
src/views/personnelManagement/contractManagement/components/formDia.vue
@@ -11,7 +11,6 @@
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :showPagination="false"
          height="600"
      ></PIMTable>
      <template #footer>
@@ -20,22 +19,24 @@
        </div>
      </template>
    </el-dialog>
    <Files ref="filesDia"></Files>
  </div>
</template>
<script setup>
import {ref} from "vue";
import {staffOnJobInfo} from "@/api/personnelManagement/employeeRecord.js";
import {findStaffContractListPage} from "@/api/personnelManagement/staffContract.js";
const Files = defineAsyncComponent(() => import( "@/views/personnelManagement/contractManagement/filesDia.vue"));
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const filesDia = ref()
const dialogFormVisible = ref(false);
const operationType = ref('')
const tableColumn = ref([
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  {
    label: "合同年限",
    prop: "contractTerm",
  },
  {
    label: "合同开始日期",
    prop: "contractStartTime",
@@ -43,6 +44,22 @@
  {
    label: "合同结束日期",
    prop: "contractEndTime",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    operation: [
      {
        name: "上传附件",
        type: "text",
        clickFun: (row) => {
          filesDia.value.openDialog( row,'合同')
        },
      }
    ],
  },
]);
const tableData = ref([]);
@@ -53,12 +70,17 @@
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    staffOnJobInfo({staffNo: row.staffNo}).then(res => {
      tableData.value = res.data
    findStaffContractListPage({staffOnJobId: row.id}).then(res => {
      tableData.value = res.data.records
    })
  }
}
const openUploadFile = (row) => {
  filesDia.value.open = true
  filesDia.value.row = row
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
src/views/personnelManagement/contractManagement/filesDia.vue
@@ -28,18 +28,13 @@
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          :page="page"
          @selection-change="handleSelectionChange"
          height="500"
          @pagination="paginationSearch"
          :total="page.total"
      >
      </PIMTable>
            <pagination
                style="margin: 10px 0"
                v-show="total > 0"
                @pagination="paginationSearch"
                :total="total"
                :page="page.current"
                :limit="page.size"
            />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
@@ -124,7 +119,7 @@
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        total.value = res.data.total;
    page.total = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
src/views/personnelManagement/contractManagement/index.vue
@@ -70,11 +70,11 @@
</template>
<script setup>
import { Search, UploadFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref } from "vue";
import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
import { ElMessageBox } from "element-plus";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import dayjs from "dayjs";
import { getToken } from "@/utils/auth.js";
import FilesDia from "./filesDia.vue";
@@ -183,7 +183,7 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 130,
    width: 120,
    operation: [
      {
        name: "详情",
@@ -191,14 +191,7 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row);
        },
      },
      }
    ],
  },
]);
@@ -245,6 +238,7 @@
  tableLoading.value = true;
  const params = { ...searchForm.value, ...page };
  params.entryDate = undefined
  params.staffState = 1
  staffOnJobListPage(params).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
@@ -272,7 +266,7 @@
    type: "warning",
  })
    .then(() => {
      proxy.download("/staff/staffOnJob/export", {}, "合同管理.xlsx");
      proxy.download("/staff/staffOnJob/export", {staffState: 1}, "合同管理.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
src/views/personnelManagement/dimission/components/formDia.vue
@@ -9,145 +9,143 @@
      <!-- å‘˜å·¥ä¿¡æ¯å±•示区域 -->
      <div class="info-section">
        <div class="info-title">员工信息</div>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">姓名:</span>
              <el-select v-model="form.staffName" placeholder="请选择人员" style="width: 100%" @change="handleSelect" filterable>
                <el-option
                  v-for="item in personList"
                  :key="item.id"
                  :label="item.staffName"
                  :value="item.staffName"
        <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px">
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="姓名:" prop="staffOnJobId">
                <el-select v-model="form.staffOnJobId"
                           placeholder="请选择人员"
                           style="width: 100%"
                           :disabled="operationType === 'edit'"
                           @change="handleSelect">
                  <el-option
                      v-for="item in personList"
                      :key="item.id"
                      :label="item.staffName"
                      :value="item.id"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="员工编号:">
                {{ currentStaffRecord.staffNo || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="性别:">
                {{ currentStaffRecord.sex || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="户籍住址:">
                {{ currentStaffRecord.nativePlace || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="岗位:">
                {{ currentStaffRecord.postName || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="现住址:">
                {{ currentStaffRecord.adress || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="第一学历:">
                {{ currentStaffRecord.firstStudy || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="专业:">
                {{ currentStaffRecord.profession || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="年龄:">
                {{ currentStaffRecord.age || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="联系电话:">
                {{ currentStaffRecord.phone || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="紧急联系人:">
                {{ currentStaffRecord.emergencyContact || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="紧急联系人联系电话:">
                {{ currentStaffRecord.emergencyContactPhone || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="离职原因:" prop="reason">
                <el-select v-model="form.reason" placeholder="请选择离职原因" style="width: 100%" @change="handleSelectDimissionReason">
                  <el-option
                      v-for="(item, index) in dimissionReasonOptions"
                      :key="index"
                      :label="item.label"
                      :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="备注:" prop="remark" v-if="form.reason === 'other'">
                <el-input
                    v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="备注"
                    maxlength="500"
                    show-word-limit
                />
              </el-select>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">员工编号:</span>
              <span class="info-value">{{ form.staffNo || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">性别:</span>
              <span class="info-value">{{ form.sex || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">户籍住址:</span>
              <span class="info-value">{{ form.nativePlace || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">岗位:</span>
              <span class="info-value">{{ form.postJob || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">现住址:</span>
              <span class="info-value">{{ form.adress || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">第一学历:</span>
              <span class="info-value">{{ form.firstStudy || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">专业:</span>
              <span class="info-value">{{ form.profession || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">年龄:</span>
              <span class="info-value">{{ form.age || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">联系电话:</span>
              <span class="info-value">{{ form.phone || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">紧急联系人:</span>
              <span class="info-value">{{ form.emergencyContact || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">紧急联系人联系电话:</span>
              <span class="info-value">{{ form.emergencyContactPhone || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">合同开始日期:</span>
              <span class="info-value">{{ form.contractStartTime || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">合同结束日期:</span>
              <span class="info-value">{{ form.contractExpireTime || '-' }}</span>
            </div>
          </el-col>
        </el-row>
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
<!--        <el-row :gutter="30">-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">离职原因:</span>-->
<!--              <el-select v-model="form.reason" placeholder="请选择人员" style="width: 100%" @change="handleSelect">-->
<!--                <el-option-->
<!--                    v-for="(item, index) in dimissionReasonOptions"-->
<!--                    :key="index"-->
<!--                    :label="item.label"-->
<!--                    :value="item.value"-->
<!--                />-->
<!--              </el-select>-->
<!--            </div>-->
<!--          </el-col>-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">员工编号:</span>-->
<!--              <span class="info-value">{{ form.staffNo || '-' }}</span>-->
<!--            </div>-->
<!--          </el-col>-->
<!--        </el-row>-->
      </div>
      <!-- ç¦»èŒä¿¡æ¯å¡«å†™åŒºåŸŸ -->
      <!-- <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef" style="margin-top: 20px">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="离职日期:" prop="dimissionDate">
              <el-date-picker
                v-model="form.dimissionDate"
                type="date"
                placeholder="请选择离职日期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                clearable
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="离职原因:" prop="dimissionReason">
              <el-input
                v-model="form.dimissionReason"
                type="textarea"
                :rows="3"
                placeholder="请输入离职原因"
                maxlength="500"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form> -->
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
@@ -160,8 +158,8 @@
<script setup>
import {ref, reactive, toRefs, getCurrentInstance} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -169,86 +167,80 @@
const operationType = ref('')
const data = reactive({
  form: {
    staffNo: "",
    staffName: "",
    sex: "",
    nativePlace: "",
    postJob: "",
    adress: "",
    firstStudy: "",
    profession: "",
    age: 0,
    phone: "",
    emergencyContact: "",
    emergencyContactPhone: "",
    contractTerm: 0,
    contractStartTime: "",
    contractExpireTime: "",
    dimissionDate: "",
    dimissionReason: "",
    staffState: "",
    staffOnJobId: undefined,
    reason: "",
    remark: "",
  },
  rules: {
    staffName: [{ required: true, message: "请选择人员", trigger: "change" }],
    dimissionDate: [{ required: true, message: "请选择离职日期", trigger: "change" }],
    dimissionReason: [{ required: true, message: "请输入离职原因", trigger: "blur" }],
    staffName: [{ required: true, message: "请选择人员" }],
    reason: [{ required: true, message: "请选择离职原因"}],
  },
  dimissionReasonOptions: [
      {label: '薪资待遇', value: 'salary'},
      {label: '职业发展', value: 'career_development'},
      {label: '工作环境', value: 'work_environment'},
      {label: '个人原因', value: 'personal_reason'},
      {label: '其他', value: 'other'},
  ],
  currentStaffRecord: {},
});
const { form, rules } = toRefs(data);
const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data);
// æ‰“开弹框
const openDialog = (type, row) => {
  getList()
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    getStaffJoinInfo(row.id).then(res => {
      form.value = {...res.data}
    })
    currentStaffRecord.value = row
    form.value.staffOnJobId = row.staffOnJobId
    form.value.reason = row.reason
    form.value.remark = row.remark
    personList.value = [
      {
        staffName: row.staffName,
        id: row.staffOnJobId,
      }
    ]
  } else {
    getList()
  }
}
const handleSelectDimissionReason = (val) => {
  if (val === 'other') {
    form.value.remark = ''
  }
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  // è¡¨å•已注释,直接提交,不进行验证
  if (!form.value.staffName) {
    proxy.$modal.msgError("请选择人员");
    return;
  }
  form.value.staffState = 0
  if (operationType.value === "add") {
    staffJoinAdd(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
    })
  } else {
    staffJoinUpdate(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
    })
  if (form.value.reason !== 'other') {
    form.value.remark = ''
  }
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      if (operationType.value === "add") {
        createStaffLeave(form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      } else {
        updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      }
    }
  })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  // è¡¨å•已注释,手动重置表单数据
  form.value = {
    staffNo: "",
    staffName: "",
    sex: "",
    nativePlace: "",
    postJob: "",
    adress: "",
    firstStudy: "",
    profession: "",
    age: 0,
    phone: "",
    emergencyContact: "",
    emergencyContactPhone: "",
    contractTerm: 0,
    contractStartTime: "",
    contractExpireTime: "",
    dimissionDate: "",
    dimissionReason: "",
    staffState: "",
    staffOnJobId: undefined,
    reason: "",
    remark: "",
  };
  dialogFormVisible.value = false;
  emit('close')
@@ -270,44 +262,11 @@
};
const handleSelect = (val) => {
  let obj = personList.value.find(item => item.staffName === val)
  let obj = personList.value.find(item => item.id === val)
  currentStaffRecord.value = {}
  if (obj) {
    let {
      sex,
      phone,
      staffNo,
      nativePlace,
      postJob,
      adress,
      firstStudy,
      profession,
      age,
      emergencyContact,
      emergencyContactPhone,
      contractTerm,
      contractStartTime,
      contractExpireTime,
      staffName
    } = obj
    // ä¿ç•™ç¦»èŒæ—¥æœŸå’Œç¦»èŒåŽŸå› ï¼Œåªæ›´æ–°å‘˜å·¥ä¿¡æ¯
    form.value = {
      ...form.value,
      sex,
      phone,
      staffNo,
      nativePlace,
      postJob,
      adress,
      firstStudy,
      profession,
      age,
      emergencyContact,
      emergencyContactPhone,
      contractTerm,
      contractStartTime,
      contractExpireTime,
      staffName
    }
    currentStaffRecord.value = obj
  }
}
defineExpose({
src/views/personnelManagement/dimission/index.vue
@@ -11,22 +11,6 @@
            clearable
            :prefix-icon="Search"
        />
        <span style="margin-left: 10px;"  class="search_title">合同开始日期:</span>
        <el-date-picker
            v-model="searchForm.entryDateStart"
            type="date"
            placeholder="请选择合同开始日期"
            size="default"
            @change="(date) => handleDateChange(date,1)"
        />
        <span style="margin-left: 10px;" class="search_title">合同结束日期:</span>
        <el-date-picker
            v-model="searchForm.entryDateEnd"
            type="date"
            placeholder="请选择合同结束日期"
            size="default"
            @change="(date) => handleDateChange(date,2)"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button
        >
@@ -58,9 +42,8 @@
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue";
import {staffJoinDel, staffJoinListPage} from "@/api/personnelManagement/onboarding.js";
import {findStaffLeaveListPage, batchDeleteStaffLeaves} from "@/api/personnelManagement/staffLeave.js";
import {ElMessageBox} from "element-plus";
import dayjs from "dayjs";
const data = reactive({
  searchForm: {
@@ -109,8 +92,12 @@
    prop: "nativePlace",
  },
  {
    label: "部门",
    prop: "deptName",
  },
  {
    label: "岗位",
    prop: "postJob",
    prop: "postName",
  },
  {
    label: "现住址",
@@ -145,24 +132,11 @@
    prop: "emergencyContactPhone",
    width:150
  },
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  {
    label: "合同开始日期",
    prop: "contractStartTime",
    width: 120
  },
  {
    label: "合同结束日期",
    prop: "contractEndTime",
    width: 120
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "编辑",
@@ -186,20 +160,6 @@
const { proxy } = getCurrentInstance()
const handleDateChange = (value,type) => {
  searchForm.value.entryDateEnd = null
  searchForm.value.entryDateStart = null
  if(type === 1){
    if (value) {
      searchForm.value.entryDateStart = dayjs(value).format("YYYY-MM-DD");
    }
  }else{
    if (value) {
      searchForm.value.entryDateEnd = dayjs(value).format("YYYY-MM-DD");
    }
  }
  getList();
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
@@ -213,7 +173,7 @@
};
const getList = () => {
  tableLoading.value = true;
  staffJoinListPage({...page, ...searchForm.value, staffState: 0}).then(res => {
  findStaffLeaveListPage({...page, ...searchForm.value}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
@@ -248,7 +208,7 @@
    type: "warning",
  })
      .then(() => {
        staffJoinDel(ids).then((res) => {
        batchDeleteStaffLeaves(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
@@ -265,7 +225,7 @@
    type: "warning",
  })
      .then(() => {
        proxy.download("/staff/staffJoinLeaveRecord/export", {staffState: 0}, "人员离职.xlsx");
        proxy.download("/staff/staffLeave/export", {}, "人员离职.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,373 @@
<template>
  <div>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增入职' : '编辑人员'"
               width="70%"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="员工编号:"
                          prop="staffNo">
              <el-input v-model="form.staffNo"
                        placeholder="请输入"
                        clearable
                        :disabled="operationType !== 'add'" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="姓名:"
                          prop="staffName">
              <el-input v-model="form.staffName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="性别:"
                          prop="sex">
              <el-select v-model="form.sex">
                <el-option label="男"
                           value="男" />
                <el-option label="女"
                           value="女" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="户籍住址:"
                          prop="nativePlace">
              <el-input v-model="form.nativePlace"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="岗位:"
                          prop="sysPostId">
              <el-select v-model="form.sysPostId"
                         placeholder="请选择岗位"
                         clearable>
                <el-option v-for="item in postOptions"
                           :key="item.postId"
                           :label="item.postName"
                           :value="item.postId"
                           :disabled="item.status === '1'" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="现住址:"
                          prop="adress">
              <el-input v-model="form.adress"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="部门:"
                          prop="sysDeptId">
              <el-tree-select v-model="form.sysDeptId"
                              :data="deptOptions"
                              :props="{ value: 'id', label: 'label', children: 'children' }"
                              value-key="id"
                              placeholder="请选择部门"
                              check-strictly />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="年龄:"
                          prop="age">
              <el-input-number v-model="form.age"
                               :precision="0"
                               :step="1"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="第一学历:"
                          prop="firstStudy">
              <el-input v-model="form.firstStudy"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="专业:"
                          prop="profession">
              <el-input v-model="form.profession"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="联系电话:"
                          prop="phone">
              <el-input v-model="form.phone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="紧急联系人:"
                          prop="emergencyContact">
              <el-input v-model="form.emergencyContact"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="紧急联系人联系电话:"
                          prop="emergencyContactPhone">
              <el-input v-model="form.emergencyContactPhone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="合同年限:"
                          prop="contractTerm">
              <el-input-number v-model="form.contractTerm"
                               :precision="0"
                               :step="1"
                               style="width: 100%"
                               :disabled="true" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同开始日期:"
                          prop="contractStartTime">
              <el-date-picker v-model="form.contractStartTime"
                              type="date"
                              placeholder="请选择日期"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%"
                              @change="calculateContractTerm" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="合同结束日期:"
                          prop="contractEndTime">
              <el-date-picker v-model="form.contractEndTime"
                              type="date"
                              placeholder="请选择日期"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%"
                              @change="calculateContractTerm" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import { findPostOptions } from "@/api/system/post.js";
  import { listDept } from "@/api/system/dept.js";
  import {
    staffOnJobInfo,
    createStaffOnJob,
    updateStaffOnJob,
  } from "@/api/personnelManagement/staffOnJob.js";
  import { deptTreeSelect } from "@/api/system/user.js";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const id = ref(0);
  const data = reactive({
    form: {
      staffNo: "",
      staffName: "",
      sex: "",
      nativePlace: "",
      postJob: "",
      adress: "",
      firstStudy: "",
      profession: "",
      age: 0,
      phone: "",
      emergencyContact: "",
      emergencyContactPhone: "",
      contractTerm: 0,
      contractStartTime: "",
      contractEndTime: "",
      sysPostId: undefined,
      sysDeptId: undefined,
    },
    rules: {
      staffNo: [{ required: true, message: "请输入", trigger: "blur" }],
      staffName: [{ required: true, message: "请输入", trigger: "blur" }],
      sex: [{ required: true, message: "请输入", trigger: "blur" }],
      nativePlace: [{ required: true, message: "请输入", trigger: "blur" }],
      postJob: [{ required: true, message: "请输入", trigger: "blur" }],
      adress: [{ required: true, message: "请输入", trigger: "blur" }],
      firstStudy: [{ required: true, message: "请输入", trigger: "blur" }],
      profession: [{ required: true, message: "请输入", trigger: "blur" }],
      age: [{ required: true, message: "请输入", trigger: "blur" }],
      phone: [{ required: true, message: "请输入", trigger: "blur" }],
      emergencyContact: [{ required: true, message: "请输入", trigger: "blur" }],
      emergencyContactPhone: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      contractTerm: [{ required: true, message: "请输入", trigger: "blur" }],
      contractStartTime: [{ required: true, message: "请输入", trigger: "blur" }],
      contractEndTime: [{ required: true, message: "请输入", trigger: "blur" }],
      sysDeptId: [{ required: true, message: "请选择", trigger: "change" }],
    },
    postOptions: [], // å²—位选项
    deptOptions: [], // éƒ¨é—¨é€‰é¡¹
  });
  const { form, rules, postOptions, deptOptions } = toRefs(data);
  // æ‰“开弹框
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    if (operationType.value === "edit") {
      id.value = row.id;
      staffOnJobInfo(id.value, {}).then(res => {
        form.value = { ...res.data };
        if (form.value.sysPostId === 0) {
          form.value.sysPostId = undefined;
        }
        if (form.value.sysDeptId === 0) {
          form.value.sysDeptId = undefined;
        }
        // ç¼–辑时也计算一次合同年限
        calculateContractTerm();
      });
    } else {
      form.value.id = "";
    }
  };
  onMounted(() => {
    fetchPostOptions();
    fetchDeptOptions();
  });
  const fetchPostOptions = () => {
    findPostOptions().then(res => {
      postOptions.value = res.data;
    });
  };
  // æŸ¥è¯¢éƒ¨é—¨åˆ—表
  const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
      deptOptions.value = filterDisabledDept(
        JSON.parse(JSON.stringify(response.data))
      );
    });
  };
  /** è¿‡æ»¤ç¦ç”¨çš„部门 */
  function filterDisabledDept(deptList) {
    return deptList.filter(dept => {
      if (dept.disabled) {
        return false;
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      }
      return true;
    });
  }
  // æäº¤äº§å“è¡¨å•
  const submitForm = () => {
    if (!form.value.sysPostId) {
      form.value.sysPostId = undefined;
    }
    if (!form.value.sysDeptId) {
      form.value.sysDeptId = undefined;
    }
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add") {
          createStaffOnJob(form.value).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        } else {
          updateStaffOnJob(id.value, form.value).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        }
      }
    });
  };
  // è®¡ç®—合同年限
  const calculateContractTerm = () => {
    if (form.value.contractStartTime && form.value.contractEndTime) {
      const startDate = new Date(form.value.contractStartTime);
      const endDate = new Date(form.value.contractEndTime);
      if (endDate > startDate) {
        // è®¡ç®—年份差
        const yearDiff = endDate.getFullYear() - startDate.getFullYear();
        const monthDiff = endDate.getMonth() - startDate.getMonth();
        const dayDiff = endDate.getDate() - startDate.getDate();
        let years = yearDiff;
        // å¦‚果结束日期的月日小于开始日期的月日,则减去1å¹´
        if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
          years = yearDiff - 1;
        }
        form.value.contractTerm = Math.max(0, years);
      } else {
        form.value.contractTerm = 0;
      }
    } else {
      form.value.contractTerm = 0;
    }
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
</style>
src/views/personnelManagement/employeeRecord/components/RenewContract.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<template>
  <el-dialog
      v-model="isShow"
      title="续签合同"
      width="800px"
      @close="closeModal"
  >
    <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
      <el-form-item label="合同开始日期:" prop="contractStartTime">
        <el-date-picker
            v-model="form.contractStartTime"
            type="date"
            placeholder="请选择日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            clearable
            style="width: 100%"
            @change="calculateContractTerm"
        />
      </el-form-item>
      <el-form-item label="合同结束日期:" prop="contractEndTime">
        <el-date-picker
            v-model="form.contractEndTime"
            type="date"
            placeholder="请选择日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            clearable
            style="width: 100%"
            @change="calculateContractTerm"
        />
      </el-form-item>
      <el-form-item label="合同年限:" prop="contractTerm">
        <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="submitForm">确认</el-button>
        <el-button @click="closeModal">取消</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
// ç»­ç­¾åˆåŒ
import { renewContract } from "@/api/personnelManagement/staffOnJob.js";
import {computed, getCurrentInstance,} from "vue";
const emit = defineEmits(['update:visible', 'completed']);
const data = reactive({
  form: {
    contractTerm: 0,
    contractStartTime: "",
    contractEndTime: "",
  },
  rules: {
    contractTerm: [{ required: true, message: "请输入", trigger: "blur" }],
    contractStartTime: [{ required: true, message: "请输入", trigger: "blur" }],
    contractEndTime: [{ required: true, message: "请输入", trigger: "blur" }],
  }
});
const { form, rules } = toRefs(data);
let { proxy } = getCurrentInstance()
const props = defineProps({
  id: {
    type: Number,
    default: 0,
  },
  visible: {
    type: Boolean,
    required: true,
  },
})
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
// è®¡ç®—合同年限
const calculateContractTerm = () => {
  if (form.value.contractStartTime && form.value.contractEndTime) {
    const startDate = new Date(form.value.contractStartTime);
    const endDate = new Date(form.value.contractEndTime);
    if (endDate > startDate) {
      // è®¡ç®—年份差
      const yearDiff = endDate.getFullYear() - startDate.getFullYear();
      const monthDiff = endDate.getMonth() - startDate.getMonth();
      const dayDiff = endDate.getDate() - startDate.getDate();
      let years = yearDiff;
      // å¦‚果结束日期的月日小于开始日期的月日,则减去1å¹´
      if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
        years = yearDiff - 1;
      }
      form.value.contractTerm = Math.max(0, years);
    } else {
      form.value.contractTerm = 0;
    }
  } else {
    form.value.contractTerm = 0;
  }
};
const submitForm = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      renewContract(props.id, form.value).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("续签合同成功");
          emit('completed');
          closeModal();
        }
      })
    }
  })
}
// å…³é—­å¼¹æ¡†
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  form.value = {
    contractTerm: 0,
    contractStartTime: "",
    contractEndTime: "",
  };
  isShow.value = false;
};
</script>
src/views/personnelManagement/employeeRecord/components/Show.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="详情"
        width="70%"
        @close="closeDia"
    >
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          height="600"
      ></PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref} from "vue";
import {staffOnJobInfo} from "@/api/personnelManagement/staffOnJob.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const tableColumn = ref([
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  {
    label: "合同开始日期",
    prop: "contractStartTime",
  },
  {
    label: "合同结束日期",
    prop: "contractEndTime",
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    staffOnJobInfo({staffNo: row.staffNo}).then(res => {
      tableData.value = res.data
    })
  }
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/personnelManagement/employeeRecord/components/formDia.vue
ÎļþÒÑɾ³ý
src/views/personnelManagement/employeeRecord/index.vue
@@ -19,9 +19,9 @@
        >
      </div>
      <div>
<!--        <el-button type="primary" @click="openForm('add')">新增入职</el-button>-->
        <el-button type="primary" @click="openFormNewOrEditFormDia('add')">新增入职</el-button>
        <el-button @click="handleOut">导出</el-button>
<!--        <el-button type="danger" plain @click="handleDelete">删除</el-button>-->
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
@@ -37,17 +37,26 @@
          :total="page.total"
      ></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
    <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
    <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import FormDia from "@/views/personnelManagement/employeeRecord/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {staffOnJobListPage} from "@/api/personnelManagement/employeeRecord.js";
import {batchDeleteStaffOnJobs, staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
import dayjs from "dayjs";
const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
const data = reactive({
  searchForm: {
@@ -58,6 +67,8 @@
  },
});
const { searchForm } = toRefs(data);
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableColumn = ref([
  {
    label: "状态",
@@ -97,6 +108,10 @@
  {
    label: "户籍住址",
    prop: "nativePlace",
  },
  {
    label: "部门",
    prop: "deptName",
  },
  {
    label: "岗位",
@@ -154,14 +169,31 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 180,
    operation: [
      {
        name: "详情",
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
          openFormNewOrEditFormDia("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      },
      // {
      //   name: "详情",
      //   type: "text",
      //   clickFun: (row) => {
      //     openForm("edit", row);
      //   },
      // },
    ],
  },
]);
@@ -174,6 +206,7 @@
  total: 0
});
const formDia = ref()
const formDiaNewOrEditFormDia = ref()
const { proxy } = getCurrentInstance()
const changeDaterange = (value) => {
@@ -200,7 +233,7 @@
  tableLoading.value = true;
  const params = { ...searchForm.value, ...page };
  params.entryDate = undefined
  staffOnJobListPage({...params, staffState: 1}).then(res => {
  staffOnJobListPage({...params}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
@@ -219,6 +252,37 @@
    formDia.value?.openDialog(type, row)
  })
};
const openFormNewOrEditFormDia = (type, row) => {
  nextTick(() => {
    formDiaNewOrEditFormDia.value?.openDialog(type, row)
  })
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        batchDeleteStaffOnJobs(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
@@ -227,7 +291,7 @@
    type: "warning",
  })
      .then(() => {
        proxy.download("/staff/staffOnJob/export", {staffState: 1}, "在职员工台账.xlsx");
        proxy.download("/staff/staffOnJob/export", {staffState: 1}, "员工台账.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
src/views/personnelManagement/monthlyStatistics/components/formDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,318 @@
<template>
  <el-dialog v-model="dialogVisible"
             :title="title"
             width="700px"
             :close-on-click-modal="false">
    <el-form ref="formRef"
             :model="form"
             :rules="rules"
             label-width="140px"
             label-position="top">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="统计月份"
                        prop="payDate">
            <el-date-picker v-model="form.payDate"
                            type="month"
                            value-format="YYYY-MM"
                            format="YYYY-MM"
                            placeholder="请选择月份"
                            style="width: 100%"
                            :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="姓名"
                        prop="staffId">
            <el-select v-model="form.staffId"
                       placeholder="请选择员工"
                       style="width: 100%"
                       :disabled="operationType === 'view'">
              <el-option v-for="item in userList"
                         :key="item.id"
                         :label="item.staffName"
                         :value="item.id" />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="基本工资"
                        prop="basicSalary">
            <el-input v-model="form.basicSalary"
                      type="number"
                      placeholder="请输入基本工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="计件工资"
                        prop="pieceworkSalary">
            <el-input v-model="form.pieceworkSalary"
                      type="number"
                      placeholder="请输入计件工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="计时工资"
                        prop="hourlySalary">
            <el-input v-model="form.hourlySalary"
                      type="number"
                      placeholder="请输入计时工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="其他收入"
                        prop="otherIncome">
            <el-input v-model="form.otherIncome"
                      type="number"
                      placeholder="请输入其他收入"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="社保个人"
                        prop="socialSecurityIndividuals">
            <el-input v-model="form.socialSecurityIndividuals"
                      type="number"
                      placeholder="请输入社保个人"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="公积金个人"
                        prop="providentFundIndividuals">
            <el-input v-model="form.providentFundIndividuals"
                      type="number"
                      placeholder="请输入公积金个人"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="个人所得税"
                        prop="personalIncomeTax">
            <el-input v-model="form.personalIncomeTax"
                      type="number"
                      placeholder="请输入个人所得税"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="其他扣款"
                        prop="otherDeductions">
            <el-input v-model="form.otherDeductions"
                      type="number"
                      placeholder="请输入其他扣款"
                      :disabled="operationType === 'view'" />
          </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="form.remark"
                      type="textarea"
                      placeholder="请输入备注"
                      :rows="3"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="submitForm"
                   v-if="operationType !== 'view'">
          ç¡®å®š
        </el-button>
      </span>
    </template>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, computed, onMounted } from "vue";
  import { ElMessage } from "element-plus";
  import {
    monthlyStatisticsAdd,
    monthlyStatisticsUpdate,
    staffOnJobList,
  } from "@/api/personnelManagement/monthlyStatistics.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    operationType: {
      type: String,
      default: "add",
    },
    row: {
      type: Object,
      default: () => ({}),
    },
  });
  const emit = defineEmits(["update:modelValue", "close"]);
  const dialogVisible = computed({
    get: () => props.modelValue,
    set: val => emit("update:modelValue", val),
  });
  const title = computed(() => {
    if (props.operationType === "add") return "新增薪资台账";
    if (props.operationType === "edit") return "编辑薪资台账";
    return "查看薪资台账";
  });
  const formRef = ref();
  const form = reactive({
    id: "",
    payDate: "",
    staffId: "",
    basicSalary: 0,
    pieceworkSalary: 0,
    hourlySalary: 0,
    otherIncome: 0,
    socialSecurityIndividuals: 0,
    providentFundIndividuals: 0,
    personalIncomeTax: 0,
    otherDeductions: 0,
    payableWages: 0,
    deductibleWages: 0,
    actualWages: 0,
    remark: "",
  });
  const rules = {
    payDate: [{ required: true, message: "请选择统计月份", trigger: "change" }],
    staffId: [{ required: true, message: "请选择员工", trigger: "change" }],
    basicSalary: [{ required: true, message: "请输入基本工资", trigger: "blur" }],
  };
  const userList = ref([]);
  const loadUserList = () => {
    // userListNoPage().then(res => {
    //   userList.value = res.data || [];
    // });
    staffOnJobList().then(res => {
      userList.value = res.data || [];
    });
  };
  const openDialog = (type, row) => {
    // é‡ç½®è¡¨å•
    Object.assign(form, {
      id: "",
      payDate: "",
      staffId: "",
      basicSalary: 0,
      pieceworkSalary: 0,
      hourlySalary: 0,
      otherIncome: 0,
      socialSecurityIndividuals: 0,
      providentFundIndividuals: 0,
      personalIncomeTax: 0,
      otherDeductions: 0,
      payableWages: 0,
      deductibleWages: 0,
      actualWages: 0,
      remark: "",
    });
    if (type === "add") {
      dialogVisible.value = true;
    } else if (type === "edit" || type === "view") {
      if (row && row.id) {
        Object.assign(form, row);
        dialogVisible.value = true;
      }
    }
  };
  const submitForm = () => {
    formRef.value.validate(valid => {
      if (valid) {
        form.basicSalary = Number(form.basicSalary);
        form.pieceworkSalary = Number(form.pieceworkSalary);
        form.hourlySalary = Number(form.hourlySalary);
        form.otherIncome = Number(form.otherIncome);
        form.socialSecurityIndividuals = Number(form.socialSecurityIndividuals);
        form.providentFundIndividuals = Number(form.providentFundIndividuals);
        form.personalIncomeTax = Number(form.personalIncomeTax);
        form.otherDeductions = Number(form.otherDeductions);
        // è®¡ç®—应发工资、应扣工资和实发工资
        const payableWages =
          form.basicSalary +
          form.pieceworkSalary +
          form.hourlySalary +
          form.otherIncome;
        const deductibleWages =
          form.socialSecurityIndividuals +
          form.providentFundIndividuals +
          form.personalIncomeTax +
          form.otherDeductions;
        const actualWages = payableWages - deductibleWages;
        const submitData = {
          ...form,
          payableWages,
          deductibleWages,
          actualWages,
        };
        if (props.operationType === "add") {
          monthlyStatisticsAdd(submitData).then(res => {
            if (res.code === 200) {
              ElMessage.success("新增成功");
              dialogVisible.value = false;
              emit("close");
            } else {
              ElMessage.error(res.msg || "新增失败");
            }
          });
        } else if (props.operationType === "edit") {
          monthlyStatisticsUpdate(submitData).then(res => {
            if (res.code === 200) {
              ElMessage.success("更新成功");
              dialogVisible.value = false;
              emit("close");
            } else {
              ElMessage.error(res.msg || "更新失败");
            }
          });
        }
      }
    });
  };
  onMounted(() => {
    loadUserList();
  });
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
  .dialog-footer {
    text-align: right;
  }
</style>
src/views/personnelManagement/monthlyStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,303 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">姓名:</span>
        <el-input v-model="searchForm.staffName"
                  style="width: 240px"
                  placeholder="请输入姓名搜索"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title ml10">月份:</span>
        <el-date-picker v-model="searchForm.payDateStr"
                        type="month"
                        @change="handleQuery"
                        value-format="YYYY-MM"
                        format="YYYY-MM"
                        placeholder="请选择月份"
                        style="width: 240px"
                        clearable />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button @click="handleExport"
                   style="margin-right: 10px">导出</el-button>
        <el-button type="primary"
                   @click="openForm('add')">新增台账</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total"></PIMTable>
    </div>
    <form-dia v-model="dialogVisible"
              :operation-type="operationType"
              :row="currentRow"
              ref="formDia"
              @close="handleQuery"></form-dia>
  </div>
</template>
<script setup>
  import { Search } from "@element-plus/icons-vue";
  import {
    onMounted,
    ref,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import FormDia from "./components/formDia.vue";
  import {
    monthlyStatisticsListPage,
    monthlyStatisticsDelete,
  } from "@/api/personnelManagement/monthlyStatistics.js";
  const data = reactive({
    searchForm: {
      staffName: "",
      payDateStr: "",
    },
  });
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "员工姓名",
      prop: "staffName",
    },
    {
      label: "部门",
      prop: "deptName",
      width: 140,
    },
    {
      label: "月份",
      prop: "payDate",
    },
    {
      label: "基本工资",
      prop: "basicSalary",
    },
    {
      label: "计件工资",
      prop: "pieceworkSalary",
    },
    {
      label: "计时工资",
      prop: "hourlySalary",
    },
    {
      label: "其他收入",
      prop: "otherIncome",
    },
    {
      label: "社保个人",
      prop: "socialSecurityIndividuals",
    },
    {
      label: "公积金个人",
      prop: "providentFundIndividuals",
      width: 140,
    },
    {
      label: "工资个税",
      prop: "personalIncomeTax",
    },
    {
      label: "其他支出",
      prop: "otherDeductions",
    },
    {
      label: "应发工资",
      prop: "payableWages",
    },
    {
      label: "应扣工资",
      prop: "deductibleWages",
    },
    {
      label: "实发工资",
      prop: "actualWages",
    },
    {
      label: "备注",
      prop: "remark",
      width: 150,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 220,
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            openForm("edit", row);
          },
        },
        // {
        //   name: "查看",
        //   type: "text",
        //   clickFun: row => {
        //     openForm("view", row);
        //   },
        // },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const formDia = ref();
  const dialogVisible = ref(false);
  const operationType = ref("add");
  const currentRow = ref({});
  const { proxy } = getCurrentInstance();
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    monthlyStatisticsListPage({ ...page, ...searchForm.value })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    operationType.value = type;
    currentRow.value = row || {};
    dialogVisible.value = true;
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
  };
  // åˆ é™¤
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        monthlyStatisticsDelete(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // å¯¼å‡º
  const handleExport = () => {
    ElMessageBox.confirm("是否确认导出人员薪资台账?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download(
          "/compensationPerformance/export",
          { ...searchForm.value, ...page },
          "人员薪资台账.xlsx"
        );
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped>
  .search_form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    flex-wrap: wrap;
    gap: 10px;
  }
  .search_title {
    font-weight: 500;
    margin-right: 5px;
  }
  .ml10 {
    margin-left: 10px;
  }
  .table_list {
    margin-top: 20px;
  }
  .dialog-footer {
    text-align: right;
  }
</style>
src/views/personnelManagement/onboarding/components/formDia.vue
ÎļþÒÑɾ³ý
src/views/personnelManagement/onboarding/components/formDiaXJHT.vue
ÎļþÒÑɾ³ý
src/views/personnelManagement/onboarding/index.vue
ÎļþÒÑɾ³ý
src/views/personnelManagement/payrollManagement/components/formDia.vue
@@ -168,8 +168,8 @@
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, getStaffOnJob, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {compensationAdd, compensationUpdate} from "@/api/personnelManagement/payrollManagement.js";
import {staffOnJobInfo, staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -234,12 +234,16 @@
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
    getStaffOnJob().then(res => {
        personList.value = res.data
    })
  staffOnJobListPage({
    current: -1,
    size: -1,
    staffState: 1
  }).then(res => {
    personList.value = res.data.records || []
  })
    form.value = {}
  if (operationType.value === 'edit') {
    getStaffJoinInfo(row.id).then(res => {
    staffOnJobInfo(row.staffId).then(res => {
            form.value = {...row}
            form.value.payDate = form.value.payDate + '-01'
    })
src/views/personnelManagement/payrollManagement/index.vue
@@ -53,7 +53,6 @@
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import FormDia from "@/views/personnelManagement/payrollManagement/components/formDia.vue";
import {staffJoinDel} from "@/api/personnelManagement/onboarding.js";
import {ElMessageBox} from "element-plus";
import dayjs from "dayjs";
import {compensationDelete, compensationListPage} from "@/api/personnelManagement/payrollManagement.js";
src/views/personnelManagement/scheduling/index.vue
@@ -253,9 +253,9 @@
import {useDict} from "@/utils/dict.js"
import {Plus, Download, Search, Refresh} from '@element-plus/icons-vue'
import {save, del, delByIds, listPage} from "@/api/personnelManagement/scheduling.js"
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import dayjs from "dayjs";
import pagination from "@/components/PIMTable/Pagination.vue";
import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
const { proxy } = getCurrentInstance();
@@ -317,8 +317,12 @@
 * èŽ·å–å½“å‰åœ¨èŒäººå‘˜åˆ—è¡¨
 */
const getPersonList = () => {
  getStaffOnJob().then(res => {
    personList.value = res.data
  staffOnJobListPage({
    current: -1,
    size: -1,
    staffState: 1
  }).then(res => {
    personList.value = res.data.records || []
  })
};
const paginationChange = (obj) => {
src/views/personnelManagement/selfService/index.vue
@@ -221,8 +221,7 @@
const { proxy } = getCurrentInstance()
import { getUserProfile } from '@/api/system/user.js'
import {staffJoinUpdate, staffJoinListPage} from "@/api/personnelManagement/onboarding.js";
import { fa, id } from 'element-plus/es/locales.mjs'
import {staffOnJobListPage, updateStaffOnJob} from "@/api/personnelManagement/staffOnJob.js";
const tableLoading = ref(false)
// åˆ†é¡µå‚æ•°
@@ -572,7 +571,7 @@
      currentUser.value = res.data
      // console.log("----",currentUser.value)
        //得到人员列表
        staffJoinListPage({staffState: 1}).then(res => {
      staffOnJobListPage({staffState: 1}).then(res => {
          //筛选出和currentUser同名的人员
          // let tableData = res.data.records
          user.value = res.data.records.find(item => item.staffName === currentUser.value.userName)
@@ -604,18 +603,15 @@
    const userRes = await getUserProfile();
    if (userRes.code === 200) {
      currentUser.value = userRes.data;
      const staffListRes = await staffJoinListPage({ staffState: 1 });
      const staffListRes = await staffOnJobListPage({ staffState: 1 });
      user.value = staffListRes.data.records.find(item => item.staffName === currentUser.value.userName);
      // console.log("++++", user.value);
      Object.assign(joinForm, user.value);
      joinForm.staffName = profileForm.name;
      joinForm.phone = profileForm.phone;
      joinForm.email = profileForm.email;
      joinForm.adress = profileForm.adress; 
      console.log(joinForm)
      // è°ƒç”¨æ›´æ–°ä¸ªäººä¿¡æ¯çš„æŽ¥å£
      staffJoinUpdate(joinForm).then(res => {
      updateStaffOnJob(user.value.id, joinForm).then(res => {
        if (res.code === 200) {
          ElMessage.success('个人信息保存成功');
          getProfile();