2067eae94e40c990f3c7c5160a46c7f8954d04a7..c58665039ce8b7c895ed4f1000ff4cf525a92085
8 天以前 gaoluyang
1.设备保养开发联调
c58665 对比 | 目录
8 天以前 gaoluyang
1.设备报修开发联调
212cab 对比 | 目录
8 天以前 gaoluyang
1.设备报修开发联调
bd0958 对比 | 目录
8 天以前 gaoluyang
1.设备台账开发联调
470d16 对比 | 目录
8 天以前 gaoluyang
1.协同审批开发联调
69a5fc 对比 | 目录
已修改5个文件
已添加19个文件
5658 ■■■■■ 文件已修改
src/api/collaborativeApproval/approvalProcess.js 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/noticeManagement.js 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/rpaManagement.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/calibration.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/ledger.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/measurementEquipment.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/upkeep.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/approve.vue 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/contactSelect.vue 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/detail.vue 931 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 260 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/ledger/detail.vue 408 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/ledger/index.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/index.vue 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/maintain.vue 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/index.vue 438 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/maintain.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/banner/view-background.png 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalProcess.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
// ååŒå®¡æ‰¹
import request from "@/utils/request";
export function approveProcessListPage(query) {
    return request({
        url: '/approveProcess/list',
        method: 'get',
        params: query,
    })
}
export function getDept(query) {
    return request({
        url: '/approveProcess/getDept',
        method: 'get',
        params: query,
    })
}
export function approveProcessGetInfo(query) {
    return request({
        url: '/approveProcess/get',
        method: 'get',
        params: query,
    })
}
// æ–°å¢žå®¡æ‰¹æµç¨‹
export function approveProcessAdd(query) {
    return request({
        url: '/approveProcess/add',
        method: 'post',
        data: query,
    })
}
// ä¿®æ”¹å®¡æ‰¹æµç¨‹
export function approveProcessUpdate(query) {
    return request({
        url: '/approveProcess/update',
        method: 'post',
        data: query,
    })
}
// æäº¤å®¡æ‰¹
export function updateApproveNode(query) {
    return request({
        url: '/approveNode/updateApproveNode',
        method: 'post',
        data: query,
    })
}
// åˆ é™¤å®¡æ‰¹æµç¨‹
export function approveProcessDelete(query) {
    return request({
        url: '/approveProcess/deleteIds',
        method: 'delete',
        data: query,
    })
}
// æŸ¥è¯¢å®¡æ‰¹æµç¨‹
export function approveProcessDetails(query) {
    return request({
        url: '/approveNode/details/' + query,
        method: 'get',
    })
}
src/api/collaborativeApproval/noticeManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
import request from '@/utils/request'
// æŸ¥è¯¢å…¬å‘Šåˆ—表
export function listNotice(query) {
  return request({
    url: '/collaborativeApproval/notice/list',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢å…¬å‘Šè¯¦ç»†
export function getNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'get'
  })
}
// æ–°å¢žå…¬å‘Š
export function addNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹å…¬å‘Š
export function updateNotice(data) {
  return request({
    url: '/collaborativeApproval/notice',
    method: 'put',
    data: data
  })
}
// åˆ é™¤å…¬å‘Š
export function delNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/' + noticeId,
    method: 'delete'
  })
}
// æ‰¹é‡åˆ é™¤å…¬å‘Š
export function delNoticeBatch(noticeIds) {
  return request({
    url: '/collaborativeApproval/notice/batch',
    method: 'delete',
    data: noticeIds
  })
}
// å‘布公告
export function publishNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/publish/' + noticeId,
    method: 'put'
  })
}
// ä¸‹çº¿å…¬å‘Š
export function offlineNotice(noticeId) {
  return request({
    url: '/collaborativeApproval/notice/offline/' + noticeId,
    method: 'put'
  })
}
src/api/collaborativeApproval/rpaManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import request from "@/utils/request";
// æŸ¥è¯¢RPA列表
export function listRpa(query) {
  return request({
    url: "/collaborativeApproval/rpa/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢RPA详细
export function getRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "get",
  });
}
// æ–°å¢žRPA
export function addRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹RPA
export function updateRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "put",
    data: data,
  });
}
// åˆ é™¤RPA
export function delRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "delete",
  });
}
// æ‰¹é‡åˆ é™¤RPA
export function delRpaBatch(rpaIds) {
  return request({
    url: "/collaborativeApproval/rpa/batch",
    method: "delete",
    data: rpaIds,
  });
}
// å¯åЍRPA
export function startRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/start/" + rpaId,
    method: "post",
  });
}
// åœæ­¢RPA
export function stopRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/stop/" + rpaId,
    method: "post",
  });
}
// èŽ·å–RPA状态
export function getRpaStatus(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/status/" + rpaId,
    method: "get",
  });
}
src/api/equipmentManagement/calibration.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
// æ£€å®šæ ¡å‡†è®°å½•
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function ledgerRecordListPage(query) {
  return request({
    url: "/measuringInstrumentLedgerRecord/listPage",
    method: "get",
    params: query,
  });
}
// æ ¡å‡†
export function ledgerRecordVerifying(query) {
  return request({
    url: "/measuringInstrumentLedger/verifying",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹æ ¡å‡†
export function ledgerRecordUpdate(query) {
  return request({
    url: "/measuringInstrumentLedgerRecord/update",
    method: "post",
    data: query,
  });
}
src/api/equipmentManagement/ledger.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
import request from "@/utils/request";
export const getLedgerPage = (params) => {
  return request({
    url: "/device/ledger/page",
    method: "get",
    params,
  });
};
export const getLedgerById = (id) => {
  return request({
    url: `/device/ledger/${id}`,
    method: "get",
  });
};
export const addLedger = (data) => {
  return request({
    url: "/device/ledger",
    method: "post",
    data,
  });
};
export const editLedger = (data) => {
  return request({
    url: "/device/ledger",
    method: "put",
    data,
  });
};
export const delLedger = (id) => {
  return request({
    url: `/device/ledger/${id}`,
    method: "delete",
  });
};
export const getDeviceLedger = () => {
  return request({
    url: "/device/ledger/getDeviceLedger",
    method: "get",
  });
};
src/api/equipmentManagement/measurementEquipment.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
// è®¡é‡å™¨å…·å°è´¦
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function measuringInstrumentListPage(query) {
  return request({
    url: "/measuringInstrumentLedger/listPage",
    method: "get",
    params: query,
  });
}
// åˆ é™¤
export function measuringInstrumentDelete(query) {
  return request({
    url: "/measuringInstrumentLedger/delete",
    method: "delete",
    data: query,
  });
}
// æ–°å¢ž
export function measuringInstrumentAdd(query) {
  return request({
    url: "/measuringInstrumentLedger/add",
    method: "post",
    data: query,
  });
}
// ä¿®æ”¹
export function measuringInstrumentUpdate(query) {
  return request({
    url: "/measuringInstrumentLedger/update",
    method: "post",
    data: query,
  });
}
src/api/equipmentManagement/repair.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import request from "@/utils/request";
/**
 * @desc è®¾å¤‡æŠ¥ä¿®åˆ—表
 * @param {分页查询} params
 * @returns
 */
export const getRepairPage = (params) => {
  return request({
    url: "/device/repair/page",
    method: "get",
    params,
  });
};
/**
 * @desc æ–°å¢žæŠ¥ä¿®
 * @param {报修参数} data
 * @returns
 */
export const addRepair = (data) => {
  return request({
    url: "/device/repair",
    method: "post",
    data,
  });
};
/**
 * @desc ç¼–辑报修
 * @param {报修参数} data
 * @returns
 */
export const editRepair = (data) => {
  return request({
    url: "/device/repair",
    method: "put",
    data,
  });
};
/**
 * @desc æ ¹æ®id查询一条报修
 * @param {报修id} id
 * @returns
 */
export const getRepairById = (id) => {
  return request({
    url: `/device/repair/${id}`,
    method: "get",
  });
};
/**
 * @desc åˆ é™¤æŠ¥ä¿®
 * @param {编号} ids
 * @returns
 */
export const delRepair = (ids) => {
  return request({
    url: `/device/repair/${ids}`,
    method: "delete",
  });
};
export const addMaintain = (data) => {
  return request({
    url: `/device/repair/repair`,
    method: "post",
    data,
  });
};
src/api/equipmentManagement/upkeep.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import request from "@/utils/request";
/**
 * @desc è®¾å¤‡ä¿å…»åˆ—表分页查询
 * @param {分页查询入参} params
 * @returns
 */
export const getUpkeepPage = (params) => {
  return request({
    url: "/device/maintenance/page",
    method: "get",
    params,
  });
};
/**
 * @desc è®¾å¤‡ä¿å…»è¯¦æƒ…
 * @param {保养但编号} id
 * @returns
 */
export const getUpkeepById = (id) => {
  return request({
    url: `/device/maintenance/${id}`,
    method: "get",
  });
};
/**
 * @desc è®¾å¤‡ä¿å…»æ–°å¢ž
 * @param {新增保养表单} data
 * @returns
 */
export const addUpkeep = (data) => {
  return request({
    url: "/device/maintenance",
    method: "post",
    data,
  });
};
/**
 * @desc è®¾å¤‡ä¿å…»ç¼–辑
 * @param {编辑保养表单} data
 * @returns
 */
export const editUpkeep = (data) => {
  return request({
    url: "/device/maintenance",
    method: "put",
    data,
  });
};
/**
 * @desc æ–°å¢žä¿å…»è¡¨å•
 * @param {新增保养表单} data
 * @returns
 */
export const addMaintenance = (data) => {
  return request({
    url: "/device/maintenance/maintenance",
    method: "post",
    data,
  });
};
export const delUpkeep = (id) => {
  return request({
    url: `/device/maintenance/${id}`,
    method: "delete",
  });
};
src/api/system/user.js
@@ -45,4 +45,11 @@
    url: '/system/user/userListNoPage',
    method: 'get'
  })
}
// æŸ¥è¯¢ç”¨æˆ·åˆ—表
export function userListNoPageByTenantId() {
  return request({
    url: '/system/user/userListNoPageByTenantId',
    method: 'get'
  })
}
src/pages.json
@@ -273,11 +273,81 @@
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/approve",
      "style": {
        "navigationBarTitleText": "审核",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/contactSelect",
      "style": {
        "navigationBarTitleText": "选择联系人",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/clientVisit/index",
      "style": {
        "navigationBarTitleText": "客户拜访",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/ledger/index",
      "style": {
        "navigationBarTitleText": "设备台账",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/ledger/detail",
        "style": {
          "navigationBarTitleText": "设备台账详情",
          "navigationStyle": "custom"
        }
    },
    {
      "path": "pages/equipmentManagement/repair/index",
        "style": {
          "navigationBarTitleText": "设备报修",
          "navigationStyle": "custom"
        }
    },
    {
      "path": "pages/equipmentManagement/repair/add",
        "style": {
          "navigationBarTitleText": "新增设备报修",
          "navigationStyle": "custom"
        }
    },
    {
      "path": "pages/equipmentManagement/repair/maintain",
        "style": {
          "navigationBarTitleText": "设备维修",
          "navigationStyle": "custom"
        }
    }
    {
      "path": "pages/equipmentManagement/upkeep/index",
        "style": {
          "navigationBarTitleText": "设备保养",
          "navigationStyle": "custom"
        }
    },
    {
      "path": "pages/equipmentManagement/upkeep/add",
        "style": {
          "navigationBarTitleText": "新增保养计划",
          "navigationStyle": "custom"
        }
    },
    {
      "path": "pages/equipmentManagement/upkeep/maintain",
        "style": {
          "navigationBarTitleText": "维修保养",
          "navigationStyle": "custom"
        }
    }
  ],
  "subPackages": [
src/pages/cooperativeOffice/collaborativeApproval/approve.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,519 @@
<template>
  <view class="approve-page">
    <PageHeader title="审核" @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
        <text class="info-title">申请信息</text>
      </view>
      <view class="info-content">
        <view class="info-row">
          <text class="info-label">申请人</text>
          <text class="info-value">{{ approvalData.approveUserName }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请部门</text>
          <text class="info-value">{{ approvalData.approveDeptName }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请事由</text>
          <text class="info-value">{{ approvalData.approveReason }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请日期</text>
          <text class="info-value">{{ approvalData.approveTime }}</text>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹æµç¨‹ -->
    <view class="approval-process">
      <view class="process-header">
        <text class="process-title">审批流程</text>
      </view>
      <view class="process-steps">
        <view
          v-for="(step, index) in approvalSteps"
          :key="index"
          class="process-step"
          :class="{
            'completed': step.status === 'completed',
            'current': step.status === 'current',
            'pending': step.status === 'pending',
            'rejected': step.status === 'rejected'
          }"
        >
          <view class="step-indicator">
            <view class="step-dot">
              <text v-if="step.status === 'completed'" class="step-icon">✓</text>
              <text v-else-if="step.status === 'rejected'" class="step-icon">✗</text>
              <text v-else class="step-number">{{ index + 1 }}</text>
            </view>
            <view v-if="index < approvalSteps.length - 1" class="step-line"></view>
          </view>
          <view class="step-content">
            <view class="step-info">
              <text class="step-title">{{ step.title }}</text>
              <text class="step-approver">{{ step.approverName }}</text>
              <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text>
            </view>
            <view v-if="step.opinion" class="step-opinion">
              <text class="opinion-label">审批意见:</text>
              <text class="opinion-content">{{ step.opinion }}</text>
            </view>
            <!-- ç­¾åå±•示 -->
            <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;">
              <text class="opinion-label">签名:</text>
              <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" />
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- å®¡æ ¸æ„è§è¾“å…¥ -->
    <view v-if="canApprove" class="approval-input">
      <view class="input-header">
        <text class="input-title">审核意见</text>
      </view>
      <view class="input-content">
        <van-field
          v-model="approvalOpinion"
          type="textarea"
          rows="4"
          placeholder="请输入审核意见"
          maxlength="200"
          show-word-limit
        />
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="canApprove" class="footer-actions">
      <van-button class="reject-btn" @click="handleReject">驳回</van-button>
      <van-button class="approve-btn" @click="handleApprove">通过</van-button>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess'
import useUserStore from '@/store/modules/user'
import { showToast } from 'vant'
import PageHeader from "@/components/PageHeader.vue";
const userStore = useUserStore()
const approvalData = ref({})
const approvalSteps = ref([])
const approvalOpinion = ref('')
const approveId = ref('')
// ä»Žè¯¦æƒ…接口字段对齐 canApprove:仅当有 isShen çš„节点时可审批
const canApprove = computed(() => {
  return approvalSteps.value.some(step => step.isShen === true)
})
onMounted(() => {
  const pages = getCurrentPages()
  const currentPage = pages[pages.length - 1]
  approveId.value = currentPage.options.approveId
  if (approveId.value) {
    loadApprovalData()
  }
})
const loadApprovalData = () => {
  // åŸºæœ¬ç”³è¯·ä¿¡æ¯
  approveProcessGetInfo({ id: approveId.value }).then(res => {
    approvalData.value = res.data || {}
  })
  // å®¡æ‰¹èŠ‚ç‚¹è¯¦æƒ…
  approveProcessDetails(approveId.value).then(res => {
    const list = Array.isArray(res.data) ? res.data : []
    // ä¿å­˜åŽŸå§‹èŠ‚ç‚¹æ•°æ®ä¾›æäº¤ä½¿ç”¨
    activities.value = list
    approvalSteps.value = list.map((it, idx) => {
      // èŠ‚ç‚¹çŠ¶æ€æ˜ å°„ï¼š1=通过,2=不通过,否则看是否当前(isShen),再默认为待处理
      let status = 'pending'
      if (it.approveNodeStatus === 1) status = 'completed'
      else if (it.approveNodeStatus === 2) status = 'rejected'
      else if (it.isShen) status = 'current'
      return {
        title: `第${idx + 1}步审批`,
        approverName: it.approveNodeUser || '未知用户',
        status,
        approveTime: it.approveTime || null,
        opinion: it.approveNodeReason || '',
        urlTem: it.urlTem || '',
        isShen: !!it.isShen
      }
    })
  })
}
const goBack = () => {
  uni.navigateBack()
}
const submitForm = (status) => {
  // å¯é€‰ï¼šæ ¡éªŒå®¡æ ¸æ„è§
  if (!approvalOpinion.value?.trim()) {
    showToast('请输入审核意见')
    return
  }
  // æ‰¾åˆ°å½“前可审批节点
  const filteredActivities = activities.value.filter(activity => activity.isShen)
  if (!filteredActivities.length) {
    showToast('当前无可审批节点')
    return
  }
  // å†™å…¥çŠ¶æ€å’Œæ„è§
  filteredActivities[0].approveNodeStatus = status
  filteredActivities[0].approveNodeReason = approvalOpinion.value || ''
  // è®¡ç®—是否为最后一步
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1
  // è°ƒç”¨åŽç«¯
  updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
    const msg = status === 1 ? '审批通过' : '审批已驳回'
    showToast(msg)
    // æç¤ºåŽè¿”回上一个页面
    setTimeout(() => {
      goBack() // å†…部是 uni.navigateBack()
    }, 800)
  })
}
const handleApprove = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要通过此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(1)
    }
  })
}
const handleReject = () => {
  uni.showModal({
    title: '确认操作',
    content: '确定要驳回此审批吗?',
    success: (res) => {
      if (res.confirm) submitForm(2)
    }
  })
}
// åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
const activities = ref([])
</script>
<style scoped lang="scss">
.approve-page {
  min-height: 100vh;
  background: #f8f9fa;
  padding-bottom: 80px;
}
.header {
  display: flex;
  align-items: center;
  background: #fff;
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  position: sticky;
  top: 0;
  z-index: 100;
}
.title {
  flex: 1;
  text-align: center;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.application-info {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
.info-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
.info-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.info-content {
  padding: 16px;
}
.info-row {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  &:last-child {
    margin-bottom: 0;
  }
}
.info-label {
  font-size: 14px;
  color: #666;
  width: 80px;
  flex-shrink: 0;
}
.info-value {
  font-size: 14px;
  color: #333;
  flex: 1;
}
.approval-process {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
.process-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
.process-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.process-steps {
  padding: 20px;
}
.process-step {
  display: flex;
  position: relative;
  margin-bottom: 24px;
  &:last-child {
    margin-bottom: 0;
    .step-line {
      display: none;
    }
  }
}
.step-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-right: 16px;
}
.step-dot {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  position: relative;
  z-index: 2;
}
.process-step.completed .step-dot {
  background: #52c41a;
  color: #fff;
}
.process-step.current .step-dot {
  background: #1890ff;
  color: #fff;
  animation: pulse 2s infinite;
}
.process-step.pending .step-dot {
  background: #d9d9d9;
  color: #999;
}
.step-line {
  width: 2px;
  height: 40px;
  background: #d9d9d9;
  margin-top: 8px;
}
.process-step.completed .step-line {
  background: #52c41a;
}
.process-step.rejected .step-dot {
  background: #ff4d4f;
  color: #fff;
}
.process-step.rejected .step-line {
  background: #ff4d4f;
}
.step-content {
  flex: 1;
  padding-top: 4px;
}
.step-info {
  margin-bottom: 8px;
}
.step-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  display: block;
  margin-bottom: 4px;
}
.step-approver {
  font-size: 14px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
.step-time {
  font-size: 12px;
  color: #999;
  display: block;
}
.step-opinion {
  background: #f8f9fa;
  padding: 12px;
  border-radius: 8px;
  border-left: 4px solid #52c41a;
}
.opinion-label {
  font-size: 12px;
  color: #666;
  display: block;
  margin-bottom: 4px;
}
.opinion-content {
  font-size: 14px;
  color: #333;
  line-height: 1.5;
}
.approval-input {
  background: #fff;
  margin: 16px;
  border-radius: 12px;
  overflow: hidden;
}
.input-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
  background: #f8f9fa;
}
.input-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.input-content {
  padding: 16px;
}
.footer-actions {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 16px;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}
.reject-btn {
  width: 120px;
  background: #ff4d4f;
  color: #fff;
  border: none;
}
.approve-btn {
  width: 120px;
  background: #52c41a;
  color: #fff;
  border: none;
}
@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
  }
  70% {
    box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
  }
}
.signature-section {
  background: #fff;
  padding: 12px 16px 16px;
  border-top: 1px solid #f0f0f0;
}
.signature-header {
  margin-bottom: 8px;
}
.signature-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
.signature-box {
  width: 100%;
  height: 180px;
  background: #fff;
  border: 1px dashed #d9d9d9;
  border-radius: 8px;
  overflow: hidden;
}
.signature-actions {
  margin-top: 8px;
  display: flex;
  justify-content: flex-end;
}
</style>
src/pages/cooperativeOffice/collaborativeApproval/contactSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,391 @@
<template>
  <view class="contact-select">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <view class="header">
      <up-icon name="arrow-left" size="20" color="#333" @click="goBack" />
      <text class="title">选择联系人</text>
      <text class="confirm-btn" @click="confirmSelect">确定</text>
    </view>
    <!-- æœç´¢æ¡† -->
