4 天以前 b59518d5c068ae8e6e32cce90ccf281bf81dc54a
feat(collaborativeApproval): 添加用印审批功能支持

- 在审批流程组件中新增用印审批详情展示界面
- 添加用印申请类型的审批流程支持(类型9)
- 实现用印申请的附件上传、预览和下载功能
- 集成用印审批相关的API接口和服务
- 更新审批管理界面以支持用印审批类型显示
- 添加用印审批表单的重新申请和附件管理功能
- 修改开发环境API基础地址配置
已修改6个文件
388 ■■■■ 文件已修改
src/api/collaborativeApproval/sealManagement.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/sealManagement.js
@@ -38,6 +38,15 @@
  });
}
// 用印申请详情(按审批流程id)
export function getSealDetail(approveProcessId) {
  return request({
    url: "/sealApplicationManagement/detail",
    method: "get",
    params: { approveProcessId },
  });
}
// 新增印章申请
export function addSealApplication(data) {
  return request({
src/views/collaborativeApproval/approvalManagement/index.vue
@@ -203,6 +203,7 @@
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' },
];
// 审批类型名称映射
@@ -214,6 +215,7 @@
  5: '采购审批',
  6: '报价审批',
  7: '发货审批',
  9: '用印审批',
};
// 审批类型图标映射
@@ -225,6 +227,7 @@
  5: 'ShoppingCart',
  6: 'DocumentChecked',
  7: 'Van',
  9: 'Stamp',
};
// 审批类型颜色映射
@@ -236,6 +239,7 @@
  5: '#909399',
  6: '#9B59B6',
  7: '#1ABC9C',
  9: '#7D3C98',
};
// 头像颜色池
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -135,6 +135,43 @@
        </el-skeleton>
      </div>
      <!-- 用印审批:展示用印申请详情 -->
      <div v-if="isSealApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">用印详情</el-divider>
        <el-skeleton :loading="sealLoading" animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentSeal || !currentSeal.applicationNum" description="未查询到对应用印申请详情" />
            <template v-else>
              <el-descriptions :column="2" border>
                <el-descriptions-item label="申请编号">{{ currentSeal.applicationNum }}</el-descriptions-item>
                <el-descriptions-item label="申请标题">{{ currentSeal.title }}</el-descriptions-item>
                <el-descriptions-item label="用印类型">{{ getSealTypeText(currentSeal.sealType) }}</el-descriptions-item>
                <el-descriptions-item label="紧急程度">{{ getUrgencyText(currentSeal.urgency) }}</el-descriptions-item>
                <el-descriptions-item label="申请原因" :span="2">{{ currentSeal.reason }}</el-descriptions-item>
              </el-descriptions>
              <div v-if="currentSeal.commonFileList?.length" style="margin-top: 20px;">
                <h4>附件</h4>
                <el-table :data="currentSeal.commonFileList" border style="width: 100%">
                  <el-table-column label="附件名称" show-overflow-tooltip>
                    <template #default="scope">{{ scope.row.name }}</template>
                  </el-table-column>
                  <el-table-column label="操作" width="150" align="center">
                    <template #default="scope">
                      <el-button link type="primary" size="small" @click="previewSealFile(scope.row)">预览</el-button>
                      <el-button link type="primary" size="small" @click="downloadSealFile(scope.row)">下载</el-button>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
