zhangwencui
17 小时以前 1ed366885433dfdec1241312356535b868c39eee
合同管理模块开发
已添加2个文件
已修改2个文件
663 ■■■■■ 文件已修改
src/api/humanResources/contractManagement.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/humanResources/contractManagement/index.vue 630 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/humanResources/contractManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import request from '@/utils/request'
// æŸ¥è¯¢åœ¨èŒå‘˜å·¥å°è´¦
export function staffOnJobListPage(query) {
    return request({
        url: '/staff/staffOnJob/listPage',
        method: 'get',
        params: query,
    })
}
export function findStaffContractListPage(query) {
    return request({
        url: "/staff/staffContract/listPage",
        method: "get",
        params: query,
    });
}
src/pages.json
@@ -886,6 +886,13 @@
      }
    },
    {
      "path": "pages/humanResources/contractManagement/index",
      "style": {
        "navigationBarTitleText": "合同管理",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/message",
      "style": {
        "navigationBarTitleText": "消息中心"
src/pages/humanResources/contractManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,630 @@
<template>
  <view class="contract-management-page">
    <PageHeader title="合同管理"
                @back="goBack" />
    <!-- æœç´¢æ  -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入员工姓名搜索"
                    v-model="searchValue"
                    @blur="handleSearch"
                    @change="handleSearch"
                    clearable />
        </view>
        <view class="filter-button"
              @click="handleSearch">
          <u-icon name="search"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
    </view>
    <!-- åˆåŒä¿¡æ¯å¡ç‰‡ -->
    <view class="contract-card">
      <!-- å¾ªçŽ¯å±•ç¤ºç”¨æˆ·åˆ—è¡¨ -->
      <view v-for="(userItem, index) in userList"
            :key="index"
            class="user-card">
        <!-- åŸºæœ¬ä¿¡æ¯å¡ç‰‡ -->
        <view class="basic-info-card">
          <view class="card-header">
            <text class="card-title">{{ userItem.staffName || '-' }}</text>
            <text class="card-subtitle"><u-tag :type="userItem.staffState == '1' ? 'primary' : 'error'"
                     :text="userItem.staffState == '1' ? ' åœ¨èŒ ' : ' ç¦»èŒ '"></u-tag></text>
          </view>
          <view class="card-content">
            <view class="info-row">
              <view class="info-item">
                <text class="info-label">员工编号</text>
                <text class="info-value">{{ userItem.staffNo || '-' }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">性别</text>
                <text class="info-value">{{ userItem.sex || '-' }}</text>
              </view>
            </view>
            <view class="info-row">
              <view class="info-item">
                <text class="info-label">岗位</text>
                <text class="info-value">{{ userItem.postJob || '-' }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">第一学历</text>
                <text class="info-value">{{ userItem.firstStudy || '-' }}</text>
              </view>
            </view>
            <view class="info-row">
              <view class="info-item">
                <text class="info-label">专业</text>
                <text class="info-value">{{ userItem.profession || '-' }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">年龄</text>
                <text class="info-value">{{ userItem.age || '-' }}</text>
              </view>
            </view>
            <view class="info-row">
              <view class="info-item">
                <text class="info-label">联系电话</text>
                <text class="info-value">{{ userItem.phone || '-' }}</text>
              </view>
            </view>
            <!-- <view class="info-row">
              <view class="info-item">
                <text class="info-label">紧急联系人</text>
                <text class="info-value">{{ userItem.emergencyContact || '-' }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">紧急联系人电话</text>
                <text class="info-value">{{ userItem.emergencyContactPhone || '-' }}</text>
              </view>
            </view> -->
            <!-- <view class="info-row">
              <view class="info-item">
                <text class="info-label">户籍住址</text>
                <text class="info-value">{{ userItem.nativePlace || '-' }}</text>
              </view>
            </view> -->
            <view class="info-row">
              <view class="info-item">
                <text class="info-label">现住址</text>
                <text class="info-value">{{ userItem.adress || '-' }}</text>
              </view>
            </view>
          </view>
        </view>
        <!-- åˆåŒåˆ—表卡片 -->
        <view class="contract-list-card">
          <view class="card-header">
            <up-button size="small"
                       :type="userItem.contractExpanded ? 'default' : 'primary'"
                       :loading="userItem.contractLoading"
                       @click="toggleContractExpanded(index)">
              {{ userItem.contractExpanded ? '隐藏合同列表' : '合同列表' }}<u-icon v-if="userItem.contractExpanded"
                      name="arrow-down"
                      size="12"
                      style="margin-left: 4px;"
                      color="#000"></u-icon>
            </up-button>
          </view>
          <view v-if="(userItem.contractExpanded || userItem.contractLoading) && userItem.contractList.length > 0"
                class="card-content">
            <view v-if="userItem.contractLoading"
                  class="list-loading">
              <u-icon name="loading"
                      size="24"
                      color="#348fe2"></u-icon>
              <text>加载中...</text>
            </view>
            <template v-else>
              <view v-for="contract in userItem.contractList"
                    :key="contract.id"
                    class="contract-item">
                <view class="contract-item-header">
                  <text class="contract-name">{{ contract.contractName }}</text>
                  <view class="contract-status"
                        :class="getContractStatusClass(contract.status)">
                    <text>{{ contract.status }}</text>
                  </view>
                </view>
                <view class="contract-item-content">
                  <view class="contract-detail">
                    <text class="detail-label">合同年限:</text>
                    <text class="detail-value">{{ contract.contractTerm || '-' }}</text>
                  </view>
                  <view class="contract-detail">
                    <text class="detail-label">合同开始日期:</text>
                    <text class="detail-value">{{ contract.contractStartTime || '-' }}</text>
                  </view>
                  <view class="contract-detail">
                    <text class="detail-label">合同结束日期:</text>
                    <text class="detail-value">{{ contract.contractEndTime || '-' }}</text>
                  </view>
                </view>
              </view>
            </template>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="!loading && userList.length === 0"
            class="empty-state">
        <u-icon name="document"
                size="60"
                color="#999"></u-icon>
        <text class="empty-text">暂无合同信息</text>
      </view>
    </view>
    <!-- åŠ è½½çŠ¶æ€ -->
    <view v-if="loading"
          class="loading-state">
      <u-icon name="loading"
              size="40"
              color="#348fe2"></u-icon>
      <text class="loading-text">加载中...</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import useUserStore from "@/store/modules/user";
  import {
    staffOnJobListPage,
    findStaffContractListPage,
  } from "@/api/humanResources/contractManagement";
  // ç”¨æˆ·åˆåŒä¿¡æ¯åˆ—表
  const userList = ref([]);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // æœç´¢å€¼
  const searchValue = ref("");
  // æ–‡ä»¶åˆ—表
  const fileList = ref([
    {
      id: 1,
      fileName: "劳动合同.pdf",
      fileSize: "2.5MB",
      uploadDate: "2026-02-01",
    },
    {
      id: 2,
      fileName: "保密协议.pdf",
      fileSize: "1.8MB",
      uploadDate: "2026-02-01",
    },
    {
      id: 3,
      fileName: "竞业限制协议.pdf",
      fileSize: "1.2MB",
      uploadDate: "2026-02-01",
    },
  ]);
  // ç”¨æˆ·å­˜å‚¨
  const userStore = useUserStore();
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // èŽ·å–ç”¨æˆ·åˆåŒä¿¡æ¯
  const getUserContractInfo = (searchName = "") => {
    loading.value = true;
    const params = {
      current: -1,
      size: -1,
      staffName: searchName || "",
    };
    staffOnJobListPage(params)
      .then(response => {
        // ä¸ºæ¯ä¸ªç”¨æˆ·æ·»åŠ contractList和contractLoading属性
        userList.value = response.data.records.map(user => ({
          ...user,
          contractList: [],
          contractLoading: false,
          contractExpanded: false,
        }));
      })
      .catch(error => {
        console.error("获取合同信息失败:", error);
      })
      .finally(() => {
        loading.value = false;
      });
  };
  // åŠ è½½åˆåŒåˆ—è¡¨
  const loadContractList = (id, index) => {
    // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å­˜åœ¨
    console.log(userList.value[index], "userList.value[index]");
    if (!userList.value[index]) return;
    const userItem = userList.value[index];
    console.log(userItem.contractList.length, "userItem.contractList.length");
    // å¦‚果已经加载过,不再重复加载
    if (userItem.contractList.length > 0) return;
    // è®¾ç½®åŠ è½½çŠ¶æ€
    userItem.contractLoading = true;
    // è°ƒç”¨è¯¦æƒ…接口
    findStaffContractListPage({ staffOnJobId: id })
      .then(res => {
        if (res.data && res.data.records) {
          userItem.contractList = res.data.records;
        } else {
          userItem.contractList = [];
        }
      })
      .finally(() => {
        // é‡ç½®åŠ è½½çŠ¶æ€
        userItem.contractLoading = false;
      });
  };
  // èŽ·å–åˆåŒçŠ¶æ€æ ·å¼
  const getContractStatusClass = status => {
    switch (status) {
      case "生效中":
        return "status-active";
      case "已过期":
        return "status-expired";
      case "已终止":
        return "status-terminated";
      default:
        return "status-default";
    }
  };
  // åˆ‡æ¢åˆåŒè¯¦æƒ…展开/关闭状态
  const toggleContractExpanded = index => {
    if (!userList.value[index]) return;
    const userItem = userList.value[index];
    // å¦‚果还没有加载合同列表,先加载数据
    if (userItem.contractList.length === 0) {
      loadContractList(userItem.id, index);
      // åŠ è½½å®ŒæˆåŽè‡ªåŠ¨å±•å¼€
      userItem.contractExpanded = true;
    } else {
      // åˆ‡æ¢å±•å¼€/关闭状态
      userItem.contractExpanded = !userItem.contractExpanded;
    }
  };
  // å¤„理搜索
  const handleSearch = () => {
    getUserContractInfo(searchValue.value);
  };
  // é¡µé¢åŠ è½½æ—¶èŽ·å–åˆåŒä¿¡æ¯
  onMounted(() => {
    getUserContractInfo("");
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  // å…¨å±€å˜é‡
  $primary-color: #2c7be5;
  $primary-light: #4a90e2;
  $primary-lightest: #e8f0fe;
  $text-primary: #333333;
  $text-secondary: #666666;
  $text-tertiary: #999999;
  $bg-color: #f8f9fa;
  $card-bg: #ffffff;
  $border-color: #e8e8e8;
  $shadow-sm: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
  $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
  $border-radius: 16rpx;
  .contract-management-page {
    min-height: 100vh;
    background-color: $bg-color;
    padding-bottom: 40rpx;
  }
  /* åˆåŒå¡ç‰‡ */
  .contract-card {
    margin: 24rpx;
    display: flex;
    flex-direction: column;
    gap: 24rpx;
  }
  /* åŸºæœ¬ä¿¡æ¯å¡ç‰‡ */
  .basic-info-card {
    background-color: $card-bg;
    border-radius: $border-radius $border-radius 0 0;
    box-shadow: $shadow-md;
    overflow: hidden;
    transition: all 0.3s ease;
  }
  /* åˆåŒåˆ—表卡片 */
  .contract-list-card {
    background-color: $card-bg;
    border-radius: 0 0 $border-radius $border-radius;
    box-shadow: $shadow-md;
    overflow: hidden;
    border-top: 1rpx solid $border-color;
    transition: all 0.3s ease;
  }
  /* ç”¨æˆ·å¡ç‰‡ */
  .user-card {
    margin-bottom: 0;
    border-radius: $border-radius;
    overflow: hidden;
    box-shadow: $shadow-md;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
  }
  .user-card:hover {
    transform: translateY(-4rpx);
    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
  }
  /* å¡ç‰‡å¤´éƒ¨ */
  .card-header {
    padding: 24rpx;
    border-bottom: 1rpx solid $border-color;
    background-color: $primary-lightest;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .card-title {
    font-size: 16px;
    font-weight: 600;
    color: $primary-color;
  }
  .card-subtitle {
    font-size: 13px;
    color: $text-secondary;
    padding: 4rpx 12rpx;
    border-radius: 12rpx;
  }
  /* å¡ç‰‡å†…容 */
  .card-content {
    padding: 24rpx;
  }
  /* ä¿¡æ¯è¡Œ */
  .info-row {
    display: flex;
    margin-bottom: 20rpx;
    gap: 20rpx;
  }
  .info-row:last-child {
    margin-bottom: 0;
  }
  /* ä¿¡æ¯é¡¹ */
  .info-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 12rpx;
    background-color: #f9fafb;
    border-radius: 12rpx;
    border: 1rpx solid $border-color;
  }
  /* ä¿¡æ¯æ ‡ç­¾ */
  .info-label {
    font-size: 12px;
    color: $text-secondary;
    margin-bottom: 6rpx;
    font-weight: 500;
  }
  /* ä¿¡æ¯å€¼ */
  .info-value {
    font-size: 14px;
    color: $text-primary;
    font-weight: 500;
    line-height: 1.4;
  }
  /* æ–‡ä»¶è¡¨æ ¼ */
  .file-table {
    width: 100%;
  }
  /* è¡¨æ ¼å¤´éƒ¨ */
  .table-header {
    display: flex;
    padding: 12rpx 0;
    border-bottom: 1rpx solid $border-color;
    font-weight: 600;
    font-size: 13px;
    color: $text-secondary;
  }
  /* è¡¨æ ¼è¡Œ */
  .table-row {
    display: flex;
    padding: 16rpx 0;
    border-bottom: 1rpx solid $border-color;
    font-size: 13px;
    color: $text-primary;
  }
  .table-row:last-child {
    border-bottom: none;
  }
  /* è¡¨æ ¼åˆ— */
  .table-col {
    flex: 1;
    text-align: center;
  }
  /* æ–‡ä»¶åç§°åˆ— */
  .file-name {
    flex: 2;
    text-align: left;
  }
  /* è¡¨æ ¼ç©ºçŠ¶æ€ */
  .table-empty {
    padding: 40rpx 0;
    text-align: center;
    color: $text-tertiary;
    font-size: 13px;
  }
  /* åˆåŒåˆ—表 */
  .contract-list {
    padding: 16rpx;
  }
  /* åˆåŒé¡¹ */
  .contract-item {
    background-color: #f9fafb;
    border-radius: 12rpx;
    padding: 16rpx;
    border-left: 4px solid $primary-color;
  }
  .contract-item:last-child {
    margin-bottom: 0;
  }
  /* åˆåŒé¡¹å¤´éƒ¨ */
  .contract-item-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12rpx;
  }
  /* åˆåŒåç§° */
  .contract-name {
    font-size: 14px;
    font-weight: 600;
    color: $text-primary;
  }
  /* åˆåŒçŠ¶æ€ */
  .contract-status {
    padding: 4rpx 12rpx;
    border-radius: 12rpx;
    font-size: 12px;
    font-weight: 500;
  }
  /* åˆåŒçŠ¶æ€æ ·å¼ */
  .status-active {
    background-color: #e6f7ee;
    color: #389e75;
  }
  .status-expired {
    background-color: #fff5f5;
    color: #dc3545;
  }
  .status-terminated {
    background-color: #f8f9fa;
    color: #6c757d;
  }
  .status-default {
    background-color: #e9ecef;
    color: #495057;
  }
  /* åˆåŒé¡¹å†…容 */
  .contract-item-content {
    display: flex;
    flex-direction: column;
    gap: 8rpx;
  }
  /* åˆåŒè¯¦æƒ… */
  .contract-detail {
    display: flex;
    font-size: 13px;
  }
  /* è¯¦æƒ…标签 */
  .detail-label {
    color: $text-secondary;
    margin-right: 8rpx;
  }
  /* è¯¦æƒ…值 */
  .detail-value {
    color: $text-primary;
    flex: 1;
  }
  /* åˆ—表空状态 */
  .list-empty {
    padding: 40rpx 0;
    text-align: center;
    color: $text-tertiary;
    font-size: 13px;
  }
  /* åˆ—表加载状态 */
  .list-loading {
    padding: 40rpx 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12rpx;
    color: $text-secondary;
    font-size: 13px;
  }
  /* åŠ è½½çŠ¶æ€ */
  .loading-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 100rpx 0;
  }
  .loading-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 20rpx;
  }
  /* ç©ºçŠ¶æ€ */
  .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 120rpx 0;
    text-align: center;
  }
  .empty-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
  }
</style>
src/pages/index.vue
@@ -309,6 +309,10 @@
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "人员薪资",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "合同管理",
    },
  ]);
  const safetyItems = reactive([
    {
@@ -716,6 +720,11 @@
          url: "/pages/humanResources/monthlyStatistics/index",
        });
        break;
      case "合同管理":
        uni.navigateTo({
          url: "/pages/humanResources/contractManagement/index",
        });
        break;
      default:
        uni.showToast({
          title: `点击了${item.label}`,
@@ -981,6 +990,7 @@
    const originalHumanResources = [
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "打卡签到" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "人员薪资" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "合同管理" },
    ];
    const filteredHumanResources = originalHumanResources.filter(item => {
      return allowedMenuTitles.has(item.label);