<!--    <view class="search-section">-->
<!--      <van-search-->
<!--        v-model="searchValue"-->
<!--        placeholder="搜索联系人"-->
<!--        @search="onSearch"-->
<!--        @input="onSearch"-->
<!--      />-->
<!--    </view>-->
    <!-- å·²é€‰æ‹©çš„联系人 -->
    <view class="selected-section" v-if="selectedContact">
      <view class="selected-header">
        <text class="selected-title">已选择</text>
        <text class="clear-btn" @click="clearSelected">清空</text>
      </view>
      <view class="selected-item">
        <view class="contact-avatar">
          <text class="avatar-text">{{ selectedContact.nickName.charAt(0) }}</text>
        </view>
        <view class="contact-details">
          <text class="contact-name">{{ selectedContact.nickName }}</text>
        </view>
        <van-icon name="cross" size="16" color="#999" @click="clearSelected" />
      </view>
    </view>
    <!-- è”系人列表 -->
    <view class="contact-list">
      <view class="list-header">
        <text class="list-title">全部联系人</text>
      </view>
      <van-list
        v-model:loading="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onLoad"
      >
        <view
          v-for="contact in userList"
          :key="contact.userId"
          class="contact-item"
          :class="{ 'selected': isSelected(contact) }"
          @click="selectContact(contact)"
        >
          <view class="contact-info">
            <view class="contact-avatar">
              <text class="avatar-text">{{ contact.nickName.charAt(0) }}</text>
            </view>
            <view class="contact-details">
              <text class="contact-name">{{ contact.nickName }}</text>
<!--              <text class="contact-dept">{{ contact.department }}</text>-->
            </view>
          </view>
        </view>
      </van-list>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { userListNoPageByTenantId } from "@/api/system/user"
const loading = ref(false)
const finished = ref(false)
const selectedContact = ref(null)
const userList = ref([])
// æŽ¥æ”¶ä¼ é€’的参数
const stepIndex = ref(0)
onMounted(() => {
  // èŽ·å–é¡µé¢å‚æ•°
  const pages = getCurrentPages()
  const currentPage = pages[pages.length - 1]
  if (currentPage.options.stepIndex !== undefined) {
    stepIndex.value = parseInt(currentPage.options.stepIndex)
  }
  // åˆå§‹åŒ–联系人数据
  initContacts()
})
const initContacts = () => {
  userListNoPageByTenantId().then((res) => {
    userList.value = res.data
  })
  finished.value = true
}
const onLoad = () => {
  // æ¨¡æ‹ŸåŠ è½½æ›´å¤šæ•°æ®
  setTimeout(() => {
    loading.value = false
    finished.value = true
  }, 1000)
}
const isSelected = (contact) => {
  return selectedContact.value && selectedContact.value.userId === contact.userId
}
const selectContact = (contact) => {
  // å•选模式,直接替换选中的联系人
  selectedContact.value = contact
}
const clearSelected = () => {
  selectedContact.value = null
}
const goBack = () => {
  uni.navigateBack()
}
const confirmSelect = () => {
  if (!selectedContact.value) {
    uni.showToast({
      title: '请选择一个联系人',
      icon: 'none'
    })
    return
  }
  // ä½¿ç”¨ uni.$emit å‘送数据
  uni.$emit('selectContact', {
    stepIndex: stepIndex.value,
    contact: selectedContact.value
  })
  uni.navigateBack()
}
</script>
<style scoped lang="scss">
.contact-select {
  min-height: 100vh;
  background: #f8f9fa;
}
.header {
  background: #ffffff;
    padding: 16px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    /* å…¼å®¹ iOS åˆ˜æµ·/灵动岛安全区 */
    padding-top: calc(env(safe-area-inset-top));
    top: 0;
    z-index: 100;
    position: relative;
}
.title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.confirm-btn {
  color: #006cfb;
  font-size: 16px;
  font-weight: 500;
}
.search-section {
  background: #fff;
  padding: 12px 16px;
  border-bottom: 1px solid #f0f0f0;
}
.selected-section {
  background: #fff;
  margin-top: 8px;
  padding: 16px;
}
.selected-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.selected-title {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
.clear-btn {
  color: #006cfb;
  font-size: 14px;
}
.selected-item {
  display: flex;
  align-items: center;
  background: #f0f8ff;
  border: 1px solid #006cfb;
  border-radius: 12px;
  padding: 12px;
  gap: 12px;
  position: relative;
  &::before {
    content: '';
    position: absolute;
    top: -2px;
    right: -2px;
    width: 16px;
    height: 16px;
    background: #52c41a;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  &::after {
    content: '✓';
    position: absolute;
    top: -1px;
    right: -1px;
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    color: #fff;
    font-weight: bold;
  }
}
.contact-list {
  background: #fff;
  margin-top: 8px;
}
.list-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
}
.list-title {
  font-size: 14px;
  color: #666;
}
.contact-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #f8f9fa;
  transition: all 0.2s;
  position: relative;
  &.selected {
    background-color: #f0f8ff;
    &::before {
      content: '';
      position: absolute;
      left: 8px;
      top: 50%;
      transform: translateY(-50%);
      width: 4px;
      height: 4px;
      background: #006cfb;
      border-radius: 50%;
      box-shadow: 0 0 0 4px rgba(0, 108, 251, 0.2);
    }
  }
  &:active {
    background-color: #f5f5f5;
  }
}
.contact-info {
  display: flex;
  align-items: center;
  flex: 1;
  padding-left: 16px;
}
.contact-avatar {
  width: 40px;
  height: 40px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12px;
  position: relative;
}
.avatar-text {
  color: #fff;
  font-size: 16px;
  font-weight: 500;
}
.contact-details {
  flex: 1;
}
.contact-name {
  display: block;
  font-size: 16px;
  color: #333;
}
.contact-dept {
  font-size: 12px;
  color: #999;
}
// è‡ªå®šä¹‰å•选按钮样式
:deep(.van-radio) {
  .van-radio__icon {
    width: 20px;
    height: 20px;
    border: 2px solid #ddd;
    border-radius: 50%;
    background: #fff;
    position: relative;
    transition: all 0.2s;
    &::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%) scale(0);
      width: 8px;
      height: 8px;
      background: #006cfb;
      border-radius: 50%;
      transition: transform 0.2s;
    }
  }
  &.van-radio--checked {
    .van-radio__icon {
      border-color: #006cfb;
      background: #fff;
      &::before {
        transform: translate(-50%, -50%) scale(1);
      }
      &::after {
        content: '';
        position: absolute;
        top: -2px;
        left: -2px;
        right: -2px;
        bottom: -2px;
        border: 2px solid rgba(0, 108, 251, 0.2);
        border-radius: 50%;
        animation: ripple 0.6s ease-out;
      }
    }
  }
}
@keyframes ripple {
  0% {
    transform: scale(0.8);
    opacity: 1;
  }
  100% {
    transform: scale(1.2);
    opacity: 0;
  }
}
</style>
src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -1,58 +1,74 @@
<template>
  <view class="account-detail">
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <view class="header">
      <up-icon name="arrow-left" size="20" color="#333" @click="goBack" />
      <text class="title">审批流程</text>
    </view>
    <PageHeader title="审批流程" @back="goBack" />
    <!-- è¡¨å•区域 -->
    <view class="form-section">
      <van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right">
        <van-cell-group inset style="height:auto">
      <van-form ref="formRef" @submit="submitForm" :rules="rules" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
                <van-cell-group style="margin-bottom: 16px;">
                    <van-field
                        v-model="form.approveReason"
                        name="approveReason"
                        rows="2"
                        autosize
                        label="申请事由"
                        type="textarea"
                        maxlength="200"
                        :rules="[{ required: true, message: '申请事由不能为空' }]"
                        placeholder="请输入申请事由"
                        show-word-limit
                        required
                    />
                </van-cell-group>
                <van-cell-group>
                    <van-field
                        v-model="form.approveDeptName"
                        readonly
                        name="picker"
                        label="申请部门"
                        placeholder="请选择申请部门"
                        :rules="[{ required: true, message: '请选择申请部门' }]"
                        @click="showPicker = true"
                        required
                    />
          <van-field
            v-model="taxPrice"
            v-model="form.approveUserName"
            name="taxPrice"
            label="姓名"
            placeholder="请输入姓名"
            :rules="[{ required: true, message: '姓名不能为空' }]"
            label="申请人"
            placeholder="请输入申请人"
            :rules="[{ required: true, message: '申请人不能为空' }]"
            required
            readonly
          />
          <van-field
            v-model="result"
            readonly
            name="picker"
            label="申请部门"
            placeholder="请选择申请部门"
            :rules="[{ required: true, message: '请选择申请部门' }]"
            @click="showPicker = true"
            required
          />
          <van-popup
            v-model:show="showPicker"
            destroy-on-close
            position="bottom"
          >
            <van-picker
              :columns="columns"
              :columns="productOptions"
              :model-value="pickerValue"
              @confirm="onConfirm"
              @cancel="showPicker = false"
            />
          </van-popup>
          <van-field
            v-model="message"
            name="message"
            rows="1"
            autosize
            label="申请事由"
            type="textarea"
            placeholder="请输入申请事由"
            height="100"
            :rules="[{ required: true, message: '申请事由不能为空' }]"
            required
          />
                    <van-field
                        v-model="form.approveTime"
                        label="申请日期"
                        placeholder="请选择"
                        readonly
                        required
                        @click="showDatePicker"
                        :rules="[{ required: true, message: '请选择来款日期' }]"
                    />
                    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
                    <van-popup v-model:show="showDate" position="bottom">
                        <van-date-picker
                            v-model="currentDate"
                            title="选择日期"
                            @confirm="onDateConfirm"
                            @cancel="showDate = false"
                        />
                    </van-popup>
        </van-cell-group>
      </van-form>
    </view>
