gaoluyang
8 天以前 69a5fcec5068f7f5d2e2cade4389651e73e26aef
1.协同审批开发联调
已修改4个文件
已添加10个文件
2581 ■■■■ 文件已修改
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 14 ●●●●● 补丁 | 查看 | 原始文档 | 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/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,6 +273,20 @@
      }
    },
    {
      "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": "客户拜访",
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>