2025-12-22 10:57:47 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="hrm-page">
|
|
|
|
|
|
<section class="panel-grid quad">
|
2025-12-22 16:53:48 +08:00
|
|
|
|
<el-card v-for="item in requestBlocks" :key="item.key" class="metal-panel" shadow="hover">
|
|
|
|
|
|
<div slot="header" class="panel-header">
|
|
|
|
|
|
<span>{{ item.title }}</span>
|
|
|
|
|
|
<div class="actions-inline">
|
|
|
|
|
|
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="openCreate(item.key)">新增</el-button>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-if="item.statusField"
|
|
|
|
|
|
v-model="item.query.status"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
placeholder="状态"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
|
@change="item.loader"
|
|
|
|
|
|
>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
<el-option label="草稿" value="draft" />
|
|
|
|
|
|
<el-option label="审批中" value="pending" />
|
|
|
|
|
|
<el-option label="通过" value="approved" />
|
|
|
|
|
|
<el-option label="驳回" value="rejected" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-button size="mini" icon="el-icon-refresh" @click="item.loader">刷新</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="item.list" v-loading="item.loading" height="300" stripe>
|
|
|
|
|
|
<el-table-column label="员工" prop="empId" min-width="100" />
|
|
|
|
|
|
<el-table-column label="类型/目的" :prop="item.typeField" min-width="120" />
|
|
|
|
|
|
<el-table-column label="开始" prop="startTime" min-width="140">
|
|
|
|
|
|
<template slot-scope="scope">{{ formatDate(scope.row.startTime) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="结束" prop="endTime" min-width="140">
|
|
|
|
|
|
<template slot-scope="scope">{{ formatDate(scope.row.endTime) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" prop="status" min-width="110">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status || '-' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="附件" prop="accessoryApplyIds" min-width="150" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-if="item.key === 'seal'"
|
|
|
|
|
|
label="操作"
|
|
|
|
|
|
min-width="220"
|
|
|
|
|
|
fixed="right"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button size="mini" type="text" @click="approveSeal(scope.row)">通过</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click="rejectSeal(scope.row)">驳回</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click="cancelSeal(scope.row)">撤销</el-button>
|
|
|
|
|
|
<el-button size="mini" type="text" @click="openStamp(scope.row)">盖章</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
title="盖章"
|
|
|
|
|
|
:visible.sync="stampDialogVisible"
|
|
|
|
|
|
width="720px"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form :model="stampForm" label-width="110px" size="small">
|
|
|
|
|
|
<el-form-item label="待盖章文件" prop="targetFileUrl">
|
|
|
|
|
|
<el-input v-model="stampForm.targetFileUrl" placeholder="PDF或图片 OSS 完整 URL" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="章图片" prop="stampImageUrl">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="stampForm.stampImageUrl"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
placeholder="选择章图(字典配置)"
|
|
|
|
|
|
@change="preloadStampImage"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in dict.type.hrm_stamp_image || []"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="页码">
|
|
|
|
|
|
<el-input-number v-model="stampForm.pageNo" :min="1" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="坐标/尺寸">
|
|
|
|
|
|
<div class="readonly-row">
|
|
|
|
|
|
<span>X: {{ stampForm.xPx || '-' }} px</span>
|
|
|
|
|
|
<span>Y: {{ stampForm.yPx || '-' }} px</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="readonly-row">
|
|
|
|
|
|
<span>宽: {{ stampForm.widthPx || stampImageNatural.width || '-' }} px</span>
|
|
|
|
|
|
<span>高: {{ stampForm.heightPx || stampImageNatural.height || '-' }} px</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="hint-text">点击右侧预览即可定位,尺寸默认取章图原始大小</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<div class="preview-card">
|
|
|
|
|
|
<div class="preview-title">图形化定位(点击预览设置坐标)</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="preview-area"
|
|
|
|
|
|
ref="previewArea"
|
|
|
|
|
|
v-if="stampForm.targetFileUrl"
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
:src="stampForm.targetFileUrl"
|
|
|
|
|
|
class="preview-img"
|
|
|
|
|
|
@load="handlePreviewLoad"
|
|
|
|
|
|
@click="handlePreviewClick"
|
|
|
|
|
|
alt="preview"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="marker.visible"
|
|
|
|
|
|
class="stamp-marker"
|
|
|
|
|
|
:style="markerStyle"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="preview-placeholder">请先填写待盖章文件URL(建议提供图片预览)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<div slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button @click="stampDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="stampSubmitting" @click="submitStamp">盖章</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2025-12-22 16:53:48 +08:00
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
title="新增申请"
|
|
|
|
|
|
:visible.sync="createDialogVisible"
|
|
|
|
|
|
width="520px"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="create-type-switch">
|
|
|
|
|
|
<el-radio-group v-model="createType" size="small" @change="() => (createForm = defaultCreateForm(createType))">
|
|
|
|
|
|
<el-radio-button label="leave">请假</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="overtime">加班</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="travel">出差</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="seal">用印</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
<span class="hint-text" style="margin-left:8px">先选类型,再填写必填项</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-form ref="createFormRef" :model="createForm" :rules="currentCreateRules" label-width="110px" size="small">
|
|
|
|
|
|
<el-form-item label="申请人">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="createForm.empId"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
placeholder="选择员工"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="emp in employees"
|
|
|
|
|
|
:key="emp.empId"
|
|
|
|
|
|
:label="`${emp.empName || emp.empNo || emp.empId}`"
|
|
|
|
|
|
:value="emp.empId"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<template v-if="createType === 'leave'">
|
|
|
|
|
|
<el-form-item label="请假类型" prop="leaveType">
|
|
|
|
|
|
<el-input v-model="createForm.leaveType" placeholder="年假/事假等" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="开始时间" prop="startTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.startTime" type="datetime" placeholder="开始" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="结束时间" prop="endTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.endTime" type="datetime" placeholder="结束" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="时长(小时)" prop="hours">
|
|
|
|
|
|
<el-input-number v-model="createForm.hours" :min="0.5" :step="0.5" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="事由" prop="reason">
|
|
|
|
|
|
<el-input v-model="createForm.reason" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="createType === 'overtime'">
|
|
|
|
|
|
<el-form-item label="加班类型" prop="otType">
|
|
|
|
|
|
<el-input v-model="createForm.otType" placeholder="工作日/休息日等" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="开始时间" prop="startTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.startTime" type="datetime" placeholder="开始" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="结束时间" prop="endTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.endTime" type="datetime" placeholder="结束" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="时长(小时)" prop="hours">
|
|
|
|
|
|
<el-input-number v-model="createForm.hours" :min="0.5" :step="0.5" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="事由" prop="reason">
|
|
|
|
|
|
<el-input v-model="createForm.reason" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="createType === 'travel'">
|
|
|
|
|
|
<el-form-item label="开始时间" prop="startTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.startTime" type="datetime" placeholder="开始" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="结束时间" prop="endTime">
|
|
|
|
|
|
<el-date-picker v-model="createForm.endTime" type="datetime" placeholder="结束" style="width: 100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="目的地" prop="destination">
|
|
|
|
|
|
<el-input v-model="createForm.destination" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="事由" prop="reason">
|
|
|
|
|
|
<el-input v-model="createForm.reason" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="createType === 'seal'">
|
|
|
|
|
|
<el-form-item label="用印类型" prop="sealType">
|
|
|
|
|
|
<el-input v-model="createForm.sealType" placeholder="合同章/法人章..." />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="用途说明" prop="purpose">
|
|
|
|
|
|
<el-input v-model="createForm.purpose" type="textarea" :rows="2" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="申请材料附件" prop="applyFileIds">
|
|
|
|
|
|
<file-upload v-model="createForm.applyFileIds" :limit="5" :file-size="50" :file-type="['pdf']" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="需要回执">
|
|
|
|
|
|
<el-switch v-model="createForm.receiptRequired" :active-value="1" :inactive-value="0" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="createSubmitting" @click="submitCreate">提交</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2025-12-22 10:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import {
|
|
|
|
|
|
listLeaveReq,
|
|
|
|
|
|
listOvertimeReq,
|
|
|
|
|
|
listTravelReq,
|
|
|
|
|
|
listSealReq,
|
2025-12-22 16:53:48 +08:00
|
|
|
|
listEmployee,
|
|
|
|
|
|
addLeaveReq,
|
|
|
|
|
|
addOvertimeReq,
|
|
|
|
|
|
addTravelReq,
|
|
|
|
|
|
addSealReq,
|
2025-12-22 10:57:47 +08:00
|
|
|
|
approveSealReq,
|
|
|
|
|
|
rejectSealReq,
|
|
|
|
|
|
cancelSealReq,
|
|
|
|
|
|
stampSealJava,
|
|
|
|
|
|
stampSealPython
|
|
|
|
|
|
} from '@/api/hrm'
|
2025-12-22 16:53:48 +08:00
|
|
|
|
import FileUpload from '@/components/FileUpload'
|
2025-12-22 10:57:47 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'HrmRequests',
|
2025-12-22 16:53:48 +08:00
|
|
|
|
dicts: ['hrm_stamp_image'],
|
|
|
|
|
|
components: { FileUpload },
|
2025-12-22 10:57:47 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
requestBlocks: [],
|
|
|
|
|
|
stampDialogVisible: false,
|
|
|
|
|
|
stampSubmitting: false,
|
|
|
|
|
|
stampForm: {
|
|
|
|
|
|
targetFileUrl: '',
|
|
|
|
|
|
stampImageUrl: '',
|
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
|
xPx: 0,
|
|
|
|
|
|
yPx: 0,
|
|
|
|
|
|
widthPx: undefined,
|
|
|
|
|
|
heightPx: undefined
|
|
|
|
|
|
},
|
|
|
|
|
|
currentSeal: null,
|
|
|
|
|
|
previewNatural: { width: 0, height: 0 },
|
|
|
|
|
|
marker: { visible: false, x: 0, y: 0, width: 0, height: 0 },
|
2025-12-22 16:53:48 +08:00
|
|
|
|
stampImageNatural: { width: 0, height: 0 },
|
|
|
|
|
|
employees: [],
|
|
|
|
|
|
createDialogVisible: false,
|
|
|
|
|
|
createType: '',
|
|
|
|
|
|
createSubmitting: false,
|
|
|
|
|
|
createForm: {},
|
|
|
|
|
|
createRules: {
|
|
|
|
|
|
empId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
|
|
|
|
|
leaveType: [{ required: true, message: '请选择请假类型', trigger: 'blur' }],
|
|
|
|
|
|
otType: [{ required: true, message: '请输入加班类型', trigger: 'blur' }],
|
|
|
|
|
|
sealType: [{ required: true, message: '请输入用印类型', trigger: 'blur' }],
|
|
|
|
|
|
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
|
|
|
|
|
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
|
|
|
|
|
|
hours: [{ required: true, message: '请输入时长', trigger: 'blur' }],
|
|
|
|
|
|
destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
|
|
|
|
|
|
purpose: [{ required: true, message: '请输入用途说明', trigger: 'blur' }],
|
|
|
|
|
|
applyFileIds: [{ required: true, message: '请上传附件', trigger: 'change' }],
|
|
|
|
|
|
reason: [{ required: true, message: '请输入事由', trigger: 'blur' }]
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
|
|
|
|
|
this.initRequests()
|
2025-12-22 16:53:48 +08:00
|
|
|
|
this.loadEmployees()
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
applicantDisplay() {
|
|
|
|
|
|
const user = this.$store?.state?.user || {}
|
|
|
|
|
|
const name = user.nickName || user.userName || ''
|
|
|
|
|
|
const id = user.userId || user.userId === 0 ? user.userId : ''
|
|
|
|
|
|
return name ? `${name}${id ? ` (${id})` : ''}` : id || '当前登录人'
|
|
|
|
|
|
},
|
|
|
|
|
|
currentCreateRules() {
|
|
|
|
|
|
// 动态裁剪规则,避免多余必填影响校验
|
|
|
|
|
|
const keys = {
|
|
|
|
|
|
leave: ['empId', 'leaveType', 'startTime', 'endTime', 'hours', 'reason'],
|
|
|
|
|
|
overtime: ['empId', 'otType', 'startTime', 'endTime', 'hours', 'reason'],
|
|
|
|
|
|
travel: ['empId', 'startTime', 'endTime', 'destination', 'reason'],
|
|
|
|
|
|
seal: ['empId', 'sealType', 'purpose', 'applyFileIds']
|
|
|
|
|
|
}[this.createType] || []
|
|
|
|
|
|
const picked = {}
|
|
|
|
|
|
keys.forEach(k => {
|
|
|
|
|
|
if (this.createRules[k]) picked[k] = this.createRules[k]
|
|
|
|
|
|
})
|
|
|
|
|
|
return picked
|
|
|
|
|
|
}
|
2025-12-22 10:57:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
statusType(status) {
|
|
|
|
|
|
if (!status) return 'info'
|
|
|
|
|
|
const map = { pending: 'warning', draft: 'info', approved: 'success', rejected: 'danger' }
|
|
|
|
|
|
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())}`
|
|
|
|
|
|
},
|
|
|
|
|
|
initRequests() {
|
|
|
|
|
|
this.requestBlocks = [
|
|
|
|
|
|
{ key: 'leave', title: '请假单', typeField: 'leaveType', statusField: 'status', query: { pageNum: 1, pageSize: 10, status: undefined }, list: [], loading: false, loader: this.loadLeaveReq },
|
|
|
|
|
|
{ key: 'overtime', title: '加班单', typeField: 'overtimeType', statusField: 'status', query: { pageNum: 1, pageSize: 10, status: undefined }, list: [], loading: false, loader: this.loadOvertimeReq },
|
|
|
|
|
|
{ key: 'travel', title: '出差单', typeField: 'travelType', statusField: 'status', query: { pageNum: 1, pageSize: 10, status: undefined }, list: [], loading: false, loader: this.loadTravelReq },
|
|
|
|
|
|
{ key: 'seal', title: '用印申请', typeField: 'sealType', statusField: 'status', query: { pageNum: 1, pageSize: 10, status: undefined }, list: [], loading: false, loader: this.loadSealReq }
|
|
|
|
|
|
]
|
|
|
|
|
|
this.requestBlocks.forEach(b => b.loader())
|
|
|
|
|
|
},
|
|
|
|
|
|
loadLeaveReq() {
|
|
|
|
|
|
const block = this.requestBlocks.find(i => i.key === 'leave')
|
|
|
|
|
|
block.loading = true
|
|
|
|
|
|
listLeaveReq(block.query)
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
block.list = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
block.loading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadOvertimeReq() {
|
|
|
|
|
|
const block = this.requestBlocks.find(i => i.key === 'overtime')
|
|
|
|
|
|
block.loading = true
|
|
|
|
|
|
listOvertimeReq(block.query)
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
block.list = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
block.loading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadTravelReq() {
|
|
|
|
|
|
const block = this.requestBlocks.find(i => i.key === 'travel')
|
|
|
|
|
|
block.loading = true
|
|
|
|
|
|
listTravelReq(block.query)
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
block.list = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
block.loading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
loadSealReq() {
|
|
|
|
|
|
const block = this.requestBlocks.find(i => i.key === 'seal')
|
|
|
|
|
|
block.loading = true
|
|
|
|
|
|
listSealReq(block.query)
|
|
|
|
|
|
.then(res => {
|
|
|
|
|
|
block.list = res.rows || []
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
block.loading = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
approveSeal(row) {
|
|
|
|
|
|
approveSealReq(row.bizId).then(() => {
|
|
|
|
|
|
this.$message.success('已通过')
|
|
|
|
|
|
this.loadSealReq()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
rejectSeal(row) {
|
|
|
|
|
|
rejectSealReq(row.bizId).then(() => {
|
|
|
|
|
|
this.$message.success('已驳回')
|
|
|
|
|
|
this.loadSealReq()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
cancelSeal(row) {
|
|
|
|
|
|
cancelSealReq(row.bizId).then(() => {
|
|
|
|
|
|
this.$message.success('已撤销')
|
|
|
|
|
|
this.loadSealReq()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
openStamp(row) {
|
|
|
|
|
|
this.currentSeal = row
|
|
|
|
|
|
this.stampDialogVisible = true
|
|
|
|
|
|
this.marker.visible = false
|
|
|
|
|
|
},
|
|
|
|
|
|
submitStamp() {
|
|
|
|
|
|
if (!this.currentSeal) return
|
|
|
|
|
|
this.stampSubmitting = true
|
|
|
|
|
|
stampSealJava(this.currentSeal.bizId, this.stampForm)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
this.$message.success('盖章指令已提交')
|
|
|
|
|
|
this.stampDialogVisible = false
|
|
|
|
|
|
this.loadSealReq()
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.stampSubmitting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
handlePreviewLoad(e) {
|
|
|
|
|
|
const img = e.target
|
|
|
|
|
|
this.previewNatural = { width: img.naturalWidth, height: img.naturalHeight }
|
|
|
|
|
|
this.updateMarkerStyle()
|
|
|
|
|
|
},
|
|
|
|
|
|
handlePreviewClick(event) {
|
|
|
|
|
|
if (!this.previewNatural.width || !this.previewNatural.height) return
|
|
|
|
|
|
const rect = this.$refs.previewArea.getBoundingClientRect()
|
|
|
|
|
|
const displayWidth = rect.width
|
|
|
|
|
|
const displayHeight = rect.height
|
|
|
|
|
|
const clickX = event.clientX - rect.left
|
|
|
|
|
|
const clickY = event.clientY - rect.top
|
|
|
|
|
|
const xRatio = clickX / displayWidth
|
|
|
|
|
|
const yRatio = clickY / displayHeight
|
|
|
|
|
|
const xPx = Math.round(xRatio * this.previewNatural.width)
|
|
|
|
|
|
// 注意 PDF 坐标原点左下,这里预览原点左上,需要转换
|
|
|
|
|
|
const yPx = Math.round((1 - yRatio) * this.previewNatural.height)
|
|
|
|
|
|
this.stampForm.xPx = xPx
|
|
|
|
|
|
this.stampForm.yPx = yPx
|
|
|
|
|
|
// 默认尺寸取章图天然尺寸
|
|
|
|
|
|
if (this.stampImageNatural.width) {
|
|
|
|
|
|
this.stampForm.widthPx = this.stampForm.widthPx || this.stampImageNatural.width
|
|
|
|
|
|
this.stampForm.heightPx = this.stampForm.heightPx || this.stampImageNatural.height
|
|
|
|
|
|
}
|
|
|
|
|
|
this.updateMarkerStyle()
|
|
|
|
|
|
},
|
|
|
|
|
|
preloadStampImage() {
|
|
|
|
|
|
if (!this.stampForm.stampImageUrl) return
|
|
|
|
|
|
const img = new Image()
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
this.stampImageNatural = { width: img.width, height: img.height }
|
|
|
|
|
|
}
|
|
|
|
|
|
img.src = this.stampForm.stampImageUrl
|
|
|
|
|
|
},
|
|
|
|
|
|
updateMarkerStyle() {
|
|
|
|
|
|
if (!this.previewNatural.width || !this.previewNatural.height) return
|
|
|
|
|
|
const rect = this.$refs.previewArea?.getBoundingClientRect?.()
|
|
|
|
|
|
if (!rect) return
|
|
|
|
|
|
const displayWidth = rect.width
|
|
|
|
|
|
const displayHeight = rect.height
|
|
|
|
|
|
const xRatio = this.stampForm.xPx / this.previewNatural.width
|
|
|
|
|
|
const yRatio = 1 - this.stampForm.yPx / this.previewNatural.height
|
|
|
|
|
|
const wRatio = (this.stampForm.widthPx || this.stampImageNatural.width || 0) / this.previewNatural.width
|
|
|
|
|
|
const hRatio = (this.stampForm.heightPx || this.stampImageNatural.height || 0) / this.previewNatural.height
|
|
|
|
|
|
this.marker = {
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
x: xRatio * displayWidth,
|
|
|
|
|
|
y: yRatio * displayHeight,
|
|
|
|
|
|
width: wRatio * displayWidth,
|
|
|
|
|
|
height: hRatio * displayHeight
|
|
|
|
|
|
}
|
2025-12-22 16:53:48 +08:00
|
|
|
|
},
|
|
|
|
|
|
openCreate(type) {
|
|
|
|
|
|
this.createType = type
|
|
|
|
|
|
this.createDialogVisible = true
|
|
|
|
|
|
this.createForm = this.defaultCreateForm(type)
|
|
|
|
|
|
this.$nextTick(() => this.$refs.createFormRef && this.$refs.createFormRef.clearValidate())
|
|
|
|
|
|
},
|
|
|
|
|
|
defaultCreateForm(type) {
|
|
|
|
|
|
const common = { empId: '', remark: '' }
|
|
|
|
|
|
if (type === 'leave') {
|
|
|
|
|
|
return { ...common, leaveType: '', startTime: '', endTime: '', hours: 0, reason: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type === 'overtime') {
|
|
|
|
|
|
return { ...common, otType: '', startTime: '', endTime: '', hours: 0, reason: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type === 'travel') {
|
|
|
|
|
|
return { ...common, startTime: '', endTime: '', destination: '', reason: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type === 'seal') {
|
|
|
|
|
|
return { ...common, sealType: '', purpose: '', applyFileIds: '', receiptRequired: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
return common
|
|
|
|
|
|
},
|
|
|
|
|
|
loadEmployees() {
|
|
|
|
|
|
listEmployee({ pageNum: 1, pageSize: 500 }).then(res => {
|
|
|
|
|
|
this.employees = res.rows || res.data || []
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
submitCreate() {
|
|
|
|
|
|
if (!this.createType) return
|
|
|
|
|
|
const apiMap = {
|
|
|
|
|
|
leave: addLeaveReq,
|
|
|
|
|
|
overtime: addOvertimeReq,
|
|
|
|
|
|
travel: addTravelReq,
|
|
|
|
|
|
seal: addSealReq
|
|
|
|
|
|
}
|
|
|
|
|
|
const fn = apiMap[this.createType]
|
|
|
|
|
|
if (!fn) return
|
|
|
|
|
|
// 默认申请人取当前登录用户
|
|
|
|
|
|
const payload = { ...this.createForm }
|
|
|
|
|
|
const userId = this.$store?.state?.user?.userId
|
|
|
|
|
|
if (userId) {
|
|
|
|
|
|
payload.empId = userId
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error('未获取到当前登录用户,无法提交')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$refs.createFormRef.validate(valid => {
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
this.createSubmitting = true
|
|
|
|
|
|
fn(payload)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
this.$message.success('提交成功')
|
|
|
|
|
|
this.createDialogVisible = false
|
|
|
|
|
|
const block = this.requestBlocks.find(i => i.key === this.createType)
|
|
|
|
|
|
block && block.loader()
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.createSubmitting = false
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2025-12-22 10:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.hrm-page {
|
|
|
|
|
|
padding: 16px 20px 32px;
|
|
|
|
|
|
background: #f8f9fb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.panel-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.metal-panel {
|
|
|
|
|
|
border: 1px solid #d7d9df;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
.actions-inline {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.coord-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-card {
|
|
|
|
|
|
border: 1px dashed #e6e8ed;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
min-height: 340px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-area {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 280px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
cursor: crosshair;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preview-placeholder {
|
|
|
|
|
|
color: #a0a3ad;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border: 1px dashed #ebeef5;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stamp-marker {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
border: 2px dashed #409eff;
|
|
|
|
|
|
background: rgba(64, 158, 255, 0.08);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
|
.panel-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|