Commit 3bed5608 by yubin

修改

parent 34ea55c1
......@@ -28,7 +28,9 @@
<template slot="action" slot-scope="{ row }">
<Button size="small" class="mr10" @click="openDetail(row)">详细</Button>
<Button size="small" type="primary" class="mr10" @click="openEdit(row)" v-if="canEdit(row)">修改</Button>
<Button size="small" type="success" class="mr10" @click="submit(row)" v-if="canSubmit(row)">提交</Button>
<Poptip confirm title="确认提交?" transfer @on-ok="submit(row)" v-if="canSubmit(row)">
<Button size="small" confirm type="success" class="mr10">提交</Button>
</Poptip>
<Poptip confirm title="确认删除?" transfer @on-ok="deleteApply(row)" v-if="canDelete(row)">
<Button size="small" type="error" class="mr10">删除</Button>
</Poptip>
......@@ -42,7 +44,7 @@
show-total show-sizer @on-change="pageChange('apply', $event)" @on-page-size-change="sizeChange('apply', $event)" />
</TabPane>
<TabPane :label="'请假待审批'" name="pending">
<TabPane :label="'请假待审批'" name="pending" v-if="power.leave_approval">
<Table border :loading="loading.pending" :columns="pendingColumns" :data="tables.pending">
<template slot="action" slot-scope="{ row }">
<div class="action-buttons">
......@@ -54,7 +56,7 @@
show-total show-sizer @on-change="pageChange('pending', $event)" @on-page-size-change="sizeChange('pending', $event)" />
</TabPane>
<TabPane label="审批历史查询" name="history">
<TabPane label="审批历史查询" name="history" v-if="power.leave_view">
<div class="search-div">
<Row type="flex" :gutter="16" align="middle">
<Col span="18">
......@@ -62,10 +64,8 @@
<DatePicker v-model="filters.history.start_time" type="date" placeholder="开始日期" style="width: 200px" class="mr10" />
<span>结束:</span>
<DatePicker v-model="filters.history.end_time" type="date" placeholder="结束日期" style="width: 200px" class="mr10" />
<span>状态:</span>
<Select v-model="filters.history.status" style="width: 200px">
<Option v-for="opt in statusOptionsFor('history')" :key="opt.id" :value="opt.id">{{ opt.name }}</Option>
</Select>
<span>审批人:</span>
<Input v-model="filters.history.approver_name" placeholder="请输入审批人姓名" style="width: 200px" />
</Col>
<Col span="6" class="text-right">
<Button type="primary" class="mr10" @click="handleSearch('history')">搜索</Button>
......@@ -82,22 +82,26 @@
show-total show-sizer @on-change="pageChange('history', $event)" @on-page-size-change="sizeChange('history', $event)" />
</TabPane>
<TabPane label="请假申请查询" name="query">
<TabPane label="请假申请查询" name="query" v-if="power.leave_view">
<div class="search-div">
<Row type="flex" :gutter="16" align="middle">
<Col span="18">
<Col span="24">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span>开始:</span>
<DatePicker v-model="filters.query.start_time" type="date" placeholder="开始日期" style="width: 200px" class="mr10" />
<span>结束:</span>
<DatePicker v-model="filters.query.end_time" type="date" placeholder="结束日期" style="width: 200px" class="mr10" />
<span>状态:</span>
<Select v-model="filters.query.status" style="width: 200px">
<Option v-for="opt in statusOptionsFor('query')" :key="opt.id" :value="opt.id">{{ opt.name }}</Option>
<Option v-for="opt in statusOptionsFor('history')" :key="opt.id" :value="opt.id">{{ opt.name }}</Option>
</Select>
</Col>
<Col span="6" class="text-right">
</div>
<div>
<Button type="primary" class="mr10" @click="handleSearch('query')">搜索</Button>
<Button type="primary" class="mr10" @click="handleReset('query')">重置</Button>
</div>
</div>
</Col>
</Row>
</div>
......@@ -110,7 +114,7 @@
show-total show-sizer @on-change="pageChange('query', $event)" @on-page-size-change="sizeChange('query', $event)" />
</TabPane>
<TabPane label="请假统计" name="stats">
<TabPane label="请假统计" name="stats" v-if="power.leave_view">
<div class="search-div">
<Row type="flex" :gutter="16" align="middle">
<Col span="18">
......@@ -144,7 +148,7 @@
<DatePicker v-model="applyModal.form.end_time" type="datetime" placeholder="请选择结束时间" />
</FormItem>
<FormItem label="请假时长(天)" prop="duration">
<Input v-model="applyModal.form.duration" placeholder="可手动录入或自动计算" />
<Input v-model="applyModal.form.duration" placeholder="请输入请假时长" />
</FormItem>
<FormItem label="请假事由" prop="reason">
<Input type="textarea" v-model="applyModal.form.reason" :rows="3" />
......@@ -235,6 +239,7 @@
</Row>
<Row class="mt8"><Col span="24"><p><strong>请假类型:</strong>{{ detailModal.data.leave_type_name || '-' }}</p></Col></Row>
<Row class="mt8"><Col span="24"><p><strong>起止时间:</strong>{{ formatDatetime(detailModal.data.start_time) }} - {{ formatDatetime(detailModal.data.end_time) }}</p></Col></Row>
<Row class="mt8"><Col span="24"><p><strong>请假时长(天):</strong>{{ detailModal.data.duration || '-' }}</p></Col></Row>
<Row class="mt8"><Col span="24"><p><strong>请假事由:</strong>{{ detailModal.data.reason || '-' }}</p></Col></Row>
<Row class="mt8"><Col span="24"><p><strong>状态:</strong>{{ mapStatusText(detailModal.data.status) }}</p></Col></Row>
<Row v-if="detailModal.data.status === -1 || String(detailModal.data.status) === '-1'" class="mt8"><Col span="24"><p><strong>驳回理由:</strong>{{ detailModal.data.back_reason || detailModal.data.backReason || '-' }}</p></Col></Row>
......@@ -284,6 +289,7 @@ import {
deleteLeaveById,
selectTransferApprovalList,
transferLeaveApproval,
getUserDmPermissionList,
getLeaveApprovalPermission
} from '@/api/key-dm'
// 注意:不单独调用后台的 isPending 接口,改为复用 getPendingLeaveList 获取 totalRecord 判定是否存在待办
......@@ -293,7 +299,7 @@ export default {
data () {
return {
activeTab: 'apply',
filters: { apply: { start_time: null, end_time: null, status: '' }, history: { start_time: null, end_time: null, status: '' }, query: { start_time: null, end_time: null, status: '' } },
filters: { apply: { start_time: null, end_time: null, status: '' }, history: { start_time: null, end_time: null, status: '' }, query: { start_time: null, end_time: null, status: '', approver_name: '' } },
// 下拉:请假审批权限可选人员
leaveApprovalOptions: [],
// 下拉:转审批审批人列表
......@@ -329,6 +335,7 @@ export default {
{ title: '申请人', key: 'user_name', align: 'center' },
{ title: '请假类型', key: 'leave_type_name', align: 'center' },
{ title: '起止时间', key: 'start_time', align: 'center' },
{ title: '请假时长(天)', key: 'duration', align: 'center' },
{ title: '提交时间', key: 'submit_time', align: 'center' },
{ title: '操作', slot: 'action', align: 'center', width: 120 }
],
......@@ -338,6 +345,27 @@ export default {
{ title: '请假类型', key: 'leave_type_name', align: 'center' },
{ title: '起止时间', key: 'start_time', align: 'center', render: (h, { row }) => h('span', `${row.start_time || '-'} ~ ${row.end_time || '-'}`) },
{ title: '审批完成时间', key: 'approval_complete_time', align: 'center' },
{ title: '结果', key: 'op_result', align: 'center', render: (h, { row }) => {
let statusText = '';
let statusClass = '';
switch (row.op_result) {
case 0:
statusText = '未通过';
statusClass = 'text-orange';
break;
case 1:
statusText = '通过';
statusClass = 'text-green';
break;
case -1:
statusText = '驳回';
statusClass = 'text-red';
break;
default:
statusText = '-';
}
return h('span', { class: statusClass }, statusText);
} },
{ title: '操作', slot: 'action', align: 'center', width: 100 }
],
queryColumns: [
......@@ -378,88 +406,47 @@ export default {
}
],
applyModal: { visible: false, isEdit: false, saving: false, form: {} },
// 控制是否在赋值时触发自动计算(true = 允许计算;false = 赋值时不计算)
suppressDurationCalc: false,
approveModal: { visible: false, record: {}, comment: '', submitting: false, transferEnabled: false, transferApprover: '', approvalFlow: [] },
transferModal: { visible: false, selectedApprover: '', comment: '', processing: false, record: {} },
detailModal: { visible: false, loading: false, data: {}, approvals: [] },
statsStart: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
statsEnd: new Date(),
statsDept: '',
power: {
leave_approval: false, // 请假审批
leave_view: false, // 请假查询(统计)
supply_approval: false, // 用品申领审批
supply_view: false, // 用品查询(统计)
inventory_manager: false // 库存管理
},
// 表单验证规则
applyRules: {
leave_type_id: [
{ required: true, message: '请选择请假类型', trigger: 'change' }
{ required: true, message: '请选择请假类型', trigger: 'input' }
],
start_time: [
{ required: true, message: '请选择开始时间', trigger: 'change', type: 'date' }
{ required: true, message: '请选择开始时间', trigger: 'input', type: 'date' }
],
end_time: [
{ required: true, message: '请选择结束时间', trigger: 'change', type: 'date' }
{ required: true, message: '请选择结束时间', trigger: 'input', type: 'date' }
],
duration: [
{
validator: function (rule, value, callback) {
const form = (this && this.applyModal && this.applyModal.form) ? this.applyModal.form : {}
const parseToDateLocal = (v) => {
if (v === null || v === undefined || v === '') return null
if (typeof v === 'number') return new Date(v)
if (typeof v === 'string') {
if (/^\d+$/.test(v)) return new Date(Number(v))
const s = v.includes('T') ? v : v.replace(' ', 'T')
const parsed = new Date(s)
return isNaN(parsed.getTime()) ? null : parsed
}
if (v instanceof Date) return isNaN(v.getTime()) ? null : v
const parsed = new Date(v)
return isNaN(parsed.getTime()) ? null : parsed
}
// 解析起止时间的整天差
const sd = parseToDateLocal(form.start_time)
const ed = parseToDateLocal(form.end_time)
let fullDays = 0
if (sd && ed) {
const diff = ed.getTime() - sd.getTime()
fullDays = Math.floor(diff / (1000 * 60 * 60 * 24))
}
// 如果不足整天,不校验(允许用户填写任意时长)
if (fullDays < 1) { callback(); return }
// 整天或以上时,必须填写且整数部分需与计算天数一致
if (value === null || value === undefined || String(value).trim() === '') {
callback(new Error('请填写请假时长')); return
}
const num = Number(value)
if (isNaN(num) || num <= 0) { callback(new Error('请假天数必须为大于 0 的数字')); return }
const integerPart = Math.floor(num)
if (integerPart !== fullDays) {
callback(new Error(`请假整数天应为 ${fullDays} 天,请确认`)); return
}
callback()
},
trigger: 'blur'
}
{ required: true, message: '请填写请假时长', trigger: 'input' }
],
approver_id: [
{ required: true, message: '请选择审批人', trigger: 'change' }
{ required: true, message: '请选择审批人', trigger: 'input' }
],
reason: [
{ required: true, message: '请填写请假事由', trigger: 'change' }
{ required: true, message: '请填写请假事由', trigger: 'input' }
],
emergency_phone: [
{ message: '请输入紧急电话', trigger: 'blur' },
{ message: '请输入紧急电话', trigger: 'input' },
{
pattern: /^(1[3-9]\d{9})|((0\d{2,3}-?)?\d{7,8})$/,
message: '请输入有效的电话号码',
trigger: 'blur'
trigger: 'input'
}
],
// (必填性与具体数值由上面的 validator 决定)
]
}
}
},
......@@ -469,23 +456,10 @@ export default {
this.loadLeaveApprovalOptions()
// 加载待审批计数,用于页面顶部/选项卡徽章显示
this.loadPendingCount()
// 将 duration 的 validator 绑定到组件实例的方法,确保内部能访问 this.applyModal.form
this.applyRules.duration = [{ validator: this.validateDuration, trigger: 'blur' }]
this.getUserDmPermission()
},
watch: {
// 监听开始时间和结束时间变化,自动计算天数
'applyModal.form.start_time' (newVal) {
if (this.suppressDurationCalc) return
// 用户修改起始时间时,先清空已有时长(避免与新时间不符),再进行自动计算
if (this.applyModal && this.applyModal.form) this.applyModal.form.duration = ''
this.calculateDuration()
},
'applyModal.form.end_time' (newVal) {
if (this.suppressDurationCalc) return
// 用户修改结束时间时,先清空已有时长(避免与新时间不符),再进行自动计算
if (this.applyModal && this.applyModal.form) this.applyModal.form.duration = ''
this.calculateDuration()
},
// 当弹窗关闭时清理选择的审批人,确保下次打开时下拉框能正确刷新显示
'applyModal.visible' (val) {
if (!val && this.applyModal && this.applyModal.form) {
......@@ -513,47 +487,21 @@ export default {
}
},
methods: {
// validator 方法:确保能通过 this 访问组件数据
validateDuration (rule, value, callback) {
const form = this.applyModal && this.applyModal.form ? this.applyModal.form : {}
const parseToDateLocal = (v) => {
if (v === null || v === undefined || v === '') return null
if (typeof v === 'number') return new Date(v)
if (typeof v === 'string') {
if (/^\d+$/.test(v)) return new Date(Number(v))
const s = v.includes('T') ? v : v.replace(' ', 'T')
const parsed = new Date(s)
return isNaN(parsed.getTime()) ? null : parsed
}
if (v instanceof Date) return isNaN(v.getTime()) ? null : v
const parsed = new Date(v)
return isNaN(parsed.getTime()) ? null : parsed
}
const sd = parseToDateLocal(form.start_time)
const ed = parseToDateLocal(form.end_time)
let fullDays = 0
if (sd && ed) {
const diff = ed.getTime() - sd.getTime()
fullDays = Math.floor(diff / (1000 * 60 * 60 * 24))
getUserDmPermission () {
getUserDmPermissionList({}).then(ret => {
console.log('ret', ret)
if (ret.data && ret.data.errcode === 0) {
const data = ret.data.data || []
for (let i = 0; i < data.length; i++) {
var ele = data[i]
if (this.power.hasOwnProperty(ele.code)) {
this.power[ele.code] = true
}
// 如果不足整天,不校验
if (fullDays < 1) { callback(); return }
if (value === null || value === undefined || String(value).trim() === '') {
callback(new Error('请填写请假时长')); return
}
const num = Number(value)
if (isNaN(num) || num <= 0) { callback(new Error('请假天数必须为大于 0 的数字')); return }
const integerPart = Math.floor(num)
if (integerPart !== fullDays) {
callback(new Error(`请假整数天应为 ${fullDays} 天,请确认`)); return
} else {
this.$Notice.error({ title: '查询失败', desc: ret.data.errmsg || '请稍后重试' })
}
callback()
})
},
// 返回给定 tab 的状态下拉选项:只有 apply tab 显示“未提交(0)”,其他 tab 隐藏该选项
statusOptionsFor (tab) {
......@@ -561,36 +509,12 @@ export default {
// 过滤掉 id 为 '0' 或 0 的未提交选项
return (this.statusOptions || []).filter(opt => String(opt.id) !== '0')
},
// 计算请假天数(只计算整天,忽略零点几部分)
calculateDuration () {
const form = this.applyModal.form
if (!form || !form.start_time || !form.end_time) return
const startDate = new Date(form.start_time)
const endDate = new Date(form.end_time)
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return
if (endDate.getTime() <= startDate.getTime()) return
// 计算整天数(向下取整),只要整天 >=1 则自动填充为整数天
const diffTime = endDate.getTime() - startDate.getTime()
const fullDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
const existing = form.duration !== null && form.duration !== undefined && String(form.duration).trim() !== ''
if (fullDays >= 1) {
// 只有当表单中尚未填写时才自动填充,避免覆盖后端/用户已有值
if (!existing) form.duration = String(fullDays)
} else {
// 少于整天时不自动计算,交由用户手动填写(例如半天、几小时)
// 不覆盖已有用户填写的值
if (!existing) form.duration = ''
}
},
handleTabChange (name) {
this.activeTab = name
if (name === 'pending') this.fetchList('pending')
if (name === 'history') this.fetchList('history')
if (name === 'query') this.fetchList('query')
if (name === 'stats') this.loadStats()
},
fetchList (tab) {
const apiMap = { apply: getLeaveListByUserId, pending: getPendingLeaveList, history: getLeaveApprovalHistory, query: getLeaveList }
......@@ -856,24 +780,22 @@ export default {
// 默认开始时间为当天(时分秒置为 0),精确到天
const today = new Date()
today.setHours(0, 0, 0, 0)
// 在赋值期间禁止自动计算,直到下一个 tick 再恢复,避免渲染期间 watcher 覆盖表单值
this.suppressDurationCalc = true
this.applyModal.form = { user_id: '', user_name: '', leave_type_id: '', start_time: today, end_time: '', duration_unit: 2, duration: '', reason: '', emergency_contact: '', emergency_phone: '', approver_id: this.selectedApprovalUser || '' }
this.$nextTick(() => { this.suppressDurationCalc = false })
this.applyModal.visible = true
},
openEdit (row) {
this.applyModal.isEdit = true
// 在赋值期间禁止自动计算,直到渲染稳定后再恢复,避免覆盖已有 duration
this.suppressDurationCalc = true
this.applyModal.form = Object.assign({}, row)
this.$nextTick(() => { this.suppressDurationCalc = false })
// ensure approver_id exists on form when editing
if (!this.applyModal.form.approver_id) this.applyModal.form.approver_id = row.approver_id || row.approverId || row.transfer_to_id || ''
// 保证与 leaveApprovalOptions 中 id 的类型一致(字符串)
if (this.applyModal.form.approver_id !== null && this.applyModal.form.approver_id !== undefined) {
this.applyModal.form.approver_id = String(this.applyModal.form.approver_id)
}
// 确保duration字段为字符串类型,避免数字0被验证为无效值
if (this.applyModal.form.duration !== null && this.applyModal.form.duration !== undefined) {
this.applyModal.form.duration = String(this.applyModal.form.duration)
}
// 编辑时需要设置leave_id为主表id,如果没有则使用id作为回退
this.applyModal.form.leave_id = row.leave_id || row.id
this.applyModal.visible = true
......@@ -904,7 +826,6 @@ export default {
const endDate = parseToDate(form.end_time)
if (startDate && endDate) {
if (endDate.getTime() <= startDate.getTime()) {
this.applyModal.saving = false
this.$Message.warning('结束时间必须大于开始时间')
return
}
......@@ -946,13 +867,9 @@ export default {
return status === 0
},
submit (row) {
this.$Modal.confirm({ title: '确认提交',
content: '确认提交此请假申请进入审批流程?',
onOk: () => {
submitLeaveApplication({ id: row.id, leave_id: row.leave_id }).then(ret => {
if (ret.data && ret.data.errcode === 0) { this.$Message.success('提交成功'); this.fetchList('apply') } else this.$Notice.error({ title: '提交失败', desc: ret.data && ret.data.errmsg })
})
} })
},
revoke (row) {
revokeLeaveApplication({ id: row.id, leave_id: row.leave_id }).then(ret => {
......@@ -1090,19 +1007,17 @@ export default {
refillApply () {
// 将批准记录数据反显到申请表单,清除 id 和 leave_id
this.applyModal.isEdit = false
// 在赋值期间禁止自动计算,直到渲染稳定后再恢复
this.suppressDurationCalc = true
this.applyModal.form = {
leave_type_id: this.approveModal.record.leave_type_id || '',
start_time: this.approveModal.record.start_time || '',
end_time: this.approveModal.record.end_time || '',
duration: this.approveModal.record.duration !== null && this.approveModal.record.duration !== undefined ? String(this.approveModal.record.duration) : '',
reason: this.approveModal.record.reason || '',
emergency_contact: this.approveModal.record.emergency_contact || '',
emergency_phone: this.approveModal.record.emergency_phone || '',
approver_id: this.approveModal.record.approver_id || this.selectedApprovalUser || ''
}
// 不保留 id 和 leave_id,保持为空
this.$nextTick(() => { this.suppressDurationCalc = false })
this.applyModal.visible = true
// 关闭审批弹窗
this.approveModal.visible = false
......@@ -1110,19 +1025,17 @@ export default {
refillApplyDirect (row) {
// 直接从列表行数据进行重新填报,清除 id 和 leave_id
this.applyModal.isEdit = false
// 在赋值期间禁止自动计算,直到渲染稳定后再恢复
this.suppressDurationCalc = true
this.applyModal.form = {
leave_type_id: row.leave_type_id || '',
start_time: row.start_time || '',
end_time: row.end_time || '',
duration: row.duration !== null && row.duration !== undefined ? String(row.duration) : '',
reason: row.reason || '',
emergency_contact: row.emergency_contact || '',
emergency_phone: row.emergency_phone || '',
approver_id: row.approver_id || this.selectedApprovalUser || ''
}
// 不保留 id 和 leave_id,保持为空
this.$nextTick(() => { this.suppressDurationCalc = false })
this.applyModal.visible = true
},
openDetail (row) {
......@@ -1165,15 +1078,32 @@ export default {
},
openHistoryDetail (row) { this.openDetail(row) },
renderApproveResult (h, { row }) { return row.approver_result === 1 ? '通过' : (row.approver_result === -1 ? '驳回' : '未处理') },
formatDatetime (val) { if (!val) return '-'; return String(val).replace('T', ' ') },
formatDatetime (val) {
if (!val) return '-';
try {
// 处理ISO格式时间字符串,如 "2023-12-31T10:30:00.000Z"
if (typeof val === 'string' && val.includes('T')) {
// 移除时区信息和毫秒,转换为本地时间格式
const date = new Date(val);
if (!isNaN(date.getTime())) {
const pad = (n) => (n < 10 ? '0' + n : String(n));
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
}
}
// 如果不是ISO格式或解析失败,返回原值的T替换版本
return String(val).replace('T', ' ');
} catch (e) {
return String(val).replace('T', ' ');
}
},
loadStats () {
this.loading.stats = true
const params = {
getLeaveStats({
start: this.statsStart,
end: this.statsEnd,
department: this.statsDept
}
getLeaveStats({ params }).then(ret => {
}).then(ret => {
if (ret.data && ret.data.errcode === 0) {
console.log('ret.data.data', ret.data.data)
this.tables.stats = ret.data.data || []
......@@ -1183,10 +1113,10 @@ export default {
}).finally(() => { this.loading.stats = false })
},
resetStats () {
this.statsStart = null
this.statsEnd = null
this.statsDept = ''
this.tables.stats = []
// 重置日期条件为默认值并直接查询数据
this.statsStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1)
this.statsEnd = new Date()
this.loadStats()
},
deleteApply (row) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论