Files
klp-oa/klp-ui/src/views/hrm/requests/sealDetail.vue
砂糖 b0ee494434 feat(flow): 添加流程实例更新功能并禁用撤回操作
添加updateFlowInstance API用于更新流程实例
在所有详情页面禁用撤回功能
修改审批状态从pending到running
在抄送页面添加详情跳转功能
2026-01-05 14:38:22 +08:00

1002 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="request-page">
<el-card class="form-card" shadow="never">
<div slot="header" class="card-header">
<span>用印详情</span>
<div class="actions">
<el-button size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="loadDetail">刷新</el-button>
</div>
</div>
<div class="form-summary" v-loading="loading">
<div class="summary-left">
<div class="summary-title">{{ seal.sealType || '用印申请' }}</div>
<div class="summary-sub">申请编号{{ seal.bizId || '-' }} · 状态<el-tag size="mini" :type="statusType(seal.status)">{{ statusText(seal.status) }}</el-tag></div>
</div>
<div class="summary-right">
<div class="summary-item">
<div class="k">申请人</div>
<div class="v">{{ applicantText }}</div>
</div>
<div class="summary-item">
<div class="k">需要回执</div>
<div class="v">{{ seal.receiptRequired === 1 ? '是' : '否' }}</div>
</div>
</div>
</div>
<!-- 流程状态 -->
<!-- <div class="block-title">流程状态</div>
<el-card class="inner-card" shadow="never" v-loading="actionLoading">
<div v-if="flowInstance" class="flow-status">
<div class="status-item">
<div class="status-label">流程状态</div>
<div class="status-value">
<el-tag :type="statusType(flowInstance.status)" size="small">
{{ statusText(flowInstance.status) }}
</el-tag>
</div>
</div>
<div v-if="currentNode" class="status-item">
<div class="status-label">当前节点</div>
<div class="status-value">{{ currentNode.nodeName || currentNode.nodeId || '未知节点' }}</div>
</div>
<div v-if="currentTask" class="status-item">
<div class="status-label">当前审批人</div>
<div class="status-value">
<el-tag type="warning" size="small">{{ currentTask.assigneeUserName || currentTask.assigneeUserId || '待分配' }}</el-tag>
</div>
</div>
</div>
<div v-else class="empty">暂无流程信息</div>
</el-card> -->
<div class="block-title">用印信息</div>
<el-descriptions :column="2" border size="small" v-loading="loading">
<el-descriptions-item label="用印类型">{{ seal.sealType || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ applicantText }}</el-descriptions-item>
<el-descriptions-item label="用途说明" :span="2">{{ seal.purpose || '-' }}</el-descriptions-item>
<el-descriptions-item label="盖章页码">
<span v-if="stampForm.pageNo" class="page-no-text"> {{ stampForm.pageNo }} </span>
<span v-else class="text-muted">未指定</span>
</el-descriptions-item>
<el-descriptions-item label="需要回执">{{ seal.receiptRequired === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ seal.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="block-title">申请材料附件</div>
<el-card class="inner-card" shadow="never" v-loading="attachmentLoading">
<!-- 回执文件盖章后生成 -->
<div v-if="seal.receiptFileIds" class="receipt-panel">
<div class="receipt-title">回执文件</div>
<div class="receipt-actions">
<el-button size="mini" type="primary" plain @click="previewReceipt">预览回执</el-button>
<el-button size="mini" type="success" plain @click="downloadReceipt">下载回执</el-button>
</div>
<div class="hint-text" style="margin: 6px 0 0;">说明回执文件为盖章后生成的新PDF当前后端以 URL 形式存储在 receiptFileIds 字段</div>
</div>
<div v-if="attachmentList.length > 0" class="attachment-list">
<div v-for="file in attachmentList" :key="file.ossId" class="attachment-item">
<div class="file-info">
<i class="el-icon-document file-icon"></i>
<div class="file-details">
<div class="file-name">{{ file.originalName || file.fileName || `文件${file.ossId}` }}</div>
<div class="file-meta">
<span v-if="file.fileSize">{{ formatFileSize(file.fileSize) }}</span>
<span v-if="file.createTime" class="file-time">{{ formatDate(file.createTime) }}</span>
</div>
</div>
</div>
<div class="file-actions">
<el-button size="mini" type="text" @click="previewFile(file)">预览</el-button>
<el-button size="mini" type="text" @click="downloadFile(file.ossId)">下载</el-button>
</div>
</div>
</div>
<div v-else class="empty">暂无附件</div>
</el-card>
<!-- 盖章操作审批通过后显示 -->
<div v-if="canStamp" class="block-title">盖章操作</div>
<el-card v-if="canStamp" class="inner-card" shadow="never">
<div class="stamp-section">
<div class="stamp-config">
<el-form :model="stampForm" label-width="120px" size="small">
<el-form-item label="待盖章PDF">
<div v-if="targetPdfFile" class="pdf-file-info">
<i class="el-icon-document"></i>
<span>{{ targetPdfFile.originalName || targetPdfFile.fileName || 'PDF文件' }}</span>
<el-button size="mini" type="text" @click="openPdfPreview">预览PDF</el-button>
</div>
<div v-else class="empty">未找到待盖章PDF文件</div>
</el-form-item>
<el-form-item label="盖章页码">
<el-input-number
v-model="stampForm.pageNo"
:min="1"
:max="999"
controls-position="right"
style="width: 200px"
/>
<div class="hint-text">从第1页开始计数</div>
</el-form-item>
<el-form-item label="选择印章" prop="stampImageUrl">
<el-select v-model="stampForm.stampImageUrl" placeholder="选择印章" filterable style="width: 100%">
<el-option
v-for="dict in dict.type.hrm_stamp_image"
:key="dict.value"
:label="dict.label"
:value="dict.value"
>
<div style="display: flex; align-items: center; gap: 8px;">
<img v-if="dict.value" :src="dict.value" style="width: 40px; height: 40px; object-fit: contain; border: 1px solid #e6e8ed;" onerror="this.style.display='none'" />
<span>{{ dict.label }}</span>
</div>
</el-option>
</el-select>
<div class="hint-text">从字典 hrm_stamp_image 加载印章列表label为章名value为URL</div>
<div v-if="!dict.type.hrm_stamp_image || dict.type.hrm_stamp_image.length === 0" class="hint-text" style="color: #e6a23c;">
提示未找到印章配置请在系统字典中配置 hrm_stamp_image 字典类型
</div>
</el-form-item>
<el-form-item label="印章图片URL" v-if="!stampForm.stampImageUrl || !dict.type.hrm_stamp_image || dict.type.hrm_stamp_image.length === 0">
<el-input
v-model="stampForm.stampImageUrl"
placeholder="手动输入印章图片的完整OSS URLhttps://oss.example.com/stamp/seal.png"
/>
<div class="hint-text">如果下拉列表中没有印章可手动输入印章图片的完整URL</div>
</el-form-item>
<el-form-item label="盖章位置">
<div class="position-hint">
<el-button size="mini" type="primary" icon="el-icon-view" @click="openPositionSelector">预览PDF并点击选择位置</el-button>
<span class="hint-text">在预览中点击PDF即可自动回填坐标</span>
</div>
<div class="hint-text" style="margin-top: 4px; color: #606266;">
坐标说明左下角为原点(0,0)X向右为正Y向上为正单位为像素(px)
</div>
<el-row :gutter="12" style="margin-top: 12px;">
<el-col :span="12">
<el-form-item label="X坐标" label-width="80px">
<el-input-number
v-model="stampForm.xPx"
:min="0"
:precision="0"
placeholder="点击PDF自动获取可微调"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Y坐标" label-width="80px">
<el-input-number
v-model="stampForm.yPx"
:min="0"
:precision="0"
placeholder="点击PDF自动获取可微调"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="宽度" label-width="80px">
<el-input-number
v-model="stampForm.widthPx"
:min="1"
:precision="0"
placeholder="宽度(px可选)"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="高度" label-width="80px">
<el-input-number
v-model="stampForm.heightPx"
:min="1"
:precision="0"
placeholder="高度(px可选)"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<div class="hint-text" style="margin-top: 4px;">
提示宽度和高度为可选不填写时使用印章图片的原始尺寸
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="stamping" @click="doStamp">执行盖章</el-button>
<el-button @click="resetStampForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
<div class="block-title">流转历史</div>
<el-card class="inner-card" shadow="never" v-loading="actionLoading">
<el-timeline v-if="actionList.length">
<el-timeline-item
v-for="(a, idx) in actionList"
:key="idx"
:timestamp="formatDate(a.createTime)"
:type="actionType(a.action)"
>
<div class="timeline-row">
<div class="t-main">
<span class="t-action">{{ actionText(a.action) }}</span>
<span class="t-user">· 办理人{{ a.actionUserId || '-' }}</span>
</div>
<div class="t-remark" v-if="a.remark">{{ a.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
<div v-else class="empty">暂无流转记录</div>
</el-card>
<div class="block-title">审批操作</div>
<el-card class="inner-card" shadow="never">
<div class="hint-text">系统将自动识别你在该单据上的当前待办任务若你不是当前办理人将不会显示办理按钮</div>
<div v-if="currentTask" class="btn-row">
<el-input v-model="actionRemark" type="textarea" :rows="3" placeholder="填写审批意见(可选)" />
<div class="btn-row mt10">
<el-button type="success" v-if="canApprove" :loading="actionSubmitting" @click="submitTaskAction('approve')">通过</el-button>
<el-button type="danger" v-if="canApprove" :loading="actionSubmitting" @click="submitTaskAction('reject')">驳回</el-button>
<el-button v-if="canWithdraw" :loading="actionSubmitting" @click="submitTaskAction('withdraw')">撤回</el-button>
</div>
</div>
<div v-else class="empty">当前无待办任务可能已处理完成或你不是当前审批人</div>
</el-card>
</el-card>
<!-- PDF盖章定位对话框点击PDF直接获取坐标 -->
<el-dialog
title="PDF预览 - 点击选择盖章位置"
:visible.sync="positionSelectorVisible"
width="1200px"
:close-on-click-modal="false"
append-to-body
class="pdf-preview-dialog"
>
<div class="pdf-preview-container">
<div v-if="targetPdfFile && targetPdfFile.url" class="pdf-viewer">
<div class="pdf-controls">
<span class="hint-text" style="margin:0;">提示在PDF上单击选择盖章位置左下为原点选中后会自动回填坐标</span>
<div style="flex:1;"></div>
<el-button size="mini" @click="openPdfPreview">在新窗口打开</el-button>
</div>
<PdfStamper
:pdf-url="targetPdfFile.url"
:initial-page="stampForm.pageNo || 1"
@change="onStampChange"
/>
<div class="pdf-hint">
<div class="hint-text">当前选择页码 {{ stampForm.pageNo || '-' }}x={{ stampForm.xPx !== null ? stampForm.xPx : '-' }}y={{ stampForm.yPx !== null ? stampForm.yPx : '-' }}</div>
</div>
</div>
<div v-else class="empty">PDF文件加载失败</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="positionSelectorVisible = false">关闭</el-button>
<el-button type="primary" :disabled="stampForm.xPx === null || stampForm.yPx === null" @click="positionSelectorVisible = false">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getSealReq,
listFlowAction,
getTodoTaskByBiz,
approveFlowTask,
rejectFlowTask,
withdrawFlowTask,
listEmployee,
stampSealJava
} from '@/api/hrm'
import { queryInstanceByBiz, getFlowInstance, listFlowNode } from '@/api/hrm/flow'
import { listByIds } from '@/api/system/oss'
import PdfStamper from '@/components/PdfStamper/index.vue'
export default {
name: 'HrmSealDetail',
components: { PdfStamper },
dicts: ['hrm_stamp_image'],
props: {
bizId: { type: [String, Number], default: null },
embedded: { type: Boolean, default: false }
},
data() {
return {
seal: {},
employees: [],
loading: false,
actionLoading: false,
actionList: [],
currentTask: null,
actionRemark: '',
actionSubmitting: false,
attachmentList: [],
attachmentLoading: false,
flowInstance: null, // 流程实例信息
flowNodes: [], // 流程节点列表
currentNode: null, // 当前节点信息
targetPdfFile: null,
stampForm: {
pageNo: 1,
stampImageUrl: '',
xPx: null,
yPx: null,
widthPx: null,
heightPx: null,
viewportWidth: null,
viewportHeight: null
},
stamping: false,
pdfPreviewVisible: false,
positionSelectorVisible: false,
selectedPosition: { x: null, y: null }
}
},
computed: {
currentBizId() {
return this.bizId || this.$route?.params?.bizId || this.$route?.query?.bizId || this.$route?.params?.id
},
applicantText() {
const empId = this.seal.empId
const emp = this.employees.find(e => String(e.empId) === String(empId))
if (emp) {
const name = emp.empName || emp.nickName || emp.userName || ''
const no = emp.empNo ? ` · ${emp.empNo}` : ''
const dept = emp.deptName ? ` · ${emp.deptName}` : ''
return `${name || '员工'}${no}${dept}`.trim()
}
return empId ? `员工ID:${empId}` : '-'
},
canApprove() {
console.log(this.currentTask, this.$store.getters.id, this.$store.getters.name)
// 只有待审批状态且是当前用户待审批的才能审批
return this.seal.status === 'running' && (this.currentTask?.assigneeUserName === this.$store.getters.name || this.currentTask?.assigneeUserId === this.$store.getters.id)
},
canStamp() {
// 审批通过后,且尚未生成回执时,可以盖章
return this.seal.status === 'approved' && !this.seal.receiptFileIds && this.targetPdfFile && this.attachmentList.length > 0
},
canWithdraw() {
return false;
return this.seal.status === 'running' && this.seal.createBy === this.$store.getters.name
},
},
created() {
this.loadEmployees()
this.loadDetail()
},
methods: {
loadEmployees() {
listEmployee({ pageNum: 1, pageSize: 1000 }).then(res => {
this.employees = res.rows || res.data || []
})
},
statusText(status) {
const map = { running: '审批中', draft: '草稿', approved: '已通过', rejected: '已驳回', canceled: '已撤销' }
return map[status] || status || '-'
},
statusType(status) {
const map = { running: 'warning', draft: 'info', approved: 'success', rejected: 'danger', canceled: 'info' }
return map[status] || 'info'
},
formatDate(val) {
if (!val) return '-'
const d = new Date(val)
const p = n => (n < 10 ? `0${n}` : n)
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
},
actionText(action) {
const map = { submit: '提交', approve: '通过', reject: '驳回', withdraw: '撤回', cancel: '撤销', stamp: '盖章' }
return map[action] || action || '-'
},
actionType(action) {
const map = { submit: 'primary', approve: 'success', reject: 'danger', withdraw: 'info', cancel: 'info', stamp: 'primary' }
return map[action] || 'info'
},
async loadDetail() {
const bizId = this.currentBizId
if (!bizId) {
this.$message.warning('缺少bizId')
return
}
this.loading = true
try {
const res = await getSealReq(bizId)
this.seal = res.data || {}
this.loadActionsByInstId(this.seal.instId)
await this.loadCurrentTask()
this.loadAttachments()
} finally {
this.loading = false
}
},
async loadAttachments() {
const fileIds = this.seal.applyFileIds
if (!fileIds) {
this.attachmentList = []
this.targetPdfFile = null
return
}
// 解析ID串可能是逗号分隔的字符串
const ids = String(fileIds).split(',').map(id => id.trim()).filter(Boolean)
if (ids.length === 0) {
this.attachmentList = []
this.targetPdfFile = null
return
}
this.attachmentLoading = true
try {
const res = await listByIds(ids)
this.attachmentList = res.data || []
// 取第一个PDF文件作为待盖章文件
this.targetPdfFile = this.attachmentList.find(f => f.fileName && f.fileName.toLowerCase().endsWith('.pdf')) || this.attachmentList[0] || null
// 从备注中解析pageNo格式[盖章页码:第X页]
let pageNo = null
if (this.seal.pageNo) {
pageNo = this.seal.pageNo
} else if (this.seal.remark) {
// 尝试从备注中解析:格式 "[盖章页码:第X页]"
const match = this.seal.remark.match(/\[盖章页码:第(\d+)页\]/)
if (match) {
pageNo = parseInt(match[1])
}
}
if (pageNo) {
this.stampForm.pageNo = pageNo
} else {
// 默认第1页
this.stampForm.pageNo = 1
}
// 如果有申请时的用印类型,尝试匹配对应的印章
const sealType = this.seal.sealType
if (sealType && this.dict.type.hrm_stamp_image && this.dict.type.hrm_stamp_image.length > 0) {
const matched = this.dict.type.hrm_stamp_image.find(d => d.label === sealType || d.label.includes(sealType))
if (matched) {
this.stampForm.stampImageUrl = matched.value
}
}
} catch (e) {
this.$message.error('加载附件失败:' + (e.message || '未知错误'))
this.attachmentList = []
this.targetPdfFile = null
} finally {
this.attachmentLoading = false
}
},
formatFileSize(bytes) {
if (!bytes) return '-'
const units = ['B', 'KB', 'MB', 'GB']
let size = Number(bytes)
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
},
previewFile(file) {
if (file.url) {
window.open(file.url, '_blank')
} else {
this.$message.warning('文件URL不存在')
}
},
downloadFile(ossId) {
window.open(`/system/oss/download/${ossId}`, '_blank')
},
// 回执(盖章后生成的新文件)
previewReceipt() {
if (!this.seal || !this.seal.receiptFileIds) return
window.open(this.seal.receiptFileIds, '_blank')
},
downloadReceipt() {
// 目前 receiptFileIds 存的是 URL非 ossId直接打开即可触发浏览器下载/另存为
if (!this.seal || !this.seal.receiptFileIds) return
window.open(this.seal.receiptFileIds, '_blank')
},
loadActionsByInstId(instId) {
if (!instId) {
this.actionList = []
return
}
this.actionLoading = true
listFlowAction({ instId, pageNum: 1, pageSize: 200 })
.then(res => {
this.actionList = res.rows || res.data || []
})
.finally(() => {
this.actionLoading = false
})
},
async loadCurrentTask() {
try {
const res = await getTodoTaskByBiz('seal', this.currentBizId)
this.currentTask = res?.data || null
} catch (e) {
this.currentTask = null
}
},
submitTaskAction(type) {
if (!this.currentTask || !this.currentTask.taskId) return
this.actionSubmitting = true
const payload = { remark: this.actionRemark }
const apiMap = { approve: approveFlowTask, reject: rejectFlowTask, withdraw: withdrawFlowTask }
apiMap[type](this.currentTask.taskId, payload)
.then(() => {
this.$message.success('操作成功')
this.loadDetail()
})
.finally(() => {
this.actionSubmitting = false
})
},
openPdfPreview() {
if (this.targetPdfFile && this.targetPdfFile.url) {
window.open(this.targetPdfFile.url, '_blank')
} else {
this.$message.warning('PDF文件URL不存在')
}
},
openPositionSelector() {
if (!this.targetPdfFile || !this.targetPdfFile.url) {
this.$message.warning('请先加载PDF文件')
return
}
this.positionSelectorVisible = true
this.selectedPosition = { x: null, y: null }
},
onStampChange(params) {
if (!params) return
// 确保值是有效的数字,避免 null/undefined
if (params.pageNo !== null && params.pageNo !== undefined) {
this.stampForm.pageNo = Number(params.pageNo) || 1
}
if (params.xPx !== null && params.xPx !== undefined && !isNaN(Number(params.xPx))) {
this.stampForm.xPx = Math.floor(Number(params.xPx))
}
if (params.yPx !== null && params.yPx !== undefined && !isNaN(Number(params.yPx))) {
this.stampForm.yPx = Math.floor(Number(params.yPx))
}
if (params.widthPx !== null && params.widthPx !== undefined && !isNaN(Number(params.widthPx))) {
this.stampForm.widthPx = Math.floor(Number(params.widthPx))
}
if (params.heightPx !== null && params.heightPx !== undefined && !isNaN(Number(params.heightPx))) {
this.stampForm.heightPx = Math.floor(Number(params.heightPx))
}
if (params.viewportWidth !== null && params.viewportWidth !== undefined && !isNaN(Number(params.viewportWidth))) {
this.stampForm.viewportWidth = Math.floor(Number(params.viewportWidth))
}
if (params.viewportHeight !== null && params.viewportHeight !== undefined && !isNaN(Number(params.viewportHeight))) {
this.stampForm.viewportHeight = Math.floor(Number(params.viewportHeight))
}
},
resetStampForm() {
this.stampForm = {
pageNo: this.seal.pageNo || 1,
stampImageUrl: '',
xPx: null,
yPx: null,
widthPx: null,
heightPx: null
}
},
async doStamp() {
if (!this.targetPdfFile || !this.targetPdfFile.url) {
this.$message.warning('请先加载PDF文件')
return
}
if (!this.stampForm.stampImageUrl) {
this.$message.warning('请选择印章')
return
}
// 验证坐标:确保是有效的数字,且不为 null/undefined
const rawXPx = this.stampForm.xPx
const rawYPx = this.stampForm.yPx
if (rawXPx === null || rawXPx === undefined || rawXPx === '' || isNaN(Number(rawXPx))) {
this.$message.warning('请选择或输入有效的X坐标')
return
}
const xPx = Math.floor(Number(rawXPx))
if (xPx < 0 || !isFinite(xPx)) {
this.$message.warning('X坐标必须是非负整数')
return
}
if (rawYPx === null || rawYPx === undefined || rawYPx === '' || isNaN(Number(rawYPx))) {
this.$message.warning('请选择或输入有效的Y坐标')
return
}
const yPx = Math.floor(Number(rawYPx))
if (yPx < 0 || !isFinite(yPx)) {
this.$message.warning('Y坐标必须是非负整数')
return
}
if (!this.stampForm.pageNo || this.stampForm.pageNo < 1) {
this.$message.warning('请输入有效的页码')
return
}
try {
await this.$confirm('确定执行盖章操作吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.stamping = true
// 构建 payload确保所有必需字段都是有效值
const payload = {
targetFileUrl: this.targetPdfFile.url,
stampImageUrl: this.stampForm.stampImageUrl,
pageNo: Number(this.stampForm.pageNo),
xPx: xPx,
yPx: yPx,
viewportWidth: this.stampForm.viewportWidth,
viewportHeight: this.stampForm.viewportHeight
}
// 可选字段:只在有值且有效时添加
if (this.stampForm.widthPx !== null && this.stampForm.widthPx !== undefined && this.stampForm.widthPx !== '') {
const width = Math.floor(Number(this.stampForm.widthPx))
if (width > 0 && isFinite(width)) {
payload.widthPx = width
}
}
if (this.stampForm.heightPx !== null && this.stampForm.heightPx !== undefined && this.stampForm.heightPx !== '') {
const height = Math.floor(Number(this.stampForm.heightPx))
if (height > 0 && isFinite(height)) {
payload.heightPx = height
}
}
// 最终验证:确保必需字段都不是 null/undefined并强制转换为整数
if (payload.xPx === null || payload.xPx === undefined || !isFinite(payload.xPx)) {
console.error('Payload validation failed - xPx:', payload.xPx, 'full payload:', payload)
this.$message.error('X坐标数据验证失败请重新填写')
this.stamping = false
return
}
if (payload.yPx === null || payload.yPx === undefined || !isFinite(payload.yPx)) {
console.error('Payload validation failed - yPx:', payload.yPx, 'full payload:', payload)
this.$message.error('Y坐标数据验证失败请重新填写')
this.stamping = false
return
}
// 强制转换为整数,确保类型正确(包括 0 值)
const finalXPx = parseInt(payload.xPx, 10)
const finalYPx = parseInt(payload.yPx, 10)
const finalPageNo = parseInt(payload.pageNo, 10)
// 最后一次检查(包括 0 值的验证)
if (isNaN(finalXPx) || finalXPx < 0) {
console.error('Final validation failed - xPx:', finalXPx, 'original:', payload.xPx)
this.$message.error('X坐标数据格式错误请重新填写')
this.stamping = false
return
}
if (isNaN(finalYPx) || finalYPx < 0) {
console.error('Final validation failed - yPx:', finalYPx, 'original:', payload.yPx)
this.$message.error('Y坐标数据格式错误请重新填写')
this.stamping = false
return
}
if (isNaN(finalPageNo) || finalPageNo < 1) {
console.error('Final validation failed - pageNo:', finalPageNo)
this.$message.error('页码数据格式错误,请重新填写')
this.stamping = false
return
}
// 重新构建 payload确保所有值都是明确的数字类型包括 0
const finalPayload = {
targetFileUrl: String(payload.targetFileUrl),
stampImageUrl: String(payload.stampImageUrl),
pageNo: finalPageNo,
xPx: finalXPx,
yPx: finalYPx
}
// 可选字段
if (payload.widthPx !== undefined && payload.widthPx !== null) {
finalPayload.widthPx = parseInt(payload.widthPx, 10)
}
if (payload.heightPx !== undefined && payload.heightPx !== null) {
finalPayload.heightPx = parseInt(payload.heightPx, 10)
}
// 最终验证:确保 yPx 不是 null即使是 0 也要确保是数字 0
if (finalPayload.yPx === null || finalPayload.yPx === undefined) {
console.error('Critical: yPx is null/undefined in finalPayload:', finalPayload)
this.$message.error('Y坐标验证失败请重新填写')
this.stamping = false
return
}
console.log('发送盖章请求finalPayload:', JSON.stringify(finalPayload, null, 2))
console.log('yPx type:', typeof finalPayload.yPx, 'value:', finalPayload.yPx)
const res = await stampSealJava(this.currentBizId, finalPayload)
this.$message.success('盖章成功!已生成新文件:' + (res.data || '已保存'))
await this.loadDetail()
} catch (error) {
if (error !== 'cancel') {
console.error('盖章失败:', error)
this.$message.error(error.message || '盖章失败,请稍后重试')
}
} finally {
this.stamping = false
}
}
}
}
</script>
<style lang="scss" scoped>
.request-page {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.form-card {
max-width: 980px;
margin: 0 auto;
border: 1px solid #d7d9df;
border-radius: 12px;
background: #ffffff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
color: #2b2f36;
}
.actions {
display: flex;
gap: 8px;
}
.block-title {
margin: 12px 0 8px;
padding-left: 10px;
font-weight: 700;
color: #2f3440;
border-left: 3px solid #9aa3b2;
}
.hint-text {
margin: 6px 0 10px;
font-size: 12px;
color: #8a8f99;
}
.muted {
font-size: 12px;
color: #8a8f99;
}
.form-summary {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 12px;
margin-bottom: 12px;
border: 1px solid #e6e8ed;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
}
.summary-title {
font-size: 16px;
font-weight: 800;
color: #2b2f36;
}
.summary-sub {
margin-top: 4px;
font-size: 12px;
color: #8a8f99;
}
.summary-right {
display: flex;
gap: 16px;
}
.summary-item .k {
font-size: 12px;
color: #8a8f99;
}
.summary-item .v {
margin-top: 2px;
font-weight: 700;
color: #2b2f36;
}
.inner-card {
border: 1px solid #e6e8ed;
}
.empty {
color: #a0a3ad;
font-size: 13px;
padding: 10px 4px;
}
.flow-status {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid #e6e8ed;
border-radius: 6px;
background: #fafbfc;
}
.status-label {
font-size: 13px;
font-weight: 600;
color: #606266;
min-width: 80px;
}
.status-value {
flex: 1;
color: #2b2f36;
font-weight: 500;
}
.timeline-row .t-main {
font-weight: 600;
color: #2b2f36;
}
.timeline-row .t-remark {
margin-top: 4px;
color: #606266;
font-size: 13px;
}
.btn-row {
display: flex;
gap: 10px;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.attachment-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid #e6e8ed;
border-radius: 8px;
background: #fafbfc;
}
.file-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.file-icon {
font-size: 24px;
color: #9aa3b2;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 600;
color: #2b2f36;
margin-bottom: 4px;
}
.file-meta {
font-size: 12px;
color: #8a8f99;
display: flex;
gap: 12px;
}
.file-time {
margin-left: 8px;
}
.file-actions {
display: flex;
gap: 8px;
}
.receipt-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
margin-bottom: 12px;
border: 1px dashed #d7d9df;
border-radius: 8px;
background: #fff;
}
.receipt-title {
font-weight: 700;
color: #2b2f36;
}
.page-no-text {
font-weight: 600;
color: #2b2f36;
}
.text-muted {
color: #8a8f99;
font-size: 12px;
}
.stamp-section {
padding: 12px 0;
}
.stamp-config {
padding: 12px;
background: #fafbfc;
border-radius: 8px;
}
.pdf-file-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fff;
border: 1px solid #e6e8ed;
border-radius: 6px;
}
.position-hint {
display: flex;
align-items: center;
gap: 12px;
}
.pdf-preview-container {
padding: 12px 0;
}
.pdf-viewer {
position: relative;
}
.pdf-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
}
.page-hint {
font-size: 13px;
color: #606266;
}
.pdf-hint {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
}
.pdf-hint .hint-text {
margin-top: 6px;
line-height: 1.6;
}
.pdf-hint .hint-text:first-child {
margin-top: 0;
}
.pdf-hint .hint-text i {
margin-right: 4px;
color: #409eff;
}
@media (max-width: 1200px) {
.summary-right {
display: none;
}
}
</style>