@@ -60,30 +76,38 @@
    <view class="approval-process">
      <view class="approval-header">
        <text class="approval-title">审核流程</text>
        <text class="approval-desc">已由管理员预设不可修改</text>
        <text class="approval-desc">每个步骤只能选择一个审批人</text>
      </view>
      <view class="approval-steps">
        <view v-for="(step, stepIndex) in approvalSteps" :key="stepIndex" class="approval-step">
        <view v-for="(step, stepIndex) in approverNodes" :key="stepIndex" class="approval-step">
          <view class="step-dot"></view>
          <view class="step-title">
            <text>审批人</text>
          </view>
          <view class="approvers-container">
            <view v-for="(approver, approverIndex) in step.approvers" :key="approverIndex" class="approver-item">
              <view class="approver-avatar"></view>
              <text class="approver-name">{{ approver.name }}</text>
              <view class="delete-approver-btn" @click="removeApprover(stepIndex, approverIndex)">×</view>
          <view class="approver-container">
            <view v-if="step.nickName" class="approver-item">
              <view class="approver-avatar">
                <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
                <view class="status-dot"></view>
              </view>
              <view class="approver-info">
                <text class="approver-name">{{ step.nickName }}</text>
              </view>
              <view class="delete-approver-btn" @click="removeApprover(stepIndex)">×</view>
            </view>
            <view class="add-approver-btn" @click="addApprover(stepIndex)">+
            <view v-else class="add-approver-btn" @click="addApprover(stepIndex)">
              <view class="add-circle">+</view>
              <text class="add-label">选择审批人</text>
            </view>
          </view>
          <view class="step-line" v-if="stepIndex < approvalSteps.length - 1"></view>
          <view class="delete-step-btn" @click="removeApprovalStep(stepIndex)">删除节点</view>
          <view class="step-line" v-if="stepIndex < approverNodes.length - 1"></view>
          <view class="delete-step-btn" v-if="approverNodes.length > 1" @click="removeApprovalStep(stepIndex)">删除节点</view>
        </view>
      </view>
      <view class="add-step-btn" @click="addApprovalStep">
        <text>新增节点审核人</text>
      <view class="add-step-btn">
                <van-button icon="plus" plain type="primary" style="width: 100%" @click="addApprovalStep">新增节点</van-button>
      </view>
    </view>
@@ -95,166 +119,238 @@
  </view>
