让步接收审批前端代码

This commit is contained in:
shaoleiliu-netizen123 2026-06-09 10:46:41 +08:00
parent 57c7eb92eb
commit 34df51c8ce
5 changed files with 510 additions and 23 deletions

View File

@ -402,8 +402,31 @@ export const fileApi = {
return request({
url: '/sys/file/upload',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
data: formData
// headers: { 'Content-Type': 'multipart/form-data' }
})
},
uploadAndDingding(file: File, path?: string, groupId?: number | null): Promise<SysFile>{
const formData = new FormData()
formData.append('file', file)
if (path) {
formData.append('path', path)
}
if (groupId !== undefined && groupId !== null && groupId > 0) {
formData.append('groupId', groupId.toString())
}
return request({
url: '/sys/file/uploadAndDingding',
method: 'post',
data: formData
})
},
getApprovalSpaceId() {
return request({
url:'sys/file/getApprovalSpaceId',
method:'get',
})
},

View File

@ -125,6 +125,12 @@ service.interceptors.request.use(
if (userStore.token) {
config.headers['Authorization'] = userStore.token
}
if (config.data instanceof FormData) {
// 文件上传:不设置 JSON 请求头、不序列化,交给浏览器自动处理 multipart
return config
}
config.headers["Content-Type"] = 'application/json;charset=utf-8'
config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data
return config

View File

@ -49,13 +49,28 @@
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
size="small"
remote
:border="false"
:single-line="false"
striped
/>
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
>
<template #prefix>
{{ pagination.itemCount }}
</template>
</n-pagination>
</div>
</n-card>
<!-- 新增/编辑弹窗 -->
@ -125,13 +140,13 @@
</template>
</n-modal>
<!-- 审批纪律弹窗 -->
<n-modal v-model:show="recordModalVisible" preset="card" title="审批记录" style="width: 800px">
<!-- 审批记录弹窗 -->
<n-modal v-model:show="recordModalVisible" ref="exportWrapRef" preset="card" title="审批记录" style="width: 800px">
<n-card bordered style="width:100%;max-width:800px;padding:24px;position:relative;">
<div class="header-bar" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<span style="font-size:20sp;color:#666">审批编号: {{ detailModel?.businessId }}</span>
<div>
<n-icon style="font-size:22px;margin:0 6px;cursor:pointer" ><DownloadOutline /></n-icon>
<n-icon style="font-size:22px;margin:0 6px;cursor:pointer" @click="downloadOutline()" ><DownloadOutline /></n-icon>
</div>
</div>
<div style="padding:10px 0;">
@ -214,6 +229,12 @@ import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline,
import { concessionApplyApi, DetailModel, type ConcessionApply } from '@/api/concessionApply'
import { dictDataApi } from '@/api/org'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import * as dd from 'dingtalk-jsapi';
const message = useMessage()
const dialog = useDialog()
@ -551,6 +572,149 @@ const getRemark=(item) =>{
}
async function downloadOutline() {
const model = detailModel.value
if (!model) {
alert("暂无审批数据")
return
}
// 1. HTML
let recordHtml = ""
const recordList = model.approveRecord || []
recordList.forEach((item, index) => {
const remark = item.remark ?? ""
//
if (index === 0) {
recordHtml += `
<tr>
<td style="background: #f5f5f5;" rowspan="${recordList.length}">审批人</td>
<td colspan="3">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
${remark ? remark + "<br>" : ""}
${item.username ?? "未知人员"} ${item.operationResult ?? ""}
</div>
<span style="color: #888; white-space: nowrap;">${item.date ?? ""}</span>
</div>
</td>
</tr>
`
} else {
// rowspan
recordHtml += `
<tr>
<td colspan="3">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
${remark ? remark + "<br>" : ""}
${item.username ?? "未知人员"} ${item.operationResult ?? ""}
</div>
<span style="color: #888; white-space: nowrap;">${item.date ?? ""}</span>
</div>
</td>
</tr>
`
}
})
// 2.
let sealText = ""
let sealBorder = "#00b42a"
let sealColor = "#00b42a"
if (model.approveResult === "已通过") {
sealText = "已通过"
} else if (model.approveResult === "已驳回") {
sealText = "已驳回"
sealBorder = "#f53f3f"
sealColor = "#f53f3f"
} else if (model.approveResult === "已撤销") {
sealText = "已撤销"
sealBorder = "#ff7d00"
sealColor = "#ff7d00"
}
const createUser = recordList[0]?.username ?? ""
const nowTime = new Date().toLocaleString()
// 3. HTML
const html = `
<div style="width: 750px; margin: 0 auto; font-family: SimSun, serif; position: relative; background: #fff;">
<div style="text-align: center; font-size: 22px; font-weight: bold; margin-bottom: 20px;">通用审批</div>
<div style="display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 20px; align-items: center;">
<span>${model.title ?? ""}</span>
<span>创建时间${model.createTime ?? ""}</span>
</div>
<table border="1" cellpadding="8" cellspacing="0" style="width: 100%; border-collapse: collapse; font-size: 14px;">
<tr>
<td style="width: 120px; background: #f5f5f5;">审批编号</td>
<td colspan="3">${model.businessId ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">创建人</td>
<td>${createUser}</td>
<td style="width: 120px; background: #f5f5f5;">创建人部门</td>
<td>${model.deptName ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">申请内容</td>
<td colspan="3">${model.content ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">审批详情</td>
<td colspan="3">${model.approveResult ?? ""}</td>
</tr>
<tr>
<td style="background: #f5f5f5;">附件</td>
<td colspan="3"></td>
</tr>
${recordHtml}
</table>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-top: 20px;">
<span>打印时间${nowTime}</span>
<span>打印人${createUser}</span>
</div>
${sealText ? `
<div style="position: absolute; top: 100px; right: 20px; width: 100px; height: 100px; border: 3px solid ${sealBorder}; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: ${sealColor}; font-size: 20px; font-weight: bold; transform: rotate(15deg);">
${sealText}
</div>
` : ""}
</div>
`
// ========== PDF ==========
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html
tempDiv.style.position = 'absolute'
tempDiv.style.left = '-9999px'
tempDiv.style.top = '0'
document.body.appendChild(tempDiv)
try {
const canvas = await html2canvas(tempDiv, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
allowTaint: true
})
const imgData = canvas.toDataURL('image/jpeg', 1.0)
const pdf = new jsPDF('p', 'mm', 'a4')
const a4W = 210
const imgH = (canvas.height * a4W) / canvas.width
pdf.addImage(imgData, 'JPEG', 0, 0, a4W, imgH)
pdf.save(`通用审批_${model.businessId || "审批记录"}.pdf`)
} catch (err) {
console.error("导出失败:", err)
alert("导出PDF失败")
} finally {
document.body.removeChild(tempDiv)
}
}
onMounted(() => {
loadData()
loadDictOptions()

View File

@ -384,6 +384,7 @@ import {fileApi} from '@/api/system1'
import value = Language.value;
import {QcResultDetail} from "@/api/qcResultDetail.ts";
import router from "@/router";
import dd from 'dingtalk-jsapi'
const message = useMessage()
const dialog = useDialog()
@ -1014,20 +1015,25 @@ async function handleSubmitResultDetail() {
//
function submitBFResultDetail(row) {
Object.assign(formDetailOKData, reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 }))
Object.assign(formDetailScrapData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 }))
Object.assign(formDetailReworkData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 }))
Object.assign(formDetailConcessionData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 }))
// Object.assign(formDetailOKData, reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'',actionStatus:1 }))
// Object.assign(formDetailScrapData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'',actionStatus:1 }))
// Object.assign(formDetailReworkData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'',actionStatus:1 }))
// Object.assign(formDetailConcessionData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'',actionStatus:1 }))
if (row.traceType != 1) {
traceCodeShow.value = true
}
submitFlag.value = false
submitDetailModalVisible.value = true
formDetailOKData.qcId = row.id
formDetailScrapData.qcId = row.id
formDetailReworkData.qcId = row.id
formDetailConcessionData.qcId = row.id
// submitDetailModalVisible.value = true
// formDetailOKData.qcId = row.id
// formDetailScrapData.qcId = row.id
// formDetailReworkData.qcId = row.id
// formDetailConcessionData.qcId = row.id
//version-2
let path = `/qc/submitDetail/${row.qcNo}/${row.id}/${submitFlag.value}/${traceCodeShow.value}`
router.push(path)
}
@ -1058,8 +1064,14 @@ function handleCancelQuality(row) {
}
const isDingTalkEnv = () => {
// dd platform 'notInDingTalk'
return typeof dd !== 'undefined' && dd.env && dd.env.platform !== 'notInDingTalk';
};
//
async function handleUploadChange ({ file, fileList: list }: any) {
//
const allowExt = ['pdf','doc','docx','xls','xlsx','jpg','jpeg','png','gif','bmp']
const invalidFiles:any[] = []
@ -1081,7 +1093,7 @@ async function handleUploadChange ({ file, fileList: list }: any) {
fileList.value = list
console.log('选中的文件列表', fileList.value)
// pendingsuccess/error/removed
const validList = list.filter(item =>
item.status === 'pending' && item.file && item.status !== 'error'
)
@ -1096,19 +1108,20 @@ async function handleUploadChange ({ file, fileList: list }: any) {
const uploadPromises = validList.map(async (item: any) => {
try {
const res = await fileApi.upload(item.file, null, null)
const res = await fileApi.uploadAndDingding(item.file, null, null)
uploadFiles.value.push(res)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) {
targetFile.status = 'success'
targetFile.percentage = 100
}
}
} catch (error) {
console.error('文件上传失败', error)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) targetFile.status = 'error'
}
})
await Promise.all(uploadPromises)
}

