Files
klp-oa/klp-ui/src/views/wms/seal/sealDetail.vue
Joshi 1207072092 feat(wms): 新增用印审批功能
- 在审批API中添加按业务ID查询审批信息的方法
- 配置用印详情页面路由,支持通过业务ID查看用印详情
- 修改待办列表,为用印类型申请隐藏同意驳回按钮
- 在待办列表数据中添加业务ID字段,完善申请类型映射
- 更新审批服务接口和实现类,添加queryByBizId方法
- 重构用印详情页面,集成审批信息加载和权限校验逻辑
- 更新领域模型中的申请类型枚举,添加用印类型支持
- 完善审批任务服务,支持用印申请详情查询和申请人姓名显示
2026-03-19 15:30:28 +08:00

574 lines
17 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-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>
<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 class="block-title">盖章操作</div>
<el-card class="inner-card" shadow="never">
<div class="hint-text">仅当前审批人可操作盖章盖章成功后才算审批通过</div>
<div v-if="canApprove" class="btn-row">
<el-input v-model="actionRemark" type="textarea" :rows="3" placeholder="填写审批意见(可选)" />
<div class="btn-row mt10">
<el-button type="success" :loading="actionSubmitting" @click="openStampDialog">同意并盖章</el-button>
<el-button type="danger" :loading="actionSubmitting" @click="reject">驳回</el-button>
</div>
</div>
<div v-else class="empty">当前无可操作的审批任务</div>
</el-card>
</el-card>
<el-dialog
title="盖章确认"
:visible.sync="stampDialogVisible"
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 class="stamp-config">
<el-form :model="stampForm" label-width="120px" size="small">
<el-form-item label="选择印章">
<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"
/>
</el-select>
</el-form-item>
<el-form-item label="盖章页码">
<el-input-number v-model="stampForm.pageNo" :min="1" :max="999" controls-position="right" style="width: 200px" />
</el-form-item>
<el-form-item label="X 坐标">
<el-input-number v-model="stampForm.xPx" :min="0" :precision="0" style="width: 200px" />
</el-form-item>
<el-form-item label="Y 坐标">
<el-input-number v-model="stampForm.yPx" :min="0" :precision="0" style="width: 200px" />
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="stampDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="stamping" @click="doStampAndApprove">盖章并通过</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getSealReq, rejectSealReq, approveSealReq, stampSealJava } from '@/api/wms/seal'
import { getApprovalByBizId } from '@/api/wms/approval'
import { listByIds } from '@/api/system/oss'
import PdfStamper from '@/components/PdfStamper/index.vue'
export default {
name: 'WmsSealDetail',
components: { PdfStamper },
dicts: ['hrm_stamp_image'],
data() {
return {
seal: {},
loading: false,
attachmentLoading: false,
attachmentList: [],
targetPdfFile: null,
stampDialogVisible: false,
stamping: false,
actionSubmitting: false,
actionRemark: '',
approvalInfo: null,
stampForm: {
pageNo: 1,
stampImageUrl: '',
xPx: null,
yPx: null,
widthPx: null,
heightPx: null,
viewportWidth: null,
viewportHeight: null
}
}
},
computed: {
currentBizId() {
return this.$route?.params?.bizId || this.$route?.query?.bizId || this.$route?.params?.id
},
applicantText() {
if (this.seal.createBy) {
return this.seal.createBy
}
return this.seal.empId ? `员工ID:${this.seal.empId}` : '-'
},
canApprove() {
if (!this.approvalInfo || !this.approvalInfo.task) {
return false
}
const currentUserId = this.$store.getters.id
const task = this.approvalInfo.task
console.log('canApprove check:', {
currentUserId,
approverId: task.approverId,
status: this.seal.status,
taskStatus: task.taskStatus
})
return this.seal.status === 'running' && task.taskStatus === 'pending' && Number(task.approverId) === Number(currentUserId)
}
},
created() {
this.loadDetail()
},
methods: {
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())}`
},
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 || {}
await this.loadAttachments()
await this.loadApprovalInfo()
} finally {
this.loading = false
}
},
async loadApprovalInfo() {
try {
const res = await getApprovalByBizId(this.currentBizId)
this.approvalInfo = res.data || null
} catch (e) {
this.approvalInfo = null
}
},
async loadAttachments() {
const fileIds = this.seal.applyFileIds
if (!fileIds) {
this.attachmentList = []
this.targetPdfFile = null
return
}
const ids = String(fileIds).split(',').map(id => id.trim()).filter(Boolean)
if (!ids.length) {
this.attachmentList = []
this.targetPdfFile = null
return
}
this.attachmentLoading = true
try {
const res = await listByIds(ids)
this.attachmentList = res.data || []
this.targetPdfFile = this.attachmentList.find(f => f.fileName && f.fileName.toLowerCase().endsWith('.pdf')) || this.attachmentList[0] || null
const match = this.seal.remark ? this.seal.remark.match(/\[盖章页码:第(\d+)页\]/) : null
if (match) {
this.stampForm.pageNo = parseInt(match[1])
}
} catch (e) {
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() {
if (!this.seal || !this.seal.receiptFileIds) return
window.open(this.seal.receiptFileIds, '_blank')
},
openPdfPreview() {
if (this.targetPdfFile && this.targetPdfFile.url) {
window.open(this.targetPdfFile.url, '_blank')
} else {
this.$message.warning('PDF文件URL不存在')
}
},
openStampDialog() {
if (!this.targetPdfFile || !this.targetPdfFile.url) {
this.$message.warning('请先加载PDF文件')
return
}
this.stampDialogVisible = true
},
onStampChange(params) {
if (!params) return
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.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))
}
},
async doStampAndApprove() {
if (!this.canApprove) {
this.$message.warning('你不是当前审批人,无法盖章')
return
}
if (!this.stampForm.stampImageUrl) {
this.$message.warning('请选择印章')
return
}
if (this.stampForm.xPx === null || this.stampForm.yPx === null) {
this.$message.warning('请选择盖章位置')
return
}
try {
this.stamping = true
const payload = {
targetFileUrl: this.targetPdfFile.url,
stampImageUrl: this.stampForm.stampImageUrl,
pageNo: Number(this.stampForm.pageNo),
xPx: Math.floor(Number(this.stampForm.xPx)),
yPx: Math.floor(Number(this.stampForm.yPx)),
viewportWidth: this.stampForm.viewportWidth,
viewportHeight: this.stampForm.viewportHeight
}
await stampSealJava(this.currentBizId, payload)
await approveSealReq(this.currentBizId, this.actionRemark)
this.$message.success('盖章成功,审批已通过')
this.stampDialogVisible = false
this.loadDetail()
} finally {
this.stamping = false
}
},
async reject() {
if (!this.canApprove) {
this.$message.warning('你不是当前审批人')
return
}
this.actionSubmitting = true
try {
await rejectSealReq(this.currentBizId, this.actionRemark)
this.$message.success('已驳回')
this.loadDetail()
} finally {
this.actionSubmitting = 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;
}
.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;
}
.btn-row {
display: flex;
flex-direction: column;
gap: 10px;
}
.mt10 {
margin-top: 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;
}
.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;
}
.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;
}
.pdf-hint {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
}
.stamp-config {
margin-top: 12px;
padding: 12px;
background: #fafbfc;
border-radius: 8px;
}
@media (max-width: 1200px) {
.summary-right {
display: none;
}
}
</style>