@@ -184,6 +221,7 @@
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
@@ -198,6 +236,8 @@
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
import { getSealDetail } from "@/api/collaborativeApproval/sealManagement.js";
import FilePreview from '@/components/filePreview/index.vue'
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
@@ -218,8 +258,12 @@
const currentQuotation = ref({})
const purchaseLoading = ref(false)
const currentPurchase = ref({})
const sealLoading = ref(false)
const currentSeal = ref({})
const filePreviewRef = ref()
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
const isSealApproval = computed(() => Number(props.approveType) === 9)
const data = reactive({
    form: {
@@ -315,6 +359,19 @@
    }
  }
  // 用印审批:按审批流程 DB 主键(row.id)查用印申请详情
  if (isSealApproval.value) {
    sealLoading.value = true
    getSealDetail(row.id).then((res) => {
      currentSeal.value = res.data || {}
    }).catch((err) => {
      console.error('查询用印详情失败:', err)
      proxy.$modal.msgError('查询用印详情失败')
    }).finally(() => {
      sealLoading.value = false
    })
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // 增加isApproval字段
@@ -339,6 +396,42 @@
        productOptions.value = res.data;
    });
};
// 用印类型文本
const getSealTypeText = (sealType) => {
  const map = { official: '公章', contract: '合同专用章', finance: '财务专用章', legal: '法人章' }
  return map[sealType] || sealType || '未知'
}
// 紧急程度文本
const getUrgencyText = (urgency) => {
  const map = { normal: '普通', urgent: '紧急', 'very-urgent': '特急' }
  return map[urgency] || urgency || '未知'
}
// 预览附件(URL 为完整地址,原样打开,不拼 host)
const previewSealFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.openUrl(url)
  } else {
    proxy.$modal.msgWarning('文件地址无效,无法预览')
  }
}
// 下载附件(锚点方式,URL 为完整地址)
const downloadSealFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.target = '_blank'
    a.rel = 'noopener noreferrer'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  } else {
    proxy.$modal.msgWarning('文件地址无效,无法下载')
  }
}
// 提交审批
const submitForm = (status) => {
  const filteredActivities = activities.value.filter(activity => activity.isShen);
@@ -367,6 +460,8 @@
  currentQuotation.value = {}
  purchaseLoading.value = false
  currentPurchase.value = {}
  sealLoading.value = false
  currentSeal.value = {}
  emit('close')
};
defineExpose({
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -70,7 +70,7 @@
          <el-button
            type="primary"
            @click="openForm('add')"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9"
            class="action-btn primary"
          >
            <el-icon><Plus /></el-icon>
@@ -84,7 +84,7 @@
            type="danger"
            plain
            @click="handleDelete"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 9"
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
@@ -159,6 +159,7 @@
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  { value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' },
];
// 当前审批类型信息
@@ -305,6 +306,7 @@
        currentApproveType.value === 5 ||
        currentApproveType.value === 6 ||
        currentApproveType.value === 7 ||
        currentApproveType.value === 9 ||
        row.approveStatus == 2 ||
        row.approveStatus == 1 ||
        row.approveStatus == 4
@@ -331,8 +333,8 @@
    },
  ];
  // 报价审批(类型 6)不展示"附件"操作
  if (!isQuotationType) {
  // 报价审批(类型 6)、用印审批(类型 9)不展示"附件"操作(附件走 common_file,不走 storage_attachment)
  if (!isQuotationType && currentApproveType.value !== 9) {
    actionOperations.push({
      name: "附件",
      type: "text",
@@ -405,6 +407,7 @@
    5: "/approveProcess/exportFive",
    6: "/approveProcess/exportSix",
    7: "/approveProcess/exportSeven",
    9: "/approveProcess/exportNine",
  }
  const url = urlMap[type] || urlMap[0]
  const nameMap = {
@@ -416,6 +419,7 @@
    5: "采购申请审批表",
    6: "报价审批表",
    7: "发货审批表",
    9: "用印审批表",
  }
  const fileName = nameMap[type] || nameMap[0]
  proxy.download(url, {}, `${fileName}.xlsx`)
src/views/collaborativeApproval/sealManagement/index.vue
@@ -7,7 +7,6 @@
        </div>
      </template>
   <!-- 用印申请管理 -->
        <div class="tab-content">
            <el-row :gutter="20" class="mb-20 ">
@@ -75,22 +74,39 @@
        <el-form-item label="申请原因" prop="reason">
          <el-input v-model="sealForm.reason" type="textarea" :rows="4" placeholder="请详细说明用印原因" />
        </el-form-item>
        <el-form-item label="审批人" prop="approveUserId">
          <el-select v-model="sealForm.approveUserId" placeholder="请选择审批人" style="width: 100%" filterable>
            <el-option
                v-for="user in userList"
                :key="user.userId"
                :label="user.nickName"
                :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
            <el-radio value="normal">普通</el-radio>
            <el-radio value="urgent">紧急</el-radio>
            <el-radio value="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            multiple
            :show-file-list="false"
            :action="sealUploadUrl"
            name="files"
            :headers="uploadHeaders"
            :limit="10"
            :before-upload="handleBeforeUpload"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            :on-exceed="handleUploadExceed"
          >
            <el-button type="primary">点击上传附件</el-button>
          </el-upload>
          <el-table v-if="sealForm.attachmentList.length" :data="sealForm.attachmentList" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">{{ scope.row.name }}</template>
            </el-table-column>
            <el-table-column label="操作" width="140" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="danger" size="small" @click="removeAttachment(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -119,21 +135,41 @@
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
        <!-- 附件列表 -->
        <div v-if="currentSealDetail.commonFileList?.length" class="attachment-section">
          <div class="attachment-title">附件列表:</div>
          <el-table :data="currentSealDetail.commonFileList" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </FormDialog>
    <!-- 文件预览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listSealApplication, addSealApplication, updateSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
import { userListNoPageByTenantId } from '@/api/system/user.js'
import { ElMessage } from 'element-plus'
import { listSealApplication, addSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
import useUserStore from '@/store/modules/user'
import { getToken } from '@/utils/auth'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FilePreview from '@/components/filePreview/index.vue'
// 响应式数据
// 用印申请相关
@@ -143,24 +179,27 @@
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const filePreviewRef = ref(null)
const sealFormRef = ref()
const userList = ref([])
// 附件上传(本地上传,非 minio,返回 [{ name, url }])
const sealUploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/uploadsList'
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
const sealForm = reactive({
  id: null,
  applicationNum: '',
  title: '',
  sealType: '',
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
  status: 'pending',
  attachmentList: []
})
const sealRules = {
  applicationNum: [{ required: true, message: '请输入申请编号', trigger: 'blur' }],
  title: [{ required: true, message: '请输入申请标题', trigger: 'blur' }],
  sealType: [{ required: true, message: '请选择用印类型', trigger: 'change' }],
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }],
  approveUserId: [{ required: true, message: '请选择审批人', trigger: 'change' }]
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
}
const sealSearchForm = reactive({
@@ -209,9 +248,9 @@
// 用印申请表格列配置(需在 getStatusText/getSealTypeText 等之后定义)
const sealTableColumn = ref([
  { label: '申请编号', prop: 'applicationNum',},
  { label: '申请编号', prop: 'applicationNum' },
  { label: '申请标题', prop: 'title', showOverflowTooltip: true },
  { label: '申请人', prop: 'createUserName', },
  { label: '申请人', prop: 'createUserName' },
  { label: '所属部门', prop: 'department', width: 150 },
  {
    label: '用印类型',
@@ -229,30 +268,19 @@
    formatData: (v) => getStatusText(v),
    formatType: (v) => getStatusType(v)
  },
  { label: '审批人', prop: 'approveUserName', width: 120 },
  {
    dataType: 'action',
    label: '操作',
    width: 250,
    width: 150,
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '查看', clickFun: (row) => viewSealDetail(row) },
      {
        name: '审批',
        clickFun: (row) => approveSeal(row),
        showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id
      },
      {
        name: '拒绝',
        clickFun: (row) => rejectSeal(row),
        showHide: (row) => row.status === 'pending' && row.approveUserId === userStore.id
      },
      {
        name: '重新申请',
        clickFun: (row) => reapplySeal(row),
        showHide: (row) => row.status === 'rejected' && row.createUser === userStore.id
      }
        showHide: (row) => row.status === 'rejected' && Number(userStore.id) === row.createUser
      },
      { name: '详情', clickFun: (row) => viewSealDetail(row) }
    ]
  }
])
@@ -261,8 +289,6 @@
const searchSealApplications = () => {
  page.current=1
  getSealApplicationList()
  // ElMessage.success('搜索完成')
}
// 重置印章申请搜索
const resetSealSearch = () => {
@@ -271,43 +297,52 @@
  sealSearchForm.applicationNum = ''
  searchSealApplications()
}
// 提交用印申请
// 重新申请用印
const reapplySeal = (row) => {
  // 预填表单数据
  Object.assign(sealForm, {
    id: row.id,
    applicationNum: row.applicationNum,
    title: row.title,
    sealType: row.sealType,
    reason: row.reason,
    urgency: row.urgency || 'normal',
    status: 'pending',
    attachmentList: row.commonFileList || []
  })
  showSealApplyDialog.value = true
}
// 提交用印申请(新增与重新申请均走 add 接口:重新申请会重新发起协同审批流程)
const submitSealApplication = async () => {
  try {
    await sealFormRef.value.validate()
    addSealApplication(sealForm).then(res => {
      if(res.code == 200){
        ElMessage.success('申请提交成功')
        ElMessage.success(sealForm.id ? '重新申请成功' : '申请提交成功')
        closeSealApplyDialog()
        getSealApplicationList()
        Object.assign(sealForm, {
        applicationNum: '',
        title: '',
        sealType: '',
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
      })
      }
    }).catch(err => {
      console.log(err.msg)
    })
  } catch (error) {
  }
}
// 关闭用印申请对话框
const closeSealApplyDialog = () => {
  // 清空表单数据
  Object.assign(sealForm, {
    id: null,
    applicationNum: '',
    title: '',
    sealType: '',
    reason: '',
    approveUserId: '',
    urgency: 'normal',
    status: 'pending'
    status: 'pending',
    attachmentList: []
  })
  // 清除表单验证状态
  if (sealFormRef.value) {
@@ -325,55 +360,64 @@
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// 审批用印申请
const approveSeal = (row) => {
  ElMessageBox.confirm('确认通过该用印申请?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    row.status = 'approved'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批通过')
        getSealApplicationList()
// 附件上传前校验(单个文件不超过 50MB)
const handleBeforeUpload = (file) => {
  if (file.size / 1024 / 1024 > 50) {
    ElMessage.error('文件大小不能超过 50MB')
    return false
      }
    })
  })
  return true
}
// 拒绝用印申请
const rejectSeal = (row) => {
  ElMessageBox.prompt('请输入拒绝原因', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    inputPattern: /\S+/,
    inputErrorMessage: '拒绝原因不能为空'
  }).then(({ value }) => {
    row.status = 'rejected'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('已拒绝申请')
        getSealApplicationList()
// 附件上传成功回调
const handleUploadSuccess = (res) => {
  if (res.code === 200) {
    const list = res.data || []
    list.forEach(item => sealForm.attachmentList.push({ name: item.name, url: item.url }))
    ElMessage.success('上传成功')
  } else {
    ElMessage.error(res.msg || '上传失败')
      }
    })
  })
}
// 附件上传失败回调
const handleUploadError = () => {
  ElMessage.error('附件上传失败')
}
// 附件数量超出
const handleUploadExceed = () => {
  ElMessage.warning('最多上传 10 个附件')
}
// 删除待提交附件
const removeAttachment = (index) => {
  sealForm.attachmentList.splice(index, 1)
}
// 预览文件(完整可访问 URL,直接打开,不拼 host)
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.openUrl(url)
  } else {
    ElMessage.warning('文件地址无效,无法预览')
  }
}
// 重新申请用印
const reapplySeal = (row) => {
  // 预填表单数据
  Object.assign(sealForm, {
    applicationNum: row.applicationNum,
    title: row.title,
    sealType: row.sealType,
    reason: row.reason,
    approveUserId: row.approveUserId,
    urgency: row.urgency || 'normal',
    status: 'pending'
  })
  showSealApplyDialog.value = true
// 下载文件
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.target = '_blank'
    a.rel = 'noopener noreferrer'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  } else {
    ElMessage.warning('文件地址无效,无法下载')
}
}
// 导出用印申请
const { proxy } = getCurrentInstance()
const handleExport = () => {
@@ -398,15 +442,6 @@
  page.size = obj.limit;
  getSealApplicationList();
};
// 监听对话框打开,获取用户列表
watch(showSealApplyDialog, (newVal) => {
  if (newVal) {
    userListNoPageByTenantId().then((res) => {
      userList.value = res.data;
    });
  }
});
onMounted(() => {
  // 路由携带 applicationNum 时,预填并查询
@@ -442,4 +477,19 @@
.ml-10 {
  margin-left: 10px;
}
.attachment-section {
  margin-top: 20px;
}
.attachment-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
}
.attachment-table {
  border-radius: 4px;
}
</style>
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9038"
          ? "http://192.168.0.10:7003"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"