</template>
<script>
import { ref, onMounted } from "vue";
<script setup>
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import PageHeader from "@/components/PageHeader.vue";
import useUserStore from "@/store/modules/user";
import {getDept, approveProcessGetInfo, approveProcessAdd, approveProcessUpdate} from "@/api/collaborativeApproval/approvalProcess";
import { showToast } from 'vant'
import {userListNoPageByTenantId} from "@/api/system/user";
export default {
  setup() {
    const rules = ref({
  taxPrice: {
    rules: [{ required: true, errorMessage: '姓名不能为空' }]
  },
  result: {
    rules: [{ required: true, errorMessage: '请选择申请部门' }]
  },
  message: {
    rules: [{ required: true, errorMessage: '申请事由不能为空' }]
  },
const data = reactive({
    form: {
        approveTime: "",
        approveId: "",
        approveUser: "",
        approveUserName: "",
        approveDeptName: "",
        approveDeptId: "",
        approveReason: "",
        checkResult: "",
        tempFileIds: [],
        approverList: [] // æ–°å¢žå­—段,存储所有节点的审批人id
    },
    rules: {
        approveTime: [{ required: false, message: "请输入", trigger: "change" },],
        approveId: [{ required: false, message: "请输入", trigger: "blur" }],
        approveUser: [{ required: false, message: "请输入", trigger: "blur" }],
        approveDeptId: [{ required: true, message: "请输入", trigger: "blur" }],
        approveReason: [{ required: true, message: "请输入", trigger: "blur" }],
        checkResult: [{ required: false, message: "请输入", trigger: "blur" }],
    },
});
    const result = ref("");
    const pickerValue = ref([]);
    const showPicker = ref(false);
    const columns = ref([]);
    onMounted(async () => {
      try {
        // æ›¿æ¢ä¸ºå®žé™…接口地址
        // const response = await axios.get('/api/getDepartments');
        columns.value = [
          {
            text: "杭州",
            value: "Hangzhou",
          },
          {
            text: "宁波",
            value: "Ningbo",
          },
          {
            text: "温州",
            value: "Wenzhou",
          },
          {
            text: "绍兴",
            value: "Shaoxing",
          },
          {
            text: "湖州",
            value: "Huzhou",
          },
        ];
      } catch (error) {
        console.error("获取部门数据失败:", error);
      }
    });
    const onConfirm = ({ selectedValues, selectedOptions }) => {
      result.value = selectedOptions[0]?.text;
      pickerValue.value = selectedValues;
      showPicker.value = false;
    };
    const taxPrice = ref("");
    const contractAmount = ref("");
    const approvalSteps = ref([
      { approvers: [{ name: '卢小敏' }, { name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] },
      { approvers: [{ name: '卢小敏' }] }
    ]);
const { form, rules } = toRefs(data);
const result = ref("");
const pickerValue = ref([]);
const showPicker = ref(false);
const productOptions = ref([]);
const operationType = ref("");
const currentApproveStatus = ref("");
const approverNodes = ref([]);
const userList = ref([]);
const formRef = ref(null);
const message = ref("");
const showDate = ref(false)
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
const userStore = useUserStore()
    const goBack = () => {
        uni.navigateBack();
    };
    const formRef = ref(null);
    const submitForm = () => {
      formRef.value.validate().then(() => {
        // è¡¨å•校验通过,可以提交数据
        console.log("表单数据:", {
          taxPrice: taxPrice.value,
          department: result.value,
          message: message.value,
          approvalSteps: approvalSteps.value
        });
        uni.showToast({
          title: "保存成功",
          icon: "success",
        });
      }).catch((error) => {
        console.error("表单校验失败:", error);
        // æ˜¾ç¤ºå…·ä½“的错误信息
        if (error.length > 0) {
          const firstError = error[0];
          uni.showToast({
            title: firstError.message || '表单校验失败',
            icon: 'none'
          });
        } else {
          uni.showToast({
            title: '表单校验失败,请检查必填项',
            icon: 'none'
          });
        }
      });
    };
    const message = ref("");
    const addApprover = (stepIndex) => {
      // åœ¨æŒ‡å®šå®¡æ‰¹æ­¥éª¤æ·»åŠ æ–°çš„å®¡æ‰¹äºº
      approvalSteps.value[stepIndex].approvers.push({ name: '卢小敏' });
    };
    const addApprovalStep = () => {
      // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
      approvalSteps.value.push({ approvers: [{ name: '卢小敏' }] });
    };
    const removeApprover = (stepIndex, approverIndex) => {
      // ç¡®ä¿æ¯ä¸ªæ­¥éª¤è‡³å°‘保留一个审批人
      if (approvalSteps.value[stepIndex].approvers.length > 1) {
        approvalSteps.value[stepIndex].approvers.splice(approverIndex, 1);
      } else {
        uni.showToast({
          title: '每个步骤至少需要一个审批人',
          icon: 'none'
        });
      }
    };
    const removeApprovalStep = (stepIndex) => {
      // ç¡®ä¿è‡³å°‘保留一个审批步骤
      if (approvalSteps.value.length > 1) {
        approvalSteps.value.splice(stepIndex, 1);
      } else {
        uni.showToast({
          title: '至少需要一个审批步骤',
          icon: 'none'
        });
      }
    };
    return {
      rules,
      removeApprovalStep,
    removeApprover,
      result,
      pickerValue,
      columns,
      onConfirm,
      showPicker,
      taxPrice,
      contractAmount,
      goBack,
      submitForm,
      approvalSteps,
      addApprover,
      addApprovalStep,
      formRef,
      message
    };
  },
const getProductOptions = () => {
    getDept().then((res) => {
        productOptions.value = res.data.map(item => ({
            value: item.deptId,
            text: item.deptName
        }))
    });
};
const fileList = ref([]);
let nextApproverId = 2;
onMounted(async () => {
  try {
        getProductOptions()
        userListNoPageByTenantId().then((res) => {
            userList.value = res.data
        })
        form.value.approveUser = userStore.id
        form.value.approveUserName = userStore.nickName
        form.value.approveTime = getCurrentDate();
        // èŽ·å–URL参数
        const pages = getCurrentPages();
        const currentPage = pages[pages.length - 1];
        operationType.value = currentPage.options.operationType || 'add';
        // å¦‚果是编辑模式,从本地存储获取数据
        if (operationType.value === 'edit') {
            const storedData = uni.getStorageSync('invoiceLedgerEditRow');
            if (storedData) {
                const row = JSON.parse(storedData);
                fileList.value = row.commonFileList || [];
                form.value.tempFileIds = fileList.value.map(file => file.id);
                currentApproveStatus.value = row.approveStatus;
                approveProcessGetInfo({id: row.approveId, approveReason: '1'}).then(res => {
                    form.value = {...res.data};
                    // åæ˜¾å®¡æ‰¹äºº
                    if (res.data && res.data.approveUserIds) {
                        const userIds = res.data.approveUserIds.split(',');
                        approverNodes.value = userIds.map((userId, idx) => {
                            const userIdNum = parseInt(userId.trim());
                            // ä»ŽuserList中找到对应的用户信息
                            const userInfo = userList.value.find(user => user.userId === userIdNum);
                            return {
                                id: idx + 1,
                                userId: userIdNum,
                                nickName: userInfo ? userInfo.nickName : null
                            };
                        });
                        nextApproverId = userIds.length + 1;
                    } else {
                        // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
                        approverNodes.value = [{ id: 1, userId: null, nickName: null }];
                        nextApproverId = 2;
                    }
                });
            }
        } else {
            // æ–°å¢žæ¨¡å¼ï¼Œåˆå§‹åŒ–一个空的审批节点
            approverNodes.value = [{ id: 1, userId: null }];
        }
    // ç›‘听联系人选择事件
    uni.$on('selectContact', handleSelectContact);
  } catch (error) {
    console.error("获取部门数据失败:", error);
  }
});
onUnmounted(() => {
  // ç§»é™¤äº‹ä»¶ç›‘听
  uni.$off('selectContact', handleSelectContact);
});
const onConfirm = ({ selectedValues, selectedOptions }) => {
  form.value.approveDeptName = selectedOptions[0]?.text;
  form.value.approveDeptId = selectedOptions[0]?.value;
  pickerValue.value = selectedValues;
  showPicker.value = false;
};
const goBack = () => {
    // æ¸…除本地存储的数据
    uni.removeStorageSync('invoiceLedgerEditRow');
  uni.navigateBack();
};
const submitForm = () => {
  // æ£€æŸ¥æ¯ä¸ªå®¡æ‰¹æ­¥éª¤æ˜¯å¦éƒ½æœ‰å®¡æ‰¹äºº
  const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
  if (hasEmptyStep) {
        showToast('请为每个审批步骤选择审批人');
    return;
  }
  formRef.value.validate().then(() => {
    // è¡¨å•校验通过,可以提交数据
        // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
        console.log('approverNodes---', approverNodes.value)
        form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
        form.value.approveType = 0
        if (operationType.value === "add" || currentApproveStatus.value == 3) {
            approveProcessAdd(form.value).then(res => {
                showToast("提交成功");
                goBack()
            })
        } else {
            approveProcessUpdate(form.value).then(res => {
                showToast("提交成功");
                goBack()
            })
        }
  }).catch((error) => {
    console.error("表单校验失败:", error);
    // æ˜¾ç¤ºå…·ä½“的错误信息
    if (error.length > 0) {
      const firstError = error[0];
      uni.showToast({
        title: firstError.message || '表单校验失败',
        icon: 'none'
      });
    } else {
      uni.showToast({
        title: '表单校验失败,请检查必填项',
        icon: 'none'
      });
    }
  });
};
// å¤„理联系人选择结果
const handleSelectContact = (data) => {
  const { stepIndex, contact } = data;
  // å°†é€‰ä¸­çš„联系人设置为对应审批步骤的审批人
  approverNodes.value[stepIndex].userId = contact.userId;
  approverNodes.value[stepIndex].nickName = contact.nickName;
};
const addApprover = (stepIndex) => {
  // è·³è½¬åˆ°è”系人选择页面
  uni.navigateTo({
    url: `/pages/cooperativeOffice/collaborativeApproval/contactSelect?stepIndex=${stepIndex}`
  });
};
const addApprovalStep = () => {
  // æ·»åŠ æ–°çš„å®¡æ‰¹æ­¥éª¤
  approverNodes.value.push({ userId: null, nickName: null });
};
const removeApprover = (stepIndex) => {
  // ç§»é™¤å®¡æ‰¹äºº
  approverNodes.value[stepIndex].userId = null;
  approverNodes.value[stepIndex].nickName = null;
};
const removeApprovalStep = (stepIndex) => {
  // ç¡®ä¿è‡³å°‘保留一个审批步骤
  if (approverNodes.value.length > 1) {
    approverNodes.value.splice(stepIndex, 1);
  } else {
    uni.showToast({
      title: '至少需要一个审批步骤',
      icon: 'none'
    });
  }
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true
}
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    form.value.approveTime = selectedValues.join('-')
    currentDate.value = selectedValues
    showDate.value = false
}
// èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
function getCurrentDate() {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0"); // æœˆä»½ä»Ž0开始
    const day = String(today.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
}
</script>
<style scoped lang="scss">
@@ -287,74 +383,6 @@
  margin-top: 16px;
}
.van-field {
  height: 56px;
  line-height: 36px;
}
.product-section {
  background: #fff;
  margin: 16px;
  border-radius: 16px;
  padding: 20px 16px 8px 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.section-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.add-btn {
  background: #2979ff;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 14px;
}
.product-card {
  background: #f8f9fa;
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 16px;
  box-shadow: 0 1px 4px rgba(41, 121, 255, 0.06);
  position: relative;
}
.product-row {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
}
.product-label {
  min-width: 60px;
  color: #888;
  font-size: 13px;
}
.del-row {
  justify-content: flex-end;
}
.del-btn {
  background: #ff4d4f;
  color: #fff;
  border-radius: 8px;
  padding: 4px 16px;
  font-size: 13px;
  margin-top: 4px;
}
.approval-process {
  background: #fff;
  margin: 16px;
@@ -380,156 +408,347 @@
  color: #999;
}
/* æ ·å¼å¢žå¼ºä¸ºâ€œç®€æ´å°åœ†åœˆé£Žæ ¼â€ */
.approval-steps {
  padding-left: 16px;
  padding-left: 22px;
  position: relative;
  &::before {
    content: '';
    position: absolute;
    left: 11px;
    top: 40px;
    bottom: 40px;
    width: 2px;
    background: linear-gradient(to bottom, #e6f7ff 0%, #bae7ff 50%, #91d5ff 100%);
    border-radius: 1px;
  }
}
.approval-step {
  position: relative;
  margin-bottom: 20px;
  margin-bottom: 24px;
  &::before {
    content: '';
    position: absolute;
    left: -18px;
    top: 14px; // ä»Ž 8px è°ƒæ•´ä¸º 14px,与文字中心对齐
    width: 12px;
    height: 12px;
    background: #fff;
    border: 3px solid #006cfb;
    border-radius: 50%;
    z-index: 2;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
}
.step-title {
    top: 12px;
  margin-bottom: 12px;
  position: relative;
    margin-left: 6px;
}
.step-title text {
  font-size: 14px;
  color: #666;
  background: #f0f0f0;
  padding: 2px 8px;
  border-radius: 4px;
}
.approvers-container {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 8px;
  padding: 4px 12px;
  border-radius: 12px;
  position: relative;
  line-height: 1.4; // ç¡®ä¿æ–‡å­—行高一致
}
.approver-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 60px;
  background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
  border-radius: 16px;
  padding: 16px;
  gap: 12px;
  position: relative;
  border: 1px solid #e6f7ff;
  box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
  transition: all 0.3s ease;
}
.approver-avatar {
  width: 40px;
  height: 40px;
  background: #e6f7ff;
  width: 48px;
  height: 48px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.approver-avatar::after {
  content: '👤';
  font-size: 20px;
.avatar-text {
  color: #fff;
  font-size: 18px;
  font-weight: 600;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.approver-info {
  flex: 1;
  position: relative;
}
.approver-name {
  font-size: 12px;
  display: block;
  font-size: 16px;
  color: #333;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 2px;
  font-weight: 500;
  position: relative;
}
.approver-dept {
  font-size: 12px;
  color: #999;
  background: rgba(0, 108, 251, 0.05);
  padding: 2px 8px;
  border-radius: 8px;
  display: inline-block;
  position: relative;
  &::before {
    content: '';
    position: absolute;
    left: 4px;
    top: 50%;
    transform: translateY(-50%);
    width: 2px;
    height: 2px;
    background: #006cfb;
    border-radius: 50%;
  }
}
.delete-approver-btn {
  font-size: 12px;
  font-size: 16px;
  color: #ff4d4f;
  background: rgba(255, 77, 79, 0.1);
  width: 16px;
  height: 16px;
  background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
  transition: all 0.3s ease;
  position: relative;
}
.add-approver-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
  border: 2px dashed #006cfb;
  border-radius: 16px;
  padding: 20px;
  color: #006cfb;
  font-size: 14px;
  position: relative;
  transition: all 0.3s ease;
  &::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 32px;
    height: 32px;
    border: 2px solid #006cfb;
    border-radius: 50%;
    opacity: 0;
    transition: all 0.3s ease;
  }
}
.delete-step-btn {
  margin-top: 8px;
  color: #ff4d4f;
  font-size: 12px;
  background: rgba(255, 77, 79, 0.1);
  padding: 2px 8px;
  border-radius: 4px;
  background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
  padding: 6px 12px;
  border-radius: 12px;
  display: inline-block;
  position: relative;
  transition: all 0.3s ease;
  &::before {
    content: '';
    position: absolute;
    left: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 4px;
    background: #ff4d4f;
    border-radius: 50%;
  }
.add-approver-btn {
  width: 40px;
  height: 40px;
  border: 1px dashed #ccc;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: #999;
  margin-top: 8px;
}
.step-line {
  position: absolute;
  left: 20px;
  top: 100%;
  width: 1px;
  height: 30px;
  background: #e0e0e0;
  display: none; // éšè—åŽŸæ¥çš„çº¿æ¡ï¼Œä½¿ç”¨ä¼ªå…ƒç´ ä»£æ›¿
}
.add-step-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 16px;
  color: #006cfb;
  font-size: 14px;
  padding: 8px 0;
  border: 1px dashed #006cfb;
  border-radius: 8px;
}
.footer-btns {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 12px 0;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
  z-index: 1000;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
  font-weight: 400;
  font-size: 16px;
  color: #ffffff;
  width: 102px;
  background: #c7c9cc;
  box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2);
  border-radius: 40px 40px 40px 40px;
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
  font-weight: 400;
  font-size: 16px;
  color: #ffffff;
  width: 224px;
  background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
  box-shadow: 0px 4px 10px 0px rgba(3, 88, 185, 0.2);
  border-radius: 40px 40px 40px 40px;
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// åŠ¨ç”»å®šä¹‰
@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.2);
    opacity: 0.7;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
@keyframes ripple {
  0% {
    transform: translate(-50%, -50%) scale(0.8);
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) scale(1.6);
    opacity: 0;
  }
}
/* å¦‚果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */
.step-line {
  position: absolute;
  left: 4px;
  top: 48px;
  width: 2px;
  height: calc(100% - 48px);
  background: #E5E7EB;
}
.approver-container {
  display: flex;
  align-items: center;
  background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
  border-radius: 16px;
  gap: 12px;
  padding: 10px 0;
  background: transparent;
  border: none;
  box-shadow: none;
}
.approver-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  background: transparent;
  border: none;
  box-shadow: none;
  border-radius: 0;
}
.approver-avatar {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #F3F4F6;
  border: 2px solid #E5E7EB;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: none; /* ç¦ç”¨æ—‹è½¬ç­‰åŠ¨ç”»ï¼Œå›žå½’ç®€æ´ */
}
.avatar-text {
  font-size: 14px;
  color: #374151;
  font-weight: 600;
}
.add-approver-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
}
.add-approver-btn .add-circle {
  width: 40px;
  height: 40px;
  border: 2px dashed #A0AEC0;
  border-radius: 50%;
  color: #6B7280;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  line-height: 1;
}
.add-approver-btn .add-label {
  color: #3B82F6;
  font-size: 14px;
}
</style>
src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -8,51 +8,87 @@
        <view class="search-filter-section">
            <view class="search-bar">
                <view class="search-input">
                    <u-input placeholder="请输入采购合同号" class="search-text" v-model="searchKeyword">
                        <template #suffix>
                            <up-icon name="search" size="24" color="#999" @click="getList"></up-icon>
                        </template>
                    </u-input>
                    <input
                        class="search-text"
                        placeholder="请输入流程编号"
                        v-model="searchForm.approveId"
                    />
                </view>
                <view class="filter-button" @click="showFilterOptions">
                    <van-icon name="filter-o" size="24" color="#999"></van-icon>
                <view class="search-button" @click="getList">
                    <up-icon name="search" size="24" color="#999"></up-icon>
                </view>
            </view>
        </view>
        <!-- é”€å”®å°è´¦ç€‘布流 -->
        <view class="ledger-list" v-if="total > 0">
        <!-- å®¡æ‰¹åˆ—表 -->
        <view class="ledger-list" v-if="ledgerList.length > 0">
            <view v-for="(item, index) in ledgerList" :key="index">
                <view class="ledger-item" @click="handleItemClick(item)">
                <view class="ledger-item">
                    <view class="item-header">
                        <view class="item-left">
                            <view class="document-icon">
                                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
                            </view>
                            <text class="item-id">{{ item.salesContractNo }}</text>
                            <text class="item-id">{{ item.approveId }}</text>
                        </view>
                        <view class="item-tag">
                            <van-tag :type="getTagClass(item.approveStatus)" size="medium">{{ formatReceiptType(item.approveStatus) }}</van-tag>
                        </view>
                    </view>
                    <up-divider></up-divider>
                    <view class="item-details">
                        <view class="detail-info">
                            <view class="detail-row">
                                <text class="detail-label">申请人</text>
                                <text class="detail-value">{{ item.entryPersonName }}</text>
                            </view>
                            <view class="detail-row">
                                <text class="detail-label">申请日期</text>
                                <text class="detail-value highlightBlue">{{ item.entryDate }}</text>
                            </view>
                        <view class="detail-row">
                            <text class="detail-label">申请人</text>
                            <text class="detail-value">{{ item.approveUserName }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">申请部门</text>
                            <text class="detail-value">{{ item.approveDeptName }}</text>
                        </view>
                        <view class="detail-row-approveReason">
                            <text class="detail-label">审批事由</text>
                            <text class="detail-value highlightBlue">{{ item.approveReason }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">申请日期</text>
                            <text class="detail-value">{{ item.approveTime }}</text>
                        </view>
                        <view class="detail-row">
                            <text class="detail-label">结束日期</text>
                            <text class="detail-value">{{ item.approveOverTime }}</text>
                        </view>
                        <up-divider></up-divider>
                        <view class="detail-info">
                            <view class="detail-row">
                                <text class="detail-label">申请部门</text>
                                <text class="detail-value">{{ item.entryPersonName }}</text>
                            <view class="detail-row-user">
                                <text class="detail-label">当前审批人</text>
                                <view class="detail-value approver-value">
                                    <view class="approver-chip">
                                        <text class="approver-name">{{ item.approveUserCurrentName || '未分配' }}</text>
                                    </view>
                                </view>
                            </view>
                            <view class="detail-row">
                                <text class="detail-label">审批状态</text>
                                <text class="detail-value highlightYellow">{{ item.entryDate }}</text>
                                <view class="actions">
                                    <van-button
                                        type="primary"
                                        size="small"
                                        class="action-btn edit"
                                        :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4"
                                        @click="handleItemClick(item)"
                                    >
                                        ç¼–辑
                                    </van-button>
                                    <van-button
                                        type="success"
                                        size="small"
                                        class="action-btn approve"
                                        :disabled="item.approveUserCurrentId == null || item.approveStatus == 2 || item.approveStatus == 3 || item.approveStatus == 4 || item.approveUserCurrentId !== userStore.id"
                                        @click="approve(item)"
                                    >
                                        å®¡æ ¸
                                    </van-button>
                                </view>
                            </view>
                        </view>
                    </view>
@@ -62,31 +98,34 @@
        <view v-else class="no-data">
            <text>暂无审批数据</text>
        </view>
<van-floating-bubble icon="plus" @click="handleAdd"/>
        <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
        <view class="fab-button" @click="handleAdd">
        <!-- <view class="fab-button" @click="handleAdd">
            <up-icon name="plus" size="24" color="#ffffff"></up-icon>
        </view>
        </view> -->
    </view>
</template>
<script setup>
    import {
        ref,
        reactive,
        onMounted
        toRefs,
        reactive
    } from "vue";
    import {
        ledgerListPage
    } from "@/api/cooperativeOffice/collaborativeApproval";
    import PageHeader from "@/components/PageHeader.vue";
    // æœç´¢å…³é”®è¯
    const searchKeyword = ref("");
    // é”€å”®å°è´¦æ•°æ®
    import {approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess";
    import {onShow} from "@dcloudio/uni-app";
    import useUserStore from "@/store/modules/user";
    const userStore = useUserStore()
    // æ•°æ®
    const ledgerList = ref([]);
    const total = ref(0);
    const data = reactive({
        searchForm: {
            approveId: "",
        },
    });
    const { searchForm } = toRefs(data);
    // è¿”回上一页
    const goBack = () => {
@@ -98,12 +137,11 @@
            current: -1,
            size: -1,
        };
        ledgerListPage({
                ...page
        approveProcessListPage({
                ...page,approveType: 0,...searchForm.value
            })
            .then((res) => {
                ledgerList.value = res.records;
                total.value = res.total;
                ledgerList.value = res.data.records;
            })
            .catch(() => {
                // tableLoading.value = false;
@@ -118,23 +156,58 @@
            },
        });
    };
    // æ ¼å¼åŒ–回款方式
    const formatReceiptType = (params) => {
        if (params == 0) {
            return "待审核";
        } else if (params == 1) {
            return "审核中";
        } else if (params == 2) {
            return "审核完成";
        } else if (params == 4) {
            return "已重新提交";
        } else {
            return '不通过';
        }
    };
    // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
    const getTagClass = (type) => {
        if (type == 0) {
            return "warning";
        } else if (type == 1) {
            return "primary";
        } else if (type == 2) {
            return "success";
        } else if (type == 4) {
            return "primary";
        } else {
            return "danger";
        }
    };
    // ç‚¹å‡»åˆ—表项
    const handleItemClick = (item) => {
        uni.showToast({
            title: `查看合同: ${item.contractId}`,
            icon: "none",
        // ä½¿ç”¨æœ¬åœ°å­˜å‚¨ä¼ é€’数据
        uni.setStorageSync('invoiceLedgerEditRow', JSON.stringify(item));
        uni.navigateTo({
            url: `/pages/cooperativeOffice/collaborativeApproval/detail?operationType=edit&approveId=${item.approveId}`,
        });
    };
    // æ·»åŠ æ–°è®°å½•
    const handleAdd = () => {
        uni.navigateTo({
            url: "/pages/cooperativeOffice/collaborativeApproval/detail",
            url: "/pages/cooperativeOffice/collaborativeApproval/detail?operationType=add",
        });
    };
    // ç‚¹å‡»å®¡æ ¸
    const approve = (item) => {
        uni.navigateTo({
            url: `/pages/cooperativeOffice/collaborativeApproval/approve?approveId=${item.approveId}`
        })
    }
    onMounted(() => {
    onShow(() => {
        // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
        getList();
    });
@@ -150,7 +223,27 @@
        background: #f8f9fa;
        position: relative;
    }
    .search-input {
        flex: 1;
        background: #f5f5f5;
        border-radius: 24px;
        padding: 10px 16px;
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .search-text {
        flex: 1;
        font-size: 14px;
        color: #333;
        background: transparent;
        border: none;
        outline: none;
    }
    .search-text::placeholder {
        color: #999;
    }
    .search-filter-section {
@@ -163,17 +256,16 @@
        align-items: center;
        gap: 12px;
    }
    .search-input {
        flex: 1;
        background: #f5f5f5;
        border-radius: 24px;
        padding: 4px 16px;
        padding: 10px 16px;
        display: flex;
        align-items: center;
        gap: 8px;
    }
    .search-text {
        flex: 1;
        font-size: 14px;
@@ -182,7 +274,7 @@
        border: none;
        outline: none;
    }
    .search-text::placeholder {
        color: #999;
    }
@@ -239,7 +331,6 @@
    }
    .item-tag {
        background: #4caf50;
        border-radius: 4px;
        padding: 2px 4px;
    }
@@ -253,16 +344,27 @@
    .item-details {
        padding: 16px 0;
    }
    .detail-row {
        display: flex;
        align-items: flex-end;
        justify-content: space-between;
        margin-bottom: 8px;
        &:last-child {
            margin-bottom: 0;
        }
    }
    .detail-row-user {
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
    .detail-row-approveReason {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 8px;
    }
    .detail-info {
@@ -316,4 +418,50 @@
        box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
        z-index: 1000;
    }
    .approver-value {
        display: flex;
        justify-content: flex-end;
    }
    .approver-chip {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        background: #f0f6ff;
        color: #2b7cff;
        border: 1px solid #e0efff;
        border-radius: 999px;
        padding: 4px 10px;
        max-width: 100%;
    }
    .approver-name {
        font-size: 12px;
        color: #2b7cff;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .actions {
        display: flex;
        gap: 10px;
        align-items: center;
        justify-content: flex-end;
    }
    .action-btn {
        border-radius: 16px;
        height: 28px;
        line-height: 28px;
        padding: 0 12px;
    }
    .action-btn.edit {
        /* primary æ ·å¼æ¥è‡ªç»„件,这里保留钩子以便后续需要扩展 */
    }
    .action-btn.approve {
        /* success æ ·å¼æ¥è‡ªç»„件,这里保留钩子以便后续需要扩展 */
    }
    :deep(.van-floating-bubble) {
        background: #ed8d05;
    }
</style>
src/pages/equipmentManagement/ledger/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,408 @@
<template>
    <view class="ledger-detail">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader :title="operationType === 'edit' ? '编辑设备台账' : '新增设备台账'" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <van-form @submit="sendForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <van-cell-group title="基本信息" inset>
                <van-field
                    v-model="form.deviceName"
                    label="设备名称"
                    placeholder="请输入设备名称"
                    :rules="formRules.deviceName"
                    required
                    clearable
                />
                <van-field
                    v-model="form.deviceModel"
                    label="规格型号"
                    placeholder="请输入规格型号"
                    :readonly="form.deviceModel != null && operationType === 'edit'"
                    :rules="formRules.deviceModel"
                    required
                    clearable
                />
                <van-field
                    v-model="form.supplierName"
                    label="供应商"
                    required
                    placeholder="请输入供应商"
                    :rules="formRules.supplierName"
                    clearable
                />
                <van-field
                    v-model="form.unit"
                    label="单位"
                    required
                    placeholder="请输入单位"
                    :rules="formRules.unit"
                    clearable
                />
                <van-field
                    v-model="form.taxRate"
                    required
                    label="税率(%)"
                    placeholder="请选择"
                    readonly
                    :rules="formRules.taxRate"
                    @click="showTaxRatePicker"
                    clearable
                />
                <van-field
                    v-model="form.number"
                    label="数量"
                    required
                    type="number"
                    placeholder="请输入数量"
                    :rules="formRules.number"
                    @blur="mathNum"
                    clearable
                />
                <van-field
                    v-model="form.taxIncludingPriceUnit"
                    label="含税单价"
                    required
                    type="number"
                    placeholder="请输入含税单价"
                    :rules="formRules.taxIncludingPriceUnit"
                    @blur="mathNum"
                    clearable
                />
                <van-field
                    v-model="form.taxIncludingPriceTotal"
                    label="含税总价"
                    placeholder="自动生成"
                    readonly
                />
                <van-field
                    v-model="form.unTaxIncludingPriceTotal"
                    label="不含税总价"
                    placeholder="自动生成"
                    readonly
                />
                <van-field
                    v-model="form.createTime"
                    label="录入日期"
                    placeholder="请选择"
                    readonly
                    @click="showDatePicker"
                    required
                    clearable
                />
            </van-cell-group>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">保存</van-button>
            </view>
        </van-form>
        <!-- ç¨ŽçŽ‡é€‰æ‹©å™¨ -->
        <van-popup v-model:show="showTaxRate" position="bottom">
            <van-picker
                :model-value="taxRatePickerValue"
                :columns="taxRateOptions"
                @confirm="onTaxRateConfirm"
                @cancel="showTaxRate = false"
            />
        </van-popup>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDate" position="bottom">
            <van-date-picker
                v-model="currentDate"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
        </van-popup>
    </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { getLedgerById, addLedger, editLedger } from '@/api/equipmentManagement/ledger';
import dayjs from "dayjs";
import {
    calculateTaxIncludeTotalPrice,
    calculateTaxExclusiveTotalPrice,
} from "@/utils/summarizeTable";
import { showToast } from 'vant';
defineOptions({
    name: "设备台账表单",
});
// è¡¨å•引用
const formRef = ref(null);
const operationType = ref('');
const loading = ref(false);
const showTaxRate = ref(false);
const taxRatePickerValue = ref([]);
const showDate = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
// è¡¨å•验证规则
const formRules = {
    deviceName: [{ required: true, trigger: "blur", message: "请输入" }],
    deviceModel: [{ required: true, trigger: "blur", message: "请输入" }],
    supplierName: [{ required: true, trigger: "blur", message: "请输入" }],
    unit: [{ required: true, trigger: "blur", message: "请输入" }],
    number: [{ required: true, trigger: "blur", message: "请输入" }],
    taxIncludingPriceUnit: [{ required: true, trigger: "blur", message: "请输入" }],
    taxRate: [{ required: true, trigger: "change", message: "请输入" }],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    deviceName: undefined, // è®¾å¤‡åç§°
    deviceModel: undefined, // è§„格型号
    supplierName: undefined, // ä¾›åº”商
    unit: undefined, // å•位
    number: undefined, // æ•°é‡
    taxIncludingPriceUnit: undefined, // å«ç¨Žå•ä»·
    taxIncludingPriceTotal: undefined, // å«ç¨Žæ€»ä»·
    taxRate: undefined, // ç¨Žçއ
    unTaxIncludingPriceTotal: undefined, // ä¸å«ç¨Žæ€»ä»·
    createTime: dayjs().format("YYYY-MM-DD"), // å½•入日期
});
// ç¨Žçއ选项
const taxRateOptions = computed(() => {
    return [
        { text: '1', value: 1 },
        { text: '6', value: 6 },
        { text: '13', value: 13 }
    ]
});
// åŠ è½½è¡¨å•æ•°æ®
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit';
    }
    try {
        const { code, data } = await getLedgerById(id);
        if (code == 200) {
            form.value.deviceName = data.deviceName;
            form.value.deviceModel = data.deviceModel;
            form.value.supplierName = data.supplierName;
            form.value.unit = data.unit;
            form.value.number = data.number;
            form.value.taxIncludingPriceUnit = data.taxIncludingPriceUnit;
            form.value.taxIncludingPriceTotal = data.taxIncludingPriceTotal;
            form.value.taxRate = data.taxRate;
            form.value.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal;
            form.value.createTime = data.createTime;
        }
    } catch (e) {
        showToast('获取详情失败');
    }
};
// æ•°å­¦è®¡ç®—
const mathNum = () => {
    if (!form.value.taxIncludingPriceUnit) {
        showToast("请输入单价");
        return;
    }
    if (!form.value.number) {
        showToast("请输入数量");
        return;
    }
    form.value.taxIncludingPriceTotal = calculateTaxIncludeTotalPrice(
        form.value.taxIncludingPriceUnit,
        form.value.number
    );
    if (form.value.taxRate) {
        form.value.unTaxIncludingPriceTotal = calculateTaxExclusiveTotalPrice(
            form.value.taxIncludingPriceTotal,
            form.value.taxRate
        );
    }
};
// æ¸…除表单校验状态
const clearValidate = () => {
    formRef.value?.clearValidate();
};
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        deviceName: undefined,
        deviceModel: undefined,
        supplierName: undefined,
        unit: undefined,
        number: undefined,
        taxIncludingPriceUnit: undefined,
        taxIncludingPriceTotal: undefined,
        taxRate: undefined,
        unTaxIncludingPriceTotal: undefined,
        createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    };
};
const resetFormAndValidate = () => {
    resetForm();
    clearValidate();
};
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        await formRef.value?.validate();
        loading.value = true;
        const id = getPageId();
        // å‡†å¤‡æäº¤æ•°æ®ï¼ŒcreateTime åŠ ä¸Šå½“å‰æ—¶åˆ†ç§’
        const submitData = { ...form.value };
        if (submitData.createTime && !submitData.createTime.includes(':')) {
            // å¦‚æžœ createTime åªåŒ…含日期,添加当前时分秒
            submitData.createTime = submitData.createTime + ' ' + dayjs().format('HH:mm:ss');
        }
        const { code } = id
            ? await editLedger({ id: id, ...submitData })
            : await addLedger(submitData);
        if (code == 200) {
            showToast("操作成功");
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('表单验证失败');
    }
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// èŽ·å–é¡µé¢å‚æ•°
const getPageParams = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    if (options.id) {
        // ç¼–辑模式,获取详情
        loadForm(options.id);
    } else {
        // æ–°å¢žæ¨¡å¼
        operationType.value = 'add';
    }
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    return options.id;
};
// æ˜¾ç¤ºç¨ŽçŽ‡é€‰æ‹©å™¨
const showTaxRatePicker = () => {
    showTaxRate.value = true;
};
// ç¡®è®¤ç¨ŽçŽ‡é€‰æ‹©
const onTaxRateConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.taxRate = selectedOptions[0].value;
    taxRatePickerValue.value = selectedValues;
    showTaxRate.value = false;
    mathNum(); // é‡æ–°è®¡ç®—
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    // åªä¿å­˜å¹´æœˆæ—¥ï¼Œä¸åŒ…含时分秒
    form.value.createTime = selectedValues.join('-');
    currentDate.value = selectedValues;
    showDate.value = false;
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
});
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–å‚æ•°
    getPageParams();
});
</script>
<style scoped lang="scss">
.ledger-detail {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
</style>
src/pages/equipmentManagement/ledger/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,359 @@
<template>
  <view class="device-ledger">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="设备台账" @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-filter-section">
      <view class="search-bar">
        <view class="search-input">
          <input
            class="search-text"
            placeholder="请输入设备名称"
            v-model="searchKeyword"
            confirm-type="search"
            @confirm="getList"
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- è®¾å¤‡å°è´¦åˆ—表 -->
    <view class="ledger-list" v-if="ledgerList.length > 0">
      <view v-for="(item, index) in ledgerList" :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">设备名称:{{ item.deviceName }}</text>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.deviceModel || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">供应商</text>
              <text class="detail-value">{{ item.supplierName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">单位</text>
              <text class="detail-value">{{ item.unit || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">数量</text>
              <text class="detail-value">{{ item.number || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">含税单价</text>
              <text class="detail-value highlight">{{ item.taxIncludingPriceUnit || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">含税总价</text>
              <text class="detail-value highlight">{{ item.taxIncludingPriceTotal || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">税率</text>
              <text class="detail-value">{{ item.taxRate || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">不含税总价</text>
              <text class="detail-value highlight">{{ item.unTaxIncludingPriceTotal || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">录入人</text>
              <text class="detail-value">{{ item.createUser || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">录入日期</text>
              <text class="detail-value">{{ item.createTime || '-' }}</text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸï¼Œå‚考 invoiceLedger çš„æ ·å¼ -->
          <view class="action-buttons">
            <van-button
              type="primary"
              size="small"
              class="action-btn"
              @click="edit(item.id)"
            >
              ç¼–辑
            </van-button>
            <van-button
              type="danger"
              size="small"
              plain
              class="action-btn"
              @click="deleteRow(item.id)"
            >
              åˆ é™¤
            </van-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无设备台账数据</text>
    </view>
    <!-- æµ®åŠ¨æ–°å¢žæŒ‰é’® -->
    <view class="fab-button" @click="add">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getLedgerPage, delLedger } from '@/api/equipmentManagement/ledger'
import useUserStore from "@/store/modules/user"
import { showToast } from 'vant';
const userStore = useUserStore()
// æœç´¢å…³é”®è¯
const searchKeyword = ref('')
// è®¾å¤‡å°è´¦æ•°æ®
const ledgerList = ref([])
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æŸ¥è¯¢åˆ—表(current/size å›ºå®šä¼  -1)
const getList = () => {
  const params = {
    current: -1,
    size: -1,
    deviceName: searchKeyword.value || undefined,
  }
  getLedgerPage(params)
    .then((res) => {
      ledgerList.value = res.records || res.data?.records || []
    })
    .catch(() => {
      showToast('获取数据失败')
    })
}
// æ–°å¢ž - è·³è½¬åˆ°è¯¦æƒ…页面
const add = () => {
  uni.navigateTo({
    url: '/pages/equipmentManagement/ledger/detail'
  })
}
// ç¼–辑 - è·³è½¬åˆ°è¯¦æƒ…页面
const edit = (id) => {
  if (!id) return
  uni.navigateTo({
    url: `/pages/equipmentManagement/ledger/detail?id=${id}`
  })
}
// åˆ é™¤
const deleteRow = async (id) => {
  if (!id) return
  uni.showModal({
    title: '提示',
    content: '此操作将永久删除该记录, æ˜¯å¦ç»§ç»­?',
    success: async (res) => {
      if (!res.confirm) return
      try {
        await delLedger(id)
        showToast('删除成功')
        getList()
      } catch (e) {
        showToast('删除失败')
      }
    }
  })
}
onMounted(() => {
  getList()
})
onShow(() => {
  getList()
})
</script>
<style scoped lang="scss">
.u-divider {
  margin: 0 !important;
}
.device-ledger {
  min-height: 100vh;
  background: #f8f9fa;
  position: relative;
  padding-bottom: 80px;
}
.search-filter-section {
  padding: 10px 20px;
  background: #ffffff;
}
.search-bar {
  display: flex;
  align-items: center;
  gap: 12px;
}
.search-input {
  flex: 1;
  background: #f5f5f5;
  border-radius: 24px;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.search-text {
  flex: 1;
  font-size: 14px;
  color: #333;
  background: transparent;
  border: none;
  outline: none;
}
.search-text::placeholder {
  color: #999;
}
.filter-button {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ledger-list {
  padding: 20px;
}
.ledger-item {
  background: #ffffff;
  border-radius: 12px;
  margin-bottom: 16px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  padding: 0 16px;
}
.item-header {
  padding: 16px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.item-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.document-icon {
  width: 24px;
  height: 24px;
  background: #2979ff;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.item-id {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
.item-details {
  padding: 16px 0;
}
.detail-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 8px;
  &:last-child {
    margin-bottom: 0;
  }
}
.detail-label {
  font-size: 12px;
  color: #777777;
  min-width: 60px;
}
.detail-value {
  font-size: 12px;
  color: #000000;
  text-align: right;
  flex: 1;
  margin-left: 16px;
}
.detail-value.highlight {
  color: #2979ff;
  font-weight: 500;
}
.no-data {
  padding: 40px 0;
  text-align: center;
  color: #999;
}
// æŒ‰é’®æ ·å¼ï¼Œå‚考 invoiceLedger
.action-buttons {
  display: flex;
  gap: 12px;
  padding: 0 0 16px 0;
  justify-content: space-between;
}
.action-btn {
  flex: 1;
}
.fab-button {
  position: fixed;
  bottom: calc(30px + env(safe-area-inset-bottom));
  right: 30px;
  width: 56px;
  height: 56px;
  background: #2979ff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
  z-index: 1000;
}
</style>
src/pages/equipmentManagement/repair/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,437 @@
<template>
    <view class="repair-add">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader :title="operationType === 'edit' ? '编辑报修' : '新增报修'" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <van-form @submit="sendForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <van-cell-group title="基本信息" inset>
                <van-field
                    v-model="deviceNameText"
                    label="设备名称"
                    placeholder="请选择设备名称"
                    :rules="formRules.deviceLedgerId"
                    required
                    readonly
                    @click="showDevicePicker"
                    clearable
                >
                    <template #right-icon>
                        <van-icon name="scan" @click.stop="startScan" class="scan-icon" />
                    </template>
                </van-field>
                <van-field
                    v-model="form.deviceModel"
                    label="规格型号"
                    placeholder="请输入规格型号"
                    readonly
                    clearable
                />
                <van-field
                    v-model="form.repairTime"
                    label="报修日期"
                    placeholder="请选择报修日期"
                    :rules="formRules.repairTime"
                    required
                    readonly
                    @click="showDatePicker"
                    clearable
                />
                <van-field
                    v-model="form.repairName"
                    label="报修人"
                    placeholder="请输入报修人"
                    :rules="formRules.repairName"
                    required
                    clearable
                />
                <van-field
                    v-model="form.remark"
                    label="故障现象"
                    type="textarea"
                    rows="3"
                    placeholder="请输入故障现象"
                    :rules="formRules.remark"
                    required
                    clearable
                    maxlength="200"
                    show-word-limit
                />
            </van-cell-group>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">保存</van-button>
            </view>
        </van-form>
        <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDevice" position="bottom">
            <van-picker
                :model-value="devicePickerValue"
                :columns="deviceColumns"
                @confirm="onDeviceConfirm"
                @cancel="showDevice = false"
            />
        </van-popup>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDate" position="bottom">
            <van-date-picker
                v-model="currentDate"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
        </van-popup>
    </view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
import { addRepair, editRepair, getRepairById } from '@/api/equipmentManagement/repair';
import dayjs from "dayjs";
import { showToast } from 'vant';
defineOptions({
    name: "设备报修表单",
});
// è¡¨å•引用
const formRef = ref(null);
const operationType = ref('add');
const loading = ref(false);
const showDevice = ref(false);
const devicePickerValue = ref([]);
const showDate = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
// è®¾å¤‡é€‰é¡¹
const deviceOptions = ref([]);
const deviceNameText = ref('');
// æ‰«ç ç›¸å…³çŠ¶æ€
const isScanning = ref(false);
const scanTimer = ref(null);
// è¡¨å•验证规则
const formRules = {
    deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
    repairTime: [{ required: true, trigger: "change", message: "请选择报修日期" }],
    repairName: [{ required: true, trigger: "blur", message: "请输入报修人" }],
    remark: [{ required: true, trigger: "blur", message: "请输入故障现象" }],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸ
    repairName: undefined, // æŠ¥ä¿®äºº
    remark: undefined, // æ•…障现象
});
// è®¾å¤‡é€‰æ‹©å™¨åˆ—
const deviceColumns = computed(() => {
    return deviceOptions.value.map(item => ({
        text: item.deviceName,
        value: item.id
    }));
});
// åŠ è½½è®¾å¤‡åˆ—è¡¨
const loadDeviceName = async () => {
    try {
        const { data } = await getDeviceLedger();
        deviceOptions.value = data || [];
    } catch (e) {
        showToast('获取设备列表失败');
    }
};
// è®¾ç½®è®¾å¤‡è§„格型号
const setDeviceModel = (id) => {
    const option = deviceOptions.value.find((item) => item.id === id);
    if (option) {
        form.value.deviceModel = option.deviceModel;
        deviceNameText.value = option.deviceName;
    }
};
// åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit';
        try {
            const { code, data } = await getRepairById(id);
            if (code == 200) {
                form.value.deviceLedgerId = data.deviceLedgerId;
                form.value.deviceModel = data.deviceModel;
                form.value.repairTime = data.repairTime;
                form.value.repairName = data.repairName;
                form.value.remark = data.remark;
                // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
                const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
                if (device) {
                    deviceNameText.value = device.deviceName;
                }
            }
        } catch (e) {
            showToast('获取详情失败');
        }
    } else {
        // æ–°å¢žæ¨¡å¼
        operationType.value = 'add';
    }
};
// æ¸…除表单校验状态
const clearValidate = () => {
    formRef.value?.clearValidate();
};
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        deviceLedgerId: undefined,
        deviceModel: undefined,
        repairTime: dayjs().format("YYYY-MM-DD"),
        repairName: undefined,
        remark: undefined,
    };
    deviceNameText.value = '';
};
const resetFormAndValidate = () => {
    resetForm();
    clearValidate();
};
// æ‰«æäºŒç»´ç åŠŸèƒ½
const startScan = () => {
    if (isScanning.value) {
        showToast('正在扫描中,请稍候...');
        return;
    }
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
        scanType: ['qrCode', 'barCode'],
        success: (res) => {
            handleScanResult(res.result);
        },
        fail: (err) => {
            console.error('扫码失败:', err);
            showToast('扫码失败,请重试');
        }
    });
};
// å¤„理扫码结果
const handleScanResult = (scanResult) => {
    if (!scanResult) {
        showToast('扫码结果为空');
        return;
    }
    isScanning.value = true;
    showToast('扫码成功,3秒后自动填充设备信息');
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
        processScanResult(scanResult);
        isScanning.value = false;
    }, 3000);
};
// å¤„理扫码结果并匹配设备
const processScanResult = (scanResult) => {
    // åœ¨è®¾å¤‡åˆ—表中查找匹配的设备
    // å‡è®¾äºŒç»´ç å†…容是设备名称或设备编号
    const matchedDevice = deviceOptions.value.find(device =>
        device.deviceName === scanResult ||
        device.deviceCode === scanResult ||
        device.id.toString() === scanResult
    );
    if (matchedDevice) {
        // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
        form.value.deviceLedgerId = matchedDevice.id;
        deviceNameText.value = matchedDevice.deviceName;
        form.value.deviceModel = matchedDevice.deviceModel;
        showToast('设备信息已自动填充');
    } else {
        // æœªæ‰¾åˆ°åŒ¹é…çš„设备
        showToast('未找到匹配的设备,请手动选择');
    }
};
// æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
const showDevicePicker = () => {
    showDevice.value = true;
};
// ç¡®è®¤è®¾å¤‡é€‰æ‹©
const onDeviceConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.deviceLedgerId = selectedOptions[0].value;
    devicePickerValue.value = selectedValues;
    showDevice.value = false;
    setDeviceModel(selectedOptions[0].value);
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    form.value.repairTime = selectedValues.join('-');
    currentDate.value = selectedValues;
    showDate.value = false;
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
});
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
});
// ç»„件卸载时清理定时器
onUnmounted(() => {
    if (scanTimer.value) {
        clearTimeout(scanTimer.value);
    }
});
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        await formRef.value?.validate();
        loading.value = true;
        const id = getPageId();
        // å‡†å¤‡æäº¤æ•°æ®
        const submitData = { ...form.value };
        const { code } = id
            ? await editRepair({ id: id, ...submitData })
            : await addRepair(submitData);
        if (code == 200) {
            showToast(`${id ? "编辑" : "新增"}报修成功`);
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('表单验证失败');
    }
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// èŽ·å–é¡µé¢å‚æ•°
const getPageParams = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (options.id) {
        // ç¼–辑模式,获取详情
        loadForm(options.id);
    } else {
        // æ–°å¢žæ¨¡å¼
        loadForm();
    }
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    return options.id;
};
</script>
<style scoped lang="scss">
.repair-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
.scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
}
</style>
src/pages/equipmentManagement/repair/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
<template>
  <view class="device-repair">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="设备报修" @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-filter-section">
      <view class="search-bar">
        <view class="search-input">
          <input
            class="search-text"
            placeholder="请输入设备名称"
            v-model="searchKeyword"
            confirm-type="search"
            @confirm="getList"
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- è®¾å¤‡æŠ¥ä¿®åˆ—表 -->
    <view class="repair-list" v-if="repairList.length > 0">
      <view v-for="(item, index) in repairList" :key="index">
        <view class="repair-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">设备名称:{{ item.deviceName }}</text>
            </view>
            <view class="status-tag">
              <van-tag v-if="item.status === 1" type="success">完结</van-tag>
              <van-tag v-if="item.status === 0" type="danger">待维修</van-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.deviceModel || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">报修日期</text>
              <text class="detail-value">{{ formatDate(item.repairTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">报修人</text>
              <text class="detail-value">{{ item.repairName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障现象</text>
              <text class="detail-value">{{ item.remark || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修人</text>
              <text class="detail-value">{{ item.maintenanceName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修结果</text>
              <text class="detail-value">{{ item.maintenanceResult || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">维修日期</text>
              <text class="detail-value">{{ formatDate(item.maintenanceTime) || '-' }}</text>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <van-button
              type="primary"
              size="small"
              class="action-btn"
              @click="edit(item.id)"
            >
              ç¼–辑
            </van-button>
            <van-button
              type="warning"
              size="small"
              class="action-btn"
              :disabled="item.status === 1"
              @click="addMaintain(item.id)"
            >
              æ–°å¢žç»´ä¿®
            </van-button>
            <van-button
              type="danger"
              size="small"
              plain
              class="action-btn"
              @click="delRepairByIds(item.id)"
            >
              åˆ é™¤
            </van-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无设备报修数据</text>
    </view>
    <!-- æµ®åŠ¨æ°”æ³¡æŒ‰é’® -->
    <van-floating-bubble
      axis="xy"
      icon="plus"
      @click="addRepair"
    />
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getRepairPage, delRepair } from '@/api/equipmentManagement/repair'
import useUserStore from "@/store/modules/user"
import { showToast } from 'vant';
const userStore = useUserStore()
// æœç´¢å…³é”®è¯
const searchKeyword = ref('')
// è®¾å¤‡æŠ¥ä¿®æ•°æ®
const repairList = ref([])
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æ ¼å¼åŒ–日期
const formatDate = (dateStr) => {
  if (!dateStr) return ''
  const date = new Date(dateStr)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}
// æŸ¥è¯¢åˆ—表
const getList = () => {
  const params = {
    current: -1,
    size: -1,
    deviceName: searchKeyword.value || undefined
  }
  getRepairPage(params)
    .then((res) => {
      repairList.value = res.records || res.data?.records || []
    })
    .catch(() => {
      showToast('获取数据失败')
    })
}
// æ–°å¢žç»´ä¿® - è·³è½¬åˆ°ç»´ä¿®é¡µé¢
const addMaintain = (id) => {
  if (!id) {
    showToast('参数错误')
    return
  }
  uni.navigateTo({
    url: `/pages/equipmentManagement/repair/maintain?id=${id}`
  })
}
// æ–°å¢žæŠ¥ä¿® - è·³è½¬åˆ°æŠ¥ä¿®é¡µé¢
const addRepair = () => {
  uni.navigateTo({
    url: '/pages/equipmentManagement/repair/add'
  })
}
// ç¼–辑 - è·³è½¬åˆ°add页面,通过id区分新增还是编辑
const edit = (id) => {
  if (!id) return
  uni.navigateTo({
    url: `/pages/equipmentManagement/repair/add?id=${id}`
  })
}
// åˆ é™¤æŠ¥ä¿®æ•°æ®
const delRepairByIds = async (ids) => {
  uni.showModal({
    title: '警告',
    content: '确认删除报修数据, æ­¤æ“ä½œä¸å¯é€†?',
    confirmText: '确定',
    cancelText: '取消',
    success: async (res) => {
      if (!res.confirm) return
      try {
        const response = await delRepair(ids)
        if (response.code === 200) {
          showToast('删除成功')
          getList()
        } else {
          showToast('删除失败')
        }
      } catch (e) {
        showToast('删除失败')
      }
    }
  })
}
onMounted(() => {
  getList()
})
onShow(() => {
  getList()
})
</script>
<style scoped lang="scss">
.u-divider {
  margin: 0 !important;
}
.device-repair {
  min-height: 100vh;
  background: #f8f9fa;
  position: relative;
  padding-bottom: 80px;
}
.search-filter-section {
  padding: 10px 20px;
  background: #ffffff;
}
.search-bar {
  display: flex;
  align-items: center;
  gap: 12px;
}
.search-input {
  flex: 1;
  background: #f5f5f5;
  border-radius: 24px;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.search-text {
  flex: 1;
  font-size: 14px;
  color: #333;
  background: transparent;
  border: none;
  outline: none;
}
.search-text::placeholder {
  color: #999;
}
.filter-button {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.repair-list {
  padding: 20px;
}
.repair-item {
  background: #ffffff;
  border-radius: 12px;
  margin-bottom: 16px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  padding: 0 16px;
}
.item-header {
  padding: 16px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.item-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.document-icon {
  width: 24px;
  height: 24px;
  background: #2979ff;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.item-id {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
.status-tag {
  display: flex;
  align-items: center;
}
.item-details {
  padding: 16px 0;
}
.detail-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 8px;
  &:last-child {
    margin-bottom: 0;
  }
}
.detail-label {
  font-size: 12px;
  color: #777777;
  min-width: 60px;
}
.detail-value {
  font-size: 12px;
  color: #000000;
  text-align: right;
  flex: 1;
  margin-left: 16px;
}
.detail-value.highlight {
  color: #2979ff;
  font-weight: 500;
}
.no-data {
  padding: 40px 0;
  text-align: center;
  color: #999;
}
.action-buttons {
  display: flex;
  gap: 8px;
  padding: 0 0 16px 0;
  justify-content: space-between;
}
.action-btn {
  flex: 1;
}
</style>
src/pages/equipmentManagement/repair/maintain.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,267 @@
<template>
    <view class="repair-maintain">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="新增维修" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <van-form @submit="sendForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <van-cell-group title="维修信息" inset>
                <van-field
                    v-model="form.maintenanceName"
                    label="维修人"
                    placeholder="请输入维修人"
                    :rules="formRules.maintenanceName"
                    required
                    clearable
                />
                <van-field
                    v-model="form.maintenanceResult"
                    label="维修结果"
                    type="textarea"
                    rows="3"
                    placeholder="请输入维修结果"
                    :rules="formRules.maintenanceResult"
                    required
                    clearable
                    maxlength="200"
                    show-word-limit
                />
                <van-field
                    v-model="form.maintenanceTime"
                    label="维修日期"
                    placeholder="请选择维修日期"
                    :rules="formRules.maintenanceTime"
                    required
                    readonly
                    @click="showDatePicker"
                    clearable
                />
            </van-cell-group>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">保存</van-button>
            </view>
        </van-form>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDate" position="bottom">
            <van-date-picker
                v-model="currentDate"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
        </van-popup>
    </view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { addMaintain } from '@/api/equipmentManagement/repair';
import useUserStore from "@/store/modules/user";
import dayjs from "dayjs";
import { showToast } from 'vant';
defineOptions({
    name: "设备维修表单",
});
const userStore = useUserStore();
// è¡¨å•引用
const formRef = ref(null);
const loading = ref(false);
const showDate = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
// è¡¨å•验证规则
const formRules = {
    maintenanceName: [{ required: true, trigger: "blur", message: "请输入维修人" }],
    maintenanceResult: [{ required: true, trigger: "blur", message: "请输入维修结果" }],
    maintenanceTime: [{ required: true, trigger: "change", message: "请选择维修日期" }],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    maintenanceName: userStore.nickName || '', // é»˜è®¤ä½¿ç”¨å½“前用户昵称
    maintenanceResult: undefined, // ç»´ä¿®ç»“æžœ
    maintenanceTime: dayjs().format("YYYY-MM-DD"), // ç»´ä¿®æ—¥æœŸï¼ˆåªæ˜¾ç¤ºæ—¥æœŸï¼‰
});
// æ¸…除表单校验状态
const clearValidate = () => {
    // Vant4中不需要手动清除验证状态,重置表单时会自动清除
    // formRef.value?.clearValidate(); // åˆ é™¤è¿™è¡Œ
};
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        maintenanceName: userStore.nickName || '',
        maintenanceResult: undefined,
        maintenanceTime: dayjs().format("YYYY-MM-DD"),
    };
};
const resetFormAndValidate = () => {
    resetForm();
    // clearValidate(); // åˆ é™¤è¿™è¡Œï¼ŒVant4会自动处理
};
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // ä½¿ç”¨Vant4的正确验证方式
        formRef.value?.validate().then(() => {
            // éªŒè¯é€šè¿‡
            submitFormData();
        }).catch((errors) => {
            // éªŒè¯å¤±è´¥
            showToast('请填写完整信息');
        });
    } catch (e) {
        showToast('表单验证失败');
    }
};
// æäº¤è¡¨å•数据
const submitFormData = async () => {
    try {
        loading.value = true;
        const id = getPageId();
        if (!id) {
            showToast('参数错误');
            loading.value = false;
            return;
        }
        // å‡†å¤‡æäº¤æ•°æ®ï¼ŒmaintenanceTime åŠ ä¸Šå½“å‰æ—¶åˆ†ç§’
        const submitData = { ...form.value };
        if (submitData.maintenanceTime && !submitData.maintenanceTime.includes(':')) {
            // å¦‚æžœ maintenanceTime åªåŒ…含日期,添加当前时分秒
            submitData.maintenanceTime = submitData.maintenanceTime + ' ' + dayjs().format('HH:mm:ss');
        }
        const { code } = await addMaintain({ id: id, ...submitData });
        if (code == 200) {
            showToast('新增维修成功');
            resetFormAndValidate();
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('操作失败');
    }
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    return options.id;
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    // åªä¿å­˜å¹´æœˆæ—¥ï¼Œä¸åŒ…含时分秒
    form.value.maintenanceTime = selectedValues.join('-');
    currentDate.value = selectedValues;
    showDate.value = false;
};
// åˆå§‹åŒ–表单数据
const initForm = () => {
    // è®¾ç½®ç»´ä¿®äººä¸ºå½“前用户昵称
    form.value.maintenanceName = userStore.nickName || '';
    // è®¾ç½®å½“前日期(只包含年月日)
    form.value.maintenanceTime = dayjs().format('YYYY-MM-DD');
    currentDate.value = [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()];
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆå§‹åŒ–表单
    initForm();
});
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–è¡¨å•
    initForm();
});
</script>
<style scoped lang="scss">
.repair-maintain {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
</style>
src/pages/equipmentManagement/upkeep/add.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,413 @@
<template>
    <view class="upkeep-add">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <van-form @submit="sendForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <van-cell-group title="基本信息" inset>
                <van-field
                    v-model="deviceNameText"
                    label="设备名称"
                    placeholder="请选择设备名称"
                    :rules="formRules.deviceLedgerId"
                    required
                    readonly
                    @click="showDevicePicker"
                    clearable
                >
                    <template #right-icon>
                        <van-icon name="scan" @click.stop="startScan" class="scan-icon" />
                    </template>
                </van-field>
                <van-field
                    v-model="form.deviceModel"
                    label="规格型号"
                    placeholder="请输入规格型号"
                    readonly
                    clearable
                />
                <van-field
                    v-model="form.maintenancePlanTime"
                    label="计划保养日期"
                    placeholder="请选择计划保养日期"
                    :rules="formRules.maintenancePlanTime"
                    required
                    readonly
                    @click="showDatePicker"
                    clearable
                />
            </van-cell-group>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">保存</van-button>
            </view>
        </van-form>
        <!-- è®¾å¤‡é€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDevice" position="bottom">
            <van-picker
                :model-value="devicePickerValue"
                :columns="deviceColumns"
                @confirm="onDeviceConfirm"
                @cancel="showDevice = false"
            />
        </van-popup>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDate" position="bottom">
            <van-date-picker
                v-model="currentDate"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
        </van-popup>
    </view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
import { addUpkeep, editUpkeep, getUpkeepById } from '@/api/equipmentManagement/upkeep';
import dayjs from "dayjs";
import { showToast } from 'vant';
defineOptions({
    name: "设备保养计划表单",
});
// è¡¨å•引用
const formRef = ref(null);
const operationType = ref('add');
const loading = ref(false);
const showDevice = ref(false);
const devicePickerValue = ref([]);
const showDate = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
// è®¾å¤‡é€‰é¡¹
const deviceOptions = ref([]);
const deviceNameText = ref('');
// æ‰«ç ç›¸å…³çŠ¶æ€
const isScanning = ref(false);
const scanTimer = ref(null);
// è¡¨å•验证规则
const formRules = {
    deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
    maintenancePlanTime: [{ required: true, trigger: "change", message: "请选择计划保养日期" }],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    deviceLedgerId: undefined, // è®¾å¤‡ID
    deviceModel: undefined, // è§„格型号
    maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // è®¡åˆ’保养日期
});
// è®¾å¤‡é€‰æ‹©å™¨åˆ—
const deviceColumns = computed(() => {
    return deviceOptions.value.map(item => ({
        text: item.deviceName,
        value: item.id
    }));
});
// åŠ è½½è®¾å¤‡åˆ—è¡¨
const loadDeviceName = async () => {
    try {
        const { data } = await getDeviceLedger();
        deviceOptions.value = data || [];
    } catch (e) {
        showToast('获取设备列表失败');
    }
};
// è®¾ç½®è®¾å¤‡è§„格型号
const setDeviceModel = (id) => {
    const option = deviceOptions.value.find((item) => item.id === id);
    if (option) {
        form.value.deviceModel = option.deviceModel;
        deviceNameText.value = option.deviceName;
    }
};
// åŠ è½½è¡¨å•æ•°æ®ï¼ˆç¼–è¾‘æ¨¡å¼ï¼‰
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit';
        try {
            const { code, data } = await getUpkeepById(id);
            if (code == 200) {
                form.value.deviceLedgerId = data.deviceLedgerId;
                form.value.deviceModel = data.deviceModel;
                form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
                // è®¾ç½®è®¾å¤‡åç§°æ˜¾ç¤º
                const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
                if (device) {
                    deviceNameText.value = device.deviceName;
                }
            }
        } catch (e) {
            showToast('获取详情失败');
        }
    } else {
        // æ–°å¢žæ¨¡å¼
        operationType.value = 'add';
    }
};
// æ¸…除表单校验状态
const clearValidate = () => {
    formRef.value?.clearValidate();
};
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        deviceLedgerId: undefined,
        deviceModel: undefined,
        maintenancePlanTime: dayjs().format("YYYY-MM-DD"),
    };
    deviceNameText.value = '';
};
const resetFormAndValidate = () => {
    resetForm();
    clearValidate();
};
// æ‰«æäºŒç»´ç åŠŸèƒ½
const startScan = () => {
    if (isScanning.value) {
        showToast('正在扫描中,请稍候...');
        return;
    }
    // è°ƒç”¨uni-app的扫码API
    uni.scanCode({
        scanType: ['qrCode', 'barCode'],
        success: (res) => {
            handleScanResult(res.result);
        },
        fail: (err) => {
            console.error('扫码失败:', err);
            showToast('扫码失败,请重试');
        }
    });
};
// å¤„理扫码结果
const handleScanResult = (scanResult) => {
    if (!scanResult) {
        showToast('扫码结果为空');
        return;
    }
    isScanning.value = true;
    showToast('扫码成功,3秒后自动填充设备信息');
    // 3秒后处理扫码结果
    scanTimer.value = setTimeout(() => {
        processScanResult(scanResult);
        isScanning.value = false;
    }, 3000);
};
// å¤„理扫码结果并匹配设备
const processScanResult = (scanResult) => {
    // åœ¨è®¾å¤‡åˆ—表中查找匹配的设备
    // å‡è®¾äºŒç»´ç å†…容是设备名称或设备编号
    const matchedDevice = deviceOptions.value.find(device =>
        device.deviceName === scanResult ||
        device.deviceCode === scanResult ||
        device.id.toString() === scanResult
    );
    if (matchedDevice) {
        // æ‰¾åˆ°åŒ¹é…çš„设备,自动填充
        form.value.deviceLedgerId = matchedDevice.id;
        deviceNameText.value = matchedDevice.deviceName;
        form.value.deviceModel = matchedDevice.deviceModel;
        showToast('设备信息已自动填充');
    } else {
        // æœªæ‰¾åˆ°åŒ¹é…çš„设备
        showToast('未找到匹配的设备,请手动选择');
    }
};
// æ˜¾ç¤ºè®¾å¤‡é€‰æ‹©å™¨
const showDevicePicker = () => {
    showDevice.value = true;
};
// ç¡®è®¤è®¾å¤‡é€‰æ‹©
const onDeviceConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.deviceLedgerId = selectedOptions[0].value;
    devicePickerValue.value = selectedValues;
    showDevice.value = false;
    setDeviceModel(selectedOptions[0].value);
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    form.value.maintenancePlanTime = selectedValues.join('-');
    currentDate.value = selectedValues;
    showDate.value = false;
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶èŽ·å–å‚æ•°
    getPageParams();
});
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶èŽ·å–è®¾å¤‡åˆ—è¡¨å’Œå‚æ•°
    loadDeviceName();
    getPageParams();
});
// ç»„件卸载时清理定时器
onUnmounted(() => {
    if (scanTimer.value) {
        clearTimeout(scanTimer.value);
    }
});
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // æ‰‹åŠ¨éªŒè¯è¡¨å•
        await formRef.value?.validate();
        loading.value = true;
        const id = getPageId();
        // å‡†å¤‡æäº¤æ•°æ®
        const submitData = { ...form.value };
        // ç¡®ä¿æ—¥æœŸæ ¼å¼æ­£ç¡®
        if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
            submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
        }
        const { code } = id
            ? await editUpkeep({ id: id, ...submitData })
            : await addUpkeep(submitData);
        if (code == 200) {
            showToast(`${id ? "编辑" : "新增"}计划成功`);
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('表单验证失败');
    }
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// èŽ·å–é¡µé¢å‚æ•°
const getPageParams = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    // æ ¹æ®æ˜¯å¦æœ‰id参数来判断是新增还是编辑
    if (options.id) {
        // ç¼–辑模式,获取详情
        loadForm(options.id);
    } else {
        // æ–°å¢žæ¨¡å¼
        loadForm();
    }
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    return options.id;
};
</script>
<style scoped lang="scss">
.upkeep-add {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
.scan-icon {
    color: #1989fa;
    font-size: 18px;
    margin-left: 8px;
    cursor: pointer;
}
</style>
src/pages/equipmentManagement/upkeep/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,438 @@
<template>
  <view class="device-upkeep">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="设备保养" @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-filter-section">
      <view class="search-bar">
        <view class="search-input">
          <input
            class="search-text"
            placeholder="请输入设备名称"
            v-model="searchKeyword"
            confirm-type="search"
            @confirm="getList"
          />
        </view>
        <view class="filter-button" @click="getList">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- è®¾å¤‡ä¿å…»åˆ—表 -->
    <view class="upkeep-list" v-if="upkeepList.length > 0">
      <view v-for="(item, index) in upkeepList" :key="index">
        <view class="upkeep-item" @click="toggleSelection(item)">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">设备名称:{{ item.deviceName }}</text>
            </view>
            <view class="status-tag">
              <van-tag v-if="item.status === 1" type="success">完结</van-tag>
              <van-tag v-if="item.status === 0" type="danger">待保养</van-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">规格型号</text>
              <text class="detail-value">{{ item.deviceModel || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">计划保养日期</text>
              <text class="detail-value">{{ formatDate(item.maintenancePlanTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">录入人</text>
              <text class="detail-value">{{ item.createUserName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">录入日期</text>
              <text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养人</text>
              <text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">实际保养日期</text>
              <text class="detail-value">{{ formatDateTime(item.maintenanceActuallyTime) || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">保养结果</text>
              <view class="detail-value">
                <van-tag v-if="item.maintenanceResult === 1" type="success">
                  å®Œå¥½
                </van-tag>
                <van-tag v-if="item.maintenanceResult === 0" type="danger">
                  ç»´ä¿®
                </van-tag>
                <text v-if="item.maintenanceResult === undefined || item.maintenanceResult === null">-</text>
              </view>
            </view>
          </view>
          <!-- æŒ‰é’®åŒºåŸŸ -->
          <view class="action-buttons">
            <van-button
              type="primary"
              size="small"
              class="action-btn"
              @click.stop="edit(item.id)"
            >
              ç¼–辑
            </van-button>
            <van-button
              type="warning"
              size="small"
              class="action-btn"
              :disabled="item.status === 1"
              @click.stop="addMaintain(item.id)"
            >
              ä¿å…»
            </van-button>
            <van-button
              type="danger"
              size="small"
              plain
              class="action-btn"
              @click.stop="delUpkeepByIds(item.id)"
            >
              åˆ é™¤
            </van-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无设备保养数据</text>
    </view>
    <!-- æµ®åŠ¨æ°”æ³¡æŒ‰é’® -->
    <van-floating-bubble
      axis="xy"
      icon="plus"
      @click="addPlan"
    />
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getUpkeepPage, delUpkeep } from '@/api/equipmentManagement/upkeep'
import useUserStore from "@/store/modules/user"
import { showToast } from 'vant';
import dayjs from "dayjs"
const userStore = useUserStore()
// æœç´¢å…³é”®è¯
const searchKeyword = ref('')
// è®¾å¤‡ä¿å…»æ•°æ®
const upkeepList = ref([])
// å¤šé€‰åˆ—表
const multipleList = ref([])
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æ ¼å¼åŒ–日期
const formatDate = (dateStr) => {
  if (!dateStr) return ''
  return dayjs(dateStr).format("YYYY-MM-DD")
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateStr) => {
  if (!dateStr) return ''
  return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss")
}
// æŸ¥è¯¢åˆ—表
const getList = () => {
  const params = {
    current: -1,
    size: -1,
    deviceName: searchKeyword.value || undefined
  }
  getUpkeepPage(params)
    .then((res) => {
      // å¦‚æžœres.data不是数组,设置为空数组
      upkeepList.value = res.records || res.data?.records || []
    })
    .catch(() => {
      showToast('获取数据失败')
    })
}
// åˆ‡æ¢é€‰æ‹©çŠ¶æ€
const toggleSelection = (item) => {
  const index = multipleList.value.findIndex(selected => selected.id === item.id)
  if (index > -1) {
    multipleList.value.splice(index, 1)
  } else {
    multipleList.value.push(item)
  }
}
// æ£€æŸ¥æ˜¯å¦å·²é€‰æ‹©
const isSelected = (item) => {
  return multipleList.value.some(selected => selected.id === item.id)
}
// æ–°å¢žä¿å…» - è·³è½¬åˆ°ä¿å…»é¡µé¢
const addMaintain = (id) => {
  if (!id && multipleList.value.length !== 1) {
    showToast('请选择一条记录')
    return
  }
  const targetId = id || multipleList.value[0].id
  uni.navigateTo({
    url: `/pages/equipmentManagement/upkeep/maintain?id=${targetId}`
  })
}
// æ–°å¢žè®¡åˆ’ - è·³è½¬åˆ°æ–°å¢žé¡µé¢
const addPlan = () => {
  uni.navigateTo({
    url: '/pages/equipmentManagement/upkeep/add'
  })
}
// ç¼–辑 - è·³è½¬åˆ°add页面,通过id区分新增还是编辑
const edit = (id) => {
  if (!id) return
  uni.navigateTo({
    url: `/pages/equipmentManagement/upkeep/add?id=${id}`
  })
}
// åˆ é™¤ä¿å…»æ•°æ®
const delUpkeepByIds = async (ids) => {
  const deleteIds = Array.isArray(ids) ? ids : [ids]
  if (deleteIds.length === 0) {
    showToast('请选择要删除的记录')
    return
  }
  uni.showModal({
    title: '警告',
    content: '确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?',
    confirmText: '确定',
    cancelText: '取消',
    success: async (res) => {
      if (!res.confirm) return
      try {
        // é€ä¸ªåˆ é™¤
        for (const id of deleteIds) {
          const response = await delUpkeep(id)
          if (response.code !== 200) {
            showToast('删除失败')
            return
          }
        }
        showToast('删除成功')
        multipleList.value = []
        getList()
      } catch (e) {
        showToast('删除失败')
      }
    }
  })
}
onMounted(() => {
  getList()
})
onShow(() => {
  getList()
})
</script>
<style scoped lang="scss">
.u-divider {
  margin: 0 !important;
}
.device-upkeep {
  min-height: 100vh;
  background: #f8f9fa;
  position: relative;
  padding-bottom: 80px;
}
.search-filter-section {
  padding: 10px 20px;
  background: #ffffff;
}
.search-bar {
  display: flex;
  align-items: center;
  gap: 12px;
}
.search-input {
  flex: 1;
  background: #f5f5f5;
  border-radius: 24px;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.search-text {
  flex: 1;
  font-size: 14px;
  color: #333;
  background: transparent;
  border: none;
  outline: none;
}
.search-text::placeholder {
  color: #999;
}
.filter-button {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.action-section {
  padding: 10px 20px;
  background: #ffffff;
  border-bottom: 1px solid #f0f0f0;
}
.action-buttons {
  display: flex;
  gap: 8px;
  justify-content: flex-start;
}
.upkeep-list {
  padding: 20px;
}
.upkeep-item {
  background: #ffffff;
  border-radius: 12px;
  margin-bottom: 16px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  padding: 0 16px;
}
.item-header {
  padding: 16px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.item-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.checkbox-wrapper {
  display: flex;
  align-items: center;
}
.document-icon {
  width: 24px;
  height: 24px;
  background: #2979ff;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.item-id {
  font-size: 14px;
  color: #333;
  font-weight: 500;
}
.status-tag {
  display: flex;
  align-items: center;
}
.item-details {
  padding: 16px 0;
}
.detail-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 8px;
  &:last-child {
    margin-bottom: 0;
  }
}
.detail-label {
  font-size: 12px;
  color: #777777;
  min-width: 80px;
}
.detail-value {
  font-size: 12px;
  color: #000000;
  text-align: right;
  flex: 1;
  margin-left: 16px;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.detail-value.highlight {
  color: #2979ff;
  font-weight: 500;
}
.no-data {
  padding: 40px 0;
  text-align: center;
  color: #999;
}
.upkeep-item .action-buttons {
  display: flex;
  gap: 8px;
  padding: 0 0 16px 0;
  justify-content: space-between;
}
.action-btn {
  flex: 1;
}
</style>
src/pages/equipmentManagement/upkeep/maintain.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,298 @@
<template>
    <view class="upkeep-maintain">
        <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
        <PageHeader title="新增保养" @back="goBack" />
        <!-- è¡¨å•内容 -->
        <van-form @submit="sendForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center">
            <!-- åŸºæœ¬ä¿¡æ¯ -->
            <van-cell-group title="保养信息" inset>
                <van-field
                    v-model="form.maintenanceActuallyName"
                    label="实际保养人"
                    placeholder="请输入实际保养人"
                    :rules="formRules.maintenanceActuallyName"
                    required
                    clearable
                />
                <van-field
                    v-model="form.maintenanceActuallyTime"
                    label="实际保养日期"
                    placeholder="请选择实际保养日期"
                    :rules="formRules.maintenanceActuallyTime"
                    required
                    readonly
                    @click="showDatePicker"
                    clearable
                />
                <van-field
                    v-model="maintenanceResultText"
                    label="保养结果"
                    placeholder="请选择保养结果"
                    :rules="formRules.maintenanceResult"
                    required
                    readonly
                    @click="showResultPicker"
                    clearable
                />
            </van-cell-group>
            <!-- æäº¤æŒ‰é’® -->
            <view class="footer-btns">
                <van-button class="cancel-btn" @click="goBack">取消</van-button>
                <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">保存</van-button>
            </view>
        </van-form>
        <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
        <van-popup v-model:show="showDate" position="bottom">
            <van-date-picker
                v-model="currentDate"
                title="选择日期"
                @confirm="onDateConfirm"
                @cancel="showDate = false"
            />
        </van-popup>
        <!-- ä¿å…»ç»“果选择器 -->
        <van-popup v-model:show="showResult" position="bottom">
            <van-picker
                :model-value="resultPickerValue"
                :columns="resultColumns"
                @confirm="onResultConfirm"
                @cancel="showResult = false"
            />
        </van-popup>
    </view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
import { addMaintenance } from '@/api/equipmentManagement/upkeep';
import useUserStore from "@/store/modules/user";
import dayjs from "dayjs";
import { showToast } from 'vant';
defineOptions({
    name: "设备保养表单",
});
const userStore = useUserStore();
// è¡¨å•引用
const formRef = ref(null);
const loading = ref(false);
const showDate = ref(false);
const showResult = ref(false);
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
const resultPickerValue = ref([]);
const maintenanceResultText = ref('');
// ä¿å…»ç»“果选项
const resultColumns = [
    { text: '完好', value: 1 },
    { text: 'ç»´ä¿®', value: 0 }
];
// è¡¨å•验证规则
const formRules = {
    maintenanceActuallyName: [{ required: true, trigger: "blur", message: "请输入实际保养人" }],
    maintenanceActuallyTime: [{ required: true, trigger: "change", message: "请选择实际保养日期" }],
    maintenanceResult: [{ required: true, trigger: "change", message: "请选择保养结果" }],
};
// ä½¿ç”¨ ref å£°æ˜Žè¡¨å•数据
const form = ref({
    maintenanceActuallyName: userStore.nickName || '', // é»˜è®¤ä½¿ç”¨å½“前用户昵称
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    maintenanceActuallyTime: dayjs().format("YYYY-MM-DD"), // å®žé™…保养日期(只显示日期)
});
// æ¸…除表单校验状态
const clearValidate = () => {
    // Vant4中不需要手动清除验证状态,重置表单时会自动清除
    // formRef.value?.clearValidate(); // åˆ é™¤è¿™è¡Œ
};
// é‡ç½®è¡¨å•数据和校验状态
const resetForm = () => {
    form.value = {
        maintenanceActuallyName: userStore.nickName || '',
        maintenanceResult: undefined,
        maintenanceActuallyTime: dayjs().format("YYYY-MM-DD"),
    };
    maintenanceResultText.value = '';
};
const resetFormAndValidate = () => {
    resetForm();
    // clearValidate(); // åˆ é™¤è¿™è¡Œï¼ŒVant4会自动处理
};
// æäº¤è¡¨å•
const sendForm = async () => {
    try {
        // ä½¿ç”¨Vant4的正确验证方式
        formRef.value?.validate().then(() => {
            // éªŒè¯é€šè¿‡
            submitFormData();
        }).catch((errors) => {
            // éªŒè¯å¤±è´¥
            showToast('请填写完整信息');
        });
    } catch (e) {
        showToast('表单验证失败');
    }
};
// æäº¤è¡¨å•数据
const submitFormData = async () => {
    try {
        loading.value = true;
        const id = getPageId();
        if (!id) {
            showToast('参数错误');
            loading.value = false;
            return;
        }
        // å‡†å¤‡æäº¤æ•°æ®ï¼ŒmaintenanceActuallyTime åŠ ä¸Šå½“å‰æ—¶åˆ†ç§’
        const submitData = { ...form.value };
        if (submitData.maintenanceActuallyTime && !submitData.maintenanceActuallyTime.includes(':')) {
            // å¦‚æžœ maintenanceActuallyTime åªåŒ…含日期,添加当前时分秒
            submitData.maintenanceActuallyTime = submitData.maintenanceActuallyTime + ' ' + dayjs().format('HH:mm:ss');
        }
        const { code } = await addMaintenance({ id: id, ...submitData });
        if (code == 200) {
            showToast('新增保养成功');
            resetFormAndValidate();
            setTimeout(() => {
                uni.navigateBack();
            }, 1500);
        } else {
            loading.value = false;
        }
    } catch (e) {
        loading.value = false;
        showToast('操作失败');
    }
};
// è¿”回上一页
const goBack = () => {
    uni.navigateBack();
};
// èŽ·å–é¡µé¢ID
const getPageId = () => {
    const pages = getCurrentPages();
    const currentPage = pages[pages.length - 1];
    const options = currentPage.options;
    return options.id;
};
// æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
const showDatePicker = () => {
    showDate.value = true;
};
// ç¡®è®¤æ—¥æœŸé€‰æ‹©
const onDateConfirm = ({ selectedValues }) => {
    // åªä¿å­˜å¹´æœˆæ—¥ï¼Œä¸åŒ…含时分秒
    form.value.maintenanceActuallyTime = selectedValues.join('-');
    currentDate.value = selectedValues;
    showDate.value = false;
};
// æ˜¾ç¤ºä¿å…»ç»“果选择器
const showResultPicker = () => {
    showResult.value = true;
};
// ç¡®è®¤ä¿å…»ç»“果选择
const onResultConfirm = ({ selectedValues, selectedOptions }) => {
    form.value.maintenanceResult = selectedOptions[0].value;
    maintenanceResultText.value = selectedOptions[0].text;
    resultPickerValue.value = selectedValues;
    showResult.value = false;
};
// åˆå§‹åŒ–表单数据
const initForm = () => {
    // è®¾ç½®ä¿å…»äººä¸ºå½“前用户昵称
    form.value.maintenanceActuallyName = userStore.nickName || '';
    // è®¾ç½®å½“前日期(只包含年月日)
    form.value.maintenanceActuallyTime = dayjs().format('YYYY-MM-DD');
    currentDate.value = [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()];
};
onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆå§‹åŒ–表单
    initForm();
});
onMounted(() => {
    // é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–è¡¨å•
    initForm();
});
</script>
<style scoped lang="scss">
.upkeep-maintain {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 5rem;
}
.footer-btns {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0.75rem 0;
    box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
    z-index: 1000;
}
.cancel-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 6.375rem;
    background: #C7C9CC;
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
.save-btn {
    font-weight: 400;
    font-size: 1rem;
    color: #FFFFFF;
    width: 14rem;
    background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
    box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
    border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
}
// å“åº”式调整
@media (max-width: 768px) {
    .submit-section {
        padding: 12px;
    }
}
.tip-text {
    padding: 4px 16px 0 16px;
    font-size: 12px;
    color: #888;
}
</style>
src/pages/index.vue
@@ -11,8 +11,8 @@
        <view class="hero-section">
            <view class="bg-img">
                <view class="hero-content">
                    <text class="hero-title">产品库存管理系统</text>
                    <text class="hero-subtitle">高效、便捷的业务管理入口</text>
                    <text class="hero-title"></text>
                    <text class="hero-subtitle"></text>
                </view>
                <view class="hero-wave"></view>
            </view>
@@ -355,9 +355,24 @@
                url: '/pages/cooperativeOffice/collaborativeApproval/index'
            });
            break;
                    case '客户拜访':
        case '客户拜访':
            uni.navigateTo({
                url: '/pages/cooperativeOffice/clientVisit/index'
            });
            break;
        case '设备台账':
            uni.navigateTo({
                url: '/pages/equipmentManagement/ledger/index'
            });
            break;
        case '设备报修':
            uni.navigateTo({
                url: '/pages/equipmentManagement/repair/index'
            });
            break;
        case '设备保养':
            uni.navigateTo({
                url: '/pages/equipmentManagement/upkeep/index'
            });
            break;
        default:
@@ -494,7 +509,8 @@
.bg-img {
    width: 100%;
    height: 8.75rem;
    background: linear-gradient(135deg, #2979ff 0%, #1565c0 100%);
    background-image: url("../static/images/banner/view-background.png");
    background-size: cover;
    border-radius: 0.75rem;
    position: relative;
    overflow: hidden;
src/static/images/banner/view-background.png