View File

@ -0,0 +1,281 @@
<template>
<div class="page-container">
<n-card :title="title" style="margin-bottom: 16px">
<n-tabs type="card" animated>
<n-tab-pane name="合格" tab="合格">
<n-form ref="formRef" :model="formDetailOKData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailOKData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailOKData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailOKData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="报废" tab="报废">
<n-form ref="formRef" :model="formDetailScrapData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailScrapData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailScrapData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailScrapData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="返工" tab="返工">
<n-form ref="formRef" :model="formDetailReworkData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailReworkData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailReworkData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailReworkData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
<n-tab-pane name="让步接收" tab="让步接收">
<n-form ref="formRef" :model="formDetailConcessionData" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailConcessionData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailConcessionData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailConcessionData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="8" :cols="3">
<n-gi>
<n-form-item label="附件">
<n-upload
multiple
directory-dnd
:max="5"
:action="null"
:custom-request="()=>{}"
@change="handleUploadChange"
:file-list="fileList"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif,.bmp"
:max-size="10*1024*1024"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveIcon />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
仅支持 PDFDOCDOCXXLSXLSXJPGPNGGIF单文件最大10MB
</n-p>
</n-upload-dragger>
</n-upload>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-tab-pane>
</n-tabs>
<n-space justify="center" style="width: 100%; margin-top: 16px;">
<n-button size="small" @click="handleReset">重置</n-button>
<n-button type="primary" size="small" @click="handleSubmitResultDetail">提交</n-button>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import {
NButton, NSpace, NIcon, NUpload,NTag, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NDropdown
} from 'naive-ui'
import {useRoute} from "vue-router";
import {QcResultDetail} from "@/api/qcResultDetail.ts";
import {fileApi} from '@/api/system1'
import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting'
const message = useMessage()
const route = useRoute()
const qcId =ref( Number(route.params.id))
const traceCodeShow = ref(route.params.traceCodeShow === 'true')
const submitFlag = ref(route.params.submitFlag === 'true')
//
const fileList = ref<any[]>([])
const uploadFiles = ref<File[]>([])
const title = ref(`质检编号: ${route.params.qcNo}`)
const defaultFormDetailData:QcResultDetail = {
qcId: undefined,
resultType: undefined,
qty: 0,
traceCode: undefined,
defectCode: undefined,
handleAction: undefined,
actionStatus: undefined
}
const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 })
const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 })
const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 })
const formDetailConcessionData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 })
async function handleSubmitResultDetail() {
formDetailConcessionData.files = uploadFiles.value
const submitParam = [
formDetailOKData,
formDetailScrapData,
formDetailReworkData,
formDetailConcessionData]
submitFlag.value? await qualityTestingApi.submit(submitParam) : await qualityTestingApi.submitBF(submitParam)
message.success('提交成功')
}
async function handleUploadChange ({ file, fileList: list }: any) {
//
const allowExt = ['pdf','doc','docx','xls','xlsx','jpg','jpeg','png','gif','bmp']
const invalidFiles:any[] = []
list.forEach(item=>{
const name = item.file.name.toLowerCase()
const ext = name.split('.').pop()
if(!allowExt.includes(ext)){
invalidFiles.push(item.file.name)
item.status = 'error'
}
})
if(invalidFiles.length){
window.$message.error(`${invalidFiles.join('、')}】文件格式不支持`)
}
fileList.value = list
console.log('选中的文件列表', fileList.value)
const validList = list.filter(item =>
item.status === 'pending' && item.file && item.status !== 'error'
)
const successIds = list.filter(x=>x.status==='success' && x.file).map(x=>x.id)
//
uploadFiles.value = uploadFiles.value.filter(item=> successIds.includes(item.uid))
// pending
const pendingList = list.filter(item => item.status === 'pending' && item.file && item.status !== 'error')
if(pendingList.length === 0) return
const uploadPromises = validList.map(async (item: any) => {
try {
const res = await fileApi.uploadAndDingding(item.file, null, null)
uploadFiles.value.push(res)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) {
targetFile.status = 'success'
targetFile.percentage = 100
}
} catch (error) {
console.error('文件上传失败', error)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) targetFile.status = 'error'
}
})
await Promise.all(uploadPromises)
}
onMounted(()=>{
})
</script>
<style>
</style>