Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
This commit is contained in:
@@ -87,7 +87,7 @@
|
||||
<div class="section-header">
|
||||
<span class="section-title">停机详情</span>
|
||||
</div>
|
||||
<div class="detail-list">
|
||||
<div>
|
||||
<div v-if="tableData.length === 0" class="empty-state">
|
||||
<span class="empty-text">暂无停机记录</span>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
<div class="section-header">
|
||||
<span class="section-title">停机趋势</span>
|
||||
</div>
|
||||
<div class="chart-wrapper trend-chart" ref="trendChart" id="trendChart"></div>
|
||||
<div class="chart-wrapper trend-chart" v-show="activeTab !== 'day'" ref="trendChart" id="trendChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -342,53 +342,7 @@ export default {
|
||||
loading.close()
|
||||
console.log('停机统计响应:', response)
|
||||
|
||||
if (response.code === 200 && response.rows && response.rows.length > 0) {
|
||||
this.tableData = response.rows.map(item => ({
|
||||
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
|
||||
duration: this.secondsToMinutes(item.duration) + 'min',
|
||||
remark: item.remark || '-',
|
||||
machine: item.unit || '-'
|
||||
}))
|
||||
|
||||
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
|
||||
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
|
||||
const totalCount = response.rows.length
|
||||
const totalAvailableMinutes = this.getTotalAvailableMinutes()
|
||||
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
|
||||
|
||||
this.summaryData = [
|
||||
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
|
||||
{ label: '停机次数', value: totalCount, unit: '次' },
|
||||
{ label: '作业率', value: workRate, unit: '%' }
|
||||
]
|
||||
|
||||
const crewMap = {}
|
||||
const typeMap = {}
|
||||
response.rows.forEach(item => {
|
||||
const crew = item.crew || '未知班组'
|
||||
const type = item.stopType || '未知类型'
|
||||
const durationMinutes = this.secondsToMinutes(item.duration)
|
||||
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
|
||||
typeMap[type] = (typeMap[type] || 0) + durationMinutes
|
||||
})
|
||||
|
||||
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
|
||||
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
|
||||
|
||||
// 渲染饼图
|
||||
this.$nextTick(() => {
|
||||
this.renderPieChart('crew', this.crewPieData)
|
||||
this.renderPieChart('type', this.typePieData)
|
||||
})
|
||||
|
||||
if (this.activeTab !== 'day') {
|
||||
if (response.rows.length > 0) {
|
||||
this.buildTrendChart(response.rows)
|
||||
} else {
|
||||
this.trendXData = []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (response.code !== 200 || !response.rows || response.rows.length === 0) {
|
||||
console.log('暂无停机数据')
|
||||
this.tableData = []
|
||||
this.summaryData = [
|
||||
@@ -403,6 +357,53 @@ export default {
|
||||
this.clearChart(this.trendChart)
|
||||
this.renderPieChart('crew', [])
|
||||
this.renderPieChart('type', [])
|
||||
return
|
||||
}
|
||||
|
||||
this.tableData = response.rows.map(item => ({
|
||||
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
|
||||
duration: this.secondsToMinutes(item.duration) + 'min',
|
||||
remark: item.remark || '-',
|
||||
machine: item.unit || '-'
|
||||
}))
|
||||
|
||||
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
|
||||
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
|
||||
const totalCount = response.rows.length
|
||||
const totalAvailableMinutes = this.getTotalAvailableMinutes()
|
||||
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
|
||||
|
||||
this.summaryData = [
|
||||
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
|
||||
{ label: '停机次数', value: totalCount, unit: '次' },
|
||||
{ label: '作业率', value: workRate, unit: '%' }
|
||||
]
|
||||
|
||||
const crewMap = {}
|
||||
const typeMap = {}
|
||||
response.rows.forEach(item => {
|
||||
const crew = item.crew || '未知班组'
|
||||
const type = item.stopType || '未知类型'
|
||||
const durationMinutes = this.secondsToMinutes(item.duration)
|
||||
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
|
||||
typeMap[type] = (typeMap[type] || 0) + durationMinutes
|
||||
})
|
||||
|
||||
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
|
||||
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
|
||||
|
||||
// 渲染饼图
|
||||
this.$nextTick(() => {
|
||||
this.renderPieChart('crew', this.crewPieData)
|
||||
this.renderPieChart('type', this.typePieData)
|
||||
})
|
||||
|
||||
if (this.activeTab !== 'day') {
|
||||
if (response.rows.length > 0) {
|
||||
this.buildTrendChart(response.rows)
|
||||
} else {
|
||||
this.trendXData = []
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
loading.close()
|
||||
@@ -807,6 +808,7 @@ export default {
|
||||
.trend-chart {
|
||||
height: 250px !important;
|
||||
min-height: 250px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
// ✅【修复】饼图容器增加固定高度,解决高度塌陷
|
||||
@@ -845,13 +847,6 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f5f7fa;
|
||||
|
||||
@@ -13,7 +13,51 @@
|
||||
<el-button type="primary" icon="el-icon-search" style="margin-left: 10px;" @click="handleQuery">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 停机统计表格 ✅【全部修改】适配真实JSON数据字段 -->
|
||||
<!-- 核心指标卡片区域 -->
|
||||
<div class="indicator-cards" style="display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap;">
|
||||
<el-card class="card-item" style="flex: 1; min-width: 200px;">
|
||||
<div class="card-content">
|
||||
<p class="card-label">停机次数</p>
|
||||
<p class="card-value">{{ stopCount }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="card-item" style="flex: 1; min-width: 200px;">
|
||||
<div class="card-content">
|
||||
<p class="card-label">停机总时长</p>
|
||||
<p class="card-value">{{ formatDuration(totalStopDuration) }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="card-item" style="flex: 1; min-width: 200px;">
|
||||
<div class="card-content">
|
||||
<p class="card-label">作业率</p>
|
||||
<p class="card-value">{{ operationRate }}%</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-container" style="display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap;">
|
||||
<!-- 按天汇总折线图 -->
|
||||
<el-card style="flex: 1; min-width: 400px;">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>按天停机统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="lineChart" style="width: 100%; height: 400px;"></div>
|
||||
</el-card>
|
||||
<!-- 按类型汇总饼图 -->
|
||||
<el-card style="flex: 1; min-width: 400px;">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>按停机类型统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="pieChart" style="width: 100%; height: 400px;"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 停机统计表格 -->
|
||||
<el-table
|
||||
:data="stoppageList"
|
||||
border
|
||||
@@ -33,14 +77,8 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="insdate" label="数据录入时间" align="center" width="200" />
|
||||
<!-- 生产相关附属字段 -->
|
||||
<el-table-column prop="coilid" label="钢卷号" align="center" width="120" />
|
||||
<el-table-column prop="shift" label="班次" align="center" width="100" />
|
||||
<!-- <el-table-column prop="crew" label="班组人员" align="center" width="120" />
|
||||
<el-table-column prop="area" label="区域" align="center" width="100" />
|
||||
<el-table-column prop="unit" label="机组" align="center" width="100" />
|
||||
<el-table-column prop="seton" label="开机人" align="center" width="100" /> -->
|
||||
<!-- 备注字段 -->
|
||||
<el-table-column prop="remark" label="备注" align="center" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
@@ -48,6 +86,14 @@
|
||||
|
||||
<script>
|
||||
import { listStoppage } from '@/api/lines/zinc/stoppage'
|
||||
// 引入ECharts核心及需要的组件
|
||||
import * as echarts from 'echarts/core'
|
||||
import { LineChart, PieChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
// 注册ECharts组件
|
||||
echarts.use([TitleComponent, TooltipComponent, LegendComponent, GridComponent, LineChart, PieChart, CanvasRenderer])
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -57,9 +103,16 @@ export default {
|
||||
loading: false, // 加载状态
|
||||
month: '', // 选中的月份 yyyy-MM格式
|
||||
queryParams: {
|
||||
startDate: '', // 开始时间 yyyy-MM-dd✅ 与后端字段名一致
|
||||
endDate: '' // 结束时间 yyyy-MM-dd✅ 与后端字段名一致
|
||||
}
|
||||
startDate: '', // 开始时间 yyyy-MM-dd
|
||||
endDate: '' // 结束时间 yyyy-MM-dd
|
||||
},
|
||||
// 指标卡数据
|
||||
stopCount: 0, // 停机次数
|
||||
totalStopDuration: 0, // 停机总时长(分钟)
|
||||
operationRate: 0, // 作业率(百分比)
|
||||
// 图表实例
|
||||
lineChartInstance: null, // 折线图实例
|
||||
pieChartInstance: null // 饼图实例
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -67,8 +120,22 @@ export default {
|
||||
this.initDefaultMonth()
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表实例
|
||||
this.initCharts()
|
||||
// 页面加载时,默认查询当月停机数据
|
||||
this.listStoppage()
|
||||
// 监听窗口大小变化,自适应图表
|
||||
window.addEventListener('resize', this.resizeCharts)
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁图表实例,防止内存泄漏
|
||||
if (this.lineChartInstance) {
|
||||
this.lineChartInstance.dispose()
|
||||
}
|
||||
if (this.pieChartInstance) {
|
||||
this.pieChartInstance.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', this.resizeCharts)
|
||||
},
|
||||
methods: {
|
||||
// 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式
|
||||
@@ -94,6 +161,7 @@ export default {
|
||||
|
||||
return result || '0分钟'
|
||||
},
|
||||
|
||||
/** 初始化默认月份和起止时间 */
|
||||
initDefaultMonth() {
|
||||
const now = new Date()
|
||||
@@ -129,6 +197,254 @@ export default {
|
||||
this.listStoppage()
|
||||
},
|
||||
|
||||
/** 初始化图表实例 */
|
||||
initCharts() {
|
||||
// 初始化折线图
|
||||
this.lineChartInstance = echarts.init(this.$refs.lineChart)
|
||||
// 初始化饼图
|
||||
this.pieChartInstance = echarts.init(this.$refs.pieChart)
|
||||
},
|
||||
|
||||
/** 图表自适应大小 */
|
||||
resizeCharts() {
|
||||
if (this.lineChartInstance) {
|
||||
this.lineChartInstance.resize()
|
||||
}
|
||||
if (this.pieChartInstance) {
|
||||
this.pieChartInstance.resize()
|
||||
}
|
||||
},
|
||||
|
||||
/** 计算核心指标数据 */
|
||||
calculateIndicators() {
|
||||
// 1. 计算停机次数
|
||||
this.stopCount = this.stoppageList.length
|
||||
|
||||
// 2. 计算停机总时长(分钟)
|
||||
this.totalStopDuration = this.stoppageList.reduce((sum, item) => {
|
||||
const duration = Number(item.duration) || 0
|
||||
return sum + duration
|
||||
}, 0)
|
||||
|
||||
// 3. 计算作业率:作业率 = (当月总分钟数 - 停机总分钟数) / 当月总分钟数 * 100%
|
||||
if (this.month) {
|
||||
const [year, month] = this.month.split('-').map(Number)
|
||||
// 获取当月的总天数
|
||||
const daysInMonth = new Date(year, month, 0).getDate()
|
||||
// 当月总分钟数
|
||||
const totalMinutesInMonth = daysInMonth * 24 * 60
|
||||
// 作业时长(分钟)
|
||||
const operationMinutes = totalMinutesInMonth - this.totalStopDuration
|
||||
// 计算作业率(保留2位小数)
|
||||
this.operationRate = totalMinutesInMonth > 0
|
||||
? (operationMinutes / totalMinutesInMonth * 100).toFixed(2)
|
||||
: 0
|
||||
} else {
|
||||
this.operationRate = 0
|
||||
}
|
||||
},
|
||||
|
||||
/** 工具函数:获取两个日期之间的所有日期(含起止日期) */
|
||||
getAllDatesBetween(startDateStr, endDateStr) {
|
||||
const dates = []
|
||||
const start = new Date(startDateStr)
|
||||
const end = new Date(endDateStr)
|
||||
// 只保留日期部分,清除时分秒
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(0, 0, 0, 0)
|
||||
|
||||
let current = new Date(start)
|
||||
while (current <= end) {
|
||||
// 转成 MM月dd日 格式
|
||||
const month = current.getMonth() + 1
|
||||
const day = current.getDate()
|
||||
dates.push(`${month}月${day}日`)
|
||||
// 日期+1
|
||||
current.setDate(current.getDate() + 1)
|
||||
}
|
||||
return dates
|
||||
},
|
||||
|
||||
/** 处理折线图数据(按天汇总停机次数和时长,跨天记录每天+1) */
|
||||
handleLineChartData() {
|
||||
if (!this.stoppageList.length) {
|
||||
// 无数据时的提示
|
||||
this.lineChartInstance.setOption({
|
||||
title: { text: '暂无停机数据', left: 'center' },
|
||||
xAxis: { data: [] },
|
||||
series: [{ data: [] }, { data: [] }]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 构建当月所有日期的数组(用于x轴)
|
||||
const [year, month] = this.month.split('-').map(Number)
|
||||
const daysInMonth = new Date(year, month, 0).getDate()
|
||||
const dateLabels = Array.from({ length: daysInMonth }, (_, i) => `${month}月${i+1}日`)
|
||||
|
||||
// 2. 初始化所有日期的统计值为0
|
||||
const dayMap = {}
|
||||
dateLabels.forEach(date => {
|
||||
dayMap[date] = { count: 0, duration: 0 }
|
||||
})
|
||||
|
||||
// 3. 遍历停机数据,按【时间区间内所有日期】统计次数,按【开始日期】统计时长
|
||||
this.stoppageList.forEach(item => {
|
||||
if (!item.startDate || !item.endDate) return
|
||||
// 获取停机区间内的所有日期
|
||||
const relateDates = this.getAllDatesBetween(item.startDate, item.endDate)
|
||||
// 遍历所有涉及日期,次数+1
|
||||
relateDates.forEach(dateKey => {
|
||||
if (dayMap[dateKey]) {
|
||||
dayMap[dateKey].count += 1
|
||||
}
|
||||
})
|
||||
// 时长:按开始日期统计(如需按天拆分时长,可在此处处理)
|
||||
const startMonth = new Date(item.startDate).getMonth() + 1
|
||||
const startDay = new Date(item.startDate).getDate()
|
||||
const startDateKey = `${startMonth}月${startDay}日`
|
||||
if (dayMap[startDateKey]) {
|
||||
dayMap[startDateKey].duration += Number(item.duration) || 0
|
||||
}
|
||||
})
|
||||
|
||||
// 4. 提取x轴和series数据
|
||||
const xAxisData = dateLabels
|
||||
const countData = xAxisData.map(date => dayMap[date].count)
|
||||
const durationData = xAxisData.map(date => dayMap[date].duration)
|
||||
|
||||
// 5. 设置折线图配置
|
||||
const lineOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
const date = params[0].name
|
||||
const count = params[0].value
|
||||
const duration = this.formatDuration(params[1].value)
|
||||
return `${date}<br/>停机次数:${count}<br/>停机时长:${duration}`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['停机次数', '停机时长(分钟)'],
|
||||
top: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xAxisData
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '停机次数',
|
||||
min: 0,
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '停机时长(分钟)',
|
||||
min: 0,
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
},
|
||||
position: 'right'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '停机次数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: countData
|
||||
},
|
||||
{
|
||||
name: '停机时长(分钟)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
yAxisIndex: 1,
|
||||
data: durationData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.lineChartInstance.setOption(lineOption)
|
||||
},
|
||||
|
||||
/** 处理饼图数据(按停机类型汇总) */
|
||||
handlePieChartData() {
|
||||
if (!this.stoppageList.length) {
|
||||
// 无数据时的提示
|
||||
this.pieChartInstance.setOption({
|
||||
title: { text: '暂无停机数据', left: 'center' },
|
||||
series: [{ data: [] }]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 按停机类型分组统计时长
|
||||
const typeMap = {}
|
||||
this.stoppageList.forEach(item => {
|
||||
const type = item.stopType || '未知类型'
|
||||
const duration = Number(item.duration) || 0
|
||||
typeMap[type] = (typeMap[type] || 0) + duration
|
||||
})
|
||||
|
||||
// 2. 转换为饼图数据格式
|
||||
const pieData = Object.entries(typeMap).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}分钟 ({d}%)'
|
||||
}
|
||||
}))
|
||||
|
||||
// 3. 设置饼图配置
|
||||
const pieOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c}分钟 ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '停机时长',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.pieChartInstance.setOption(pieOption)
|
||||
},
|
||||
|
||||
/** 核心:查询停机统计列表数据,带时间筛选参数 */
|
||||
async listStoppage() {
|
||||
this.loading = true
|
||||
@@ -138,11 +454,23 @@ export default {
|
||||
// 适配后端返回格式:数组/分页对象都兼容
|
||||
this.stoppageList = res.data || res || []
|
||||
this.total = res.total || this.stoppageList.length
|
||||
|
||||
// 计算核心指标
|
||||
this.calculateIndicators()
|
||||
// 更新图表数据
|
||||
this.handleLineChartData()
|
||||
this.handlePieChartData()
|
||||
} catch (err) {
|
||||
this.$message.error('查询停机统计数据失败!')
|
||||
console.error('停机统计查询异常:', err)
|
||||
this.stoppageList = []
|
||||
this.total = 0
|
||||
// 重置指标和图表
|
||||
this.stopCount = 0
|
||||
this.totalStopDuration = 0
|
||||
this.operationRate = 0
|
||||
this.handleLineChartData()
|
||||
this.handlePieChartData()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -162,4 +490,34 @@ export default {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
/* 指标卡样式 */
|
||||
.indicator-cards {
|
||||
width: 100%;
|
||||
}
|
||||
.card-item {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
.card-content {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1989fa;
|
||||
margin: 0;
|
||||
}
|
||||
/* 图表样式 */
|
||||
.charts-container {
|
||||
width: 100%;
|
||||
}
|
||||
.chart-header {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -37,7 +37,7 @@
|
||||
<el-form-item label="外出小时数" prop="outHours">
|
||||
<el-input v-model="form.outHours" placeholder="选择时间后自动计算,也可手动修改" />
|
||||
</el-form-item>
|
||||
<el-form-item label="外出地点" prop="outPlace">
|
||||
<el-form-item label="外出地点" prop="outPlace">
|
||||
<el-input v-model="form.outPlace" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="外出原因" prop="outReason">
|
||||
@@ -51,7 +51,8 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="text-align: center;">
|
||||
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.applyId ? '更新申请' : '提交申请' }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.outId ? '更新申请' : '提交申请'
|
||||
}}</el-button>
|
||||
<el-button @click="handleReset">重置表单</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -63,7 +64,13 @@
|
||||
<el-button style="float: right;" icon="el-icon-refresh" @click="getList">刷新</el-button>
|
||||
</template>
|
||||
<el-table v-loading="loading" :data="leaveRequestList">
|
||||
<el-table-column label="申请状态" align="center" prop="approvalStatus"></el-table-column>
|
||||
<el-table-column prop="approvalStatus" label="审批状态" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.approvalStatus)">
|
||||
{{ getStatusText(scope.row.approvalStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="外出类型" align="center" prop="outType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.hrm_out_type" :value="scope.row.outType" />
|
||||
@@ -89,9 +96,13 @@
|
||||
<el-table-column label="外出地点" align="center" prop="outPlace" show-overflow-tooltip />
|
||||
<el-table-column label="外出原因" align="center" prop="outReason" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="160">
|
||||
<template slot-scope="scope" v-if="scope.row.approvalStatus === '待审批'">
|
||||
<el-button icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)">修改</el-button>
|
||||
<el-button icon="el-icon-delete" size="mini" @click="handleWithdraw(scope.row)">撤回</el-button>
|
||||
<template slot-scope="scope">
|
||||
<el-button icon="el-icon-printer" size="mini" v-if="scope.row.approvalStatus === '已同意'"
|
||||
@click="handlePrint(scope.row)">打印</el-button>
|
||||
<el-button icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)"
|
||||
v-if="scope.row.approvalStatus === '待审批'">修改</el-button>
|
||||
<el-button icon="el-icon-delete" size="mini" @click="handleWithdraw(scope.row)"
|
||||
v-if="scope.row.approvalStatus === '待审批'">撤回</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -100,6 +111,8 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<out-label-printer ref="outLabelPrinter" :printer-info="printerInfo"></out-label-printer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,13 +122,15 @@ import { listApproval, updateApproval } from "@/api/wms/approval"
|
||||
import { listDept } from "@/api/system/dept"
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
import DictSelect from '@/components/DictSelect'
|
||||
import OutLabelPrinter from '../components/outLabelPrinter'
|
||||
|
||||
export default {
|
||||
name: 'LeaveApply',
|
||||
dicts: ['hrm_leave_shift', 'hrm_out_type', 'hrm_department', 'hrm_leave_employee'],
|
||||
components: {
|
||||
FileUpload,
|
||||
DictSelect
|
||||
DictSelect,
|
||||
OutLabelPrinter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -134,18 +149,17 @@ export default {
|
||||
},
|
||||
// 表单校验规则【核心新增:完整必填校验】
|
||||
rules: {
|
||||
leaveTitle: [{ required: true, message: '请假原因不能为空', trigger: ['blur', 'change'] }],
|
||||
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'change' }],
|
||||
applicantName: [{ required: true, message: '请假人姓名不能为空', trigger: 'change' }],
|
||||
outType: [{ required: true, message: '外出类型不能为空', trigger: 'change' }],
|
||||
applicantName: [{ required: true, message: '外出人姓名不能为空', trigger: 'change' }],
|
||||
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
|
||||
leaveShift: [{ required: true, message: '请假班次不能为空', trigger: 'change' }],
|
||||
leaveDays: [
|
||||
{ required: true, message: '请假天数不能为空', trigger: ['blur', 'change'] },
|
||||
],
|
||||
outHours: [{ required: true, message: '外出小时数不能为空', trigger: ['blur', 'change'] }],
|
||||
outPlace: [{ required: true, message: '外出地点不能为空', trigger: ['blur', 'change'] }],
|
||||
outReason: [{ required: true, message: '外出原因不能为空', trigger: ['blur', 'change'] }],
|
||||
applicantDeptName: [{ required: true, message: '审批部门不能为空', trigger: 'change' }],
|
||||
},
|
||||
deptOptions: []
|
||||
deptOptions: [],
|
||||
printerInfo: {},
|
||||
}
|
||||
},
|
||||
// 核心新增:监听开始/结束时间变化,自动计算天数
|
||||
@@ -247,6 +261,35 @@ export default {
|
||||
this.form = response.data;
|
||||
});
|
||||
},
|
||||
handlePrint(row) {
|
||||
const startYear = new Date(row.startTime).getFullYear()
|
||||
const endYear = new Date(row.endTime).getFullYear()
|
||||
const startMonth = new Date(row.startTime).getMonth() + 1
|
||||
const endMonth = new Date(row.endTime).getMonth() + 1
|
||||
const startDay = new Date(row.startTime).getDate()
|
||||
const endDay = new Date(row.endTime).getDate()
|
||||
const applyName = row.applicantName
|
||||
const approverName = row.approverName
|
||||
const reason = row.outReason
|
||||
const outHours = parseInt(row.outHours)
|
||||
const outPlace = row.outPlace
|
||||
this.printerInfo = {
|
||||
startYear,
|
||||
endYear,
|
||||
startMonth,
|
||||
endMonth,
|
||||
startDay,
|
||||
endDay,
|
||||
applyName,
|
||||
approverName,
|
||||
reason,
|
||||
outHours,
|
||||
outPlace,
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs["outLabelPrinter"].print()
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
handleSubmit() {
|
||||
this.form.outTitle = `${this.form.applicantName}-${this.form.outType}-${this.form.startTime}-${this.form.outReason || ''}`
|
||||
@@ -289,6 +332,27 @@ export default {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 获取审批状态对应的标签类型
|
||||
getStatusTagType(status) {
|
||||
const typeMap = {
|
||||
'待审批': 'warning',
|
||||
'已同意': 'success',
|
||||
'已驳回': 'danger',
|
||||
'已撤销': 'info',
|
||||
}
|
||||
return typeMap[status] || 'default'
|
||||
},
|
||||
|
||||
// 获取审批状态的中文文本
|
||||
getStatusText(status) {
|
||||
const textMap = {
|
||||
'待审批': '待审批',
|
||||
'已同意': '已同意',
|
||||
'已驳回': '已驳回',
|
||||
'已撤销': '已撤销',
|
||||
}
|
||||
return textMap[status] || '未知状态'
|
||||
},
|
||||
// 核心新增:自动计算请假天数的方法
|
||||
calculateLeaveDays() {
|
||||
const { startTime, endTime } = this.form;
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
<!-- 左侧是新增表单 -->
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
|
||||
<el-form-item label="审批部门" prop="applicantDeptName">
|
||||
<!-- <dict-select v-model="form.applicantDeptName" dict-type="hrm_department"
|
||||
placeholder="请选择审批部门"></dict-select> -->
|
||||
<el-select v-model="form.applicantDeptName" placeholder="请选择审批部门" filterable @change="getDeptLeader">
|
||||
<el-option v-for="item in deptOptions" :key="item.deptId"
|
||||
:label="item.deptName + '(' + (item.leader || '无负责人') + ')'" :value="item.deptName"></el-option>
|
||||
@@ -26,28 +24,54 @@
|
||||
<dict-select v-model="form.applicantName" dict-type="hrm_leave_employee"
|
||||
placeholder="请选择请假人姓名"></dict-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker clearable v-model="form.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择请假开始时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker clearable v-model="form.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择请假结束时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假班次" prop="leaveShift">
|
||||
<el-select v-model="form.leaveShift" placeholder="请选择请假班次">
|
||||
<el-option v-for="dict in dict.type.hrm_leave_shift" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假天数" prop="leaveDays">
|
||||
<el-input v-model="form.leaveDays" placeholder="选择时间后自动计算,也可手动修改" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" prop="leaveReason">
|
||||
<el-input v-model="form.leaveReason" type="textarea" placeholder="请输入内容" />
|
||||
|
||||
<!-- 批量请假时段区域 -->
|
||||
<div v-for="(item, index) in form.list" :key="index" class="leave-item">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span style="font-weight: 600;">请假时段 {{ index + 1 }}</span>
|
||||
<!-- 只有多行时显示删除按钮 -->
|
||||
<el-button
|
||||
v-if="form.list.length > 1"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
@click="removeLeaveItem(index)"
|
||||
style="color: #f56c6c;"
|
||||
>删除</el-button>
|
||||
</div>
|
||||
<!-- 注意:prop需要指定数组索引,支持嵌套校验 -->
|
||||
<el-form-item label="开始时间" :prop="`list[${index}].startTime`" :rules="rules.listItem.startTime">
|
||||
<el-date-picker clearable v-model="item.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择请假开始时间" @change="calculateLeaveDays(index)">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" :prop="`list[${index}].endTime`" :rules="rules.listItem.endTime">
|
||||
<el-date-picker clearable v-model="item.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择请假结束时间" @change="calculateLeaveDays(index)">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假班次" :prop="`list[${index}].leaveShift`" :rules="rules.listItem.leaveShift">
|
||||
<el-select v-model="item.leaveShift" placeholder="请选择请假班次">
|
||||
<el-option v-for="dict in dict.type.hrm_leave_shift" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假天数" :prop="`list[${index}].leaveDays`" :rules="rules.listItem.leaveDays">
|
||||
<el-input v-model="item.leaveDays" placeholder="选择时间后自动计算,也可手动修改" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" :prop="`list[${index}].leaveReason`" :rules="rules.listItem.leaveReason">
|
||||
<el-input v-model="item.leaveReason" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-divider v-if="index < form.list.length - 1"></el-divider>
|
||||
</div>
|
||||
|
||||
<!-- 新增请假时段按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="dashed" icon="el-icon-plus" @click="addLeaveItem" style="width: 100%;">
|
||||
新增请假时段
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="附件" prop="attachmentUrls">
|
||||
<FileUpload v-model="form.attachmentUrls" :max-count="1" :show-file-list="true" />
|
||||
</el-form-item>
|
||||
@@ -56,7 +80,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="text-align: center;">
|
||||
<el-button type="primary" @click="handleSubmit" v-loading="loading">{{ form.applyId ? '更新申请' : '提交申请' }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" v-loading="buttonLoading">{{ form.leaveId ? '更新申请' : '批量提交申请' }}</el-button>
|
||||
<el-button @click="handleReset">重置表单</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -68,7 +92,13 @@
|
||||
<el-button style="float: right;" icon="el-icon-refresh" @click="getList">刷新</el-button>
|
||||
</template>
|
||||
<el-table v-loading="loading" :data="leaveRequestList">
|
||||
<el-table-column label="申请状态" align="center" prop="approvalStatus"></el-table-column>
|
||||
<el-table-column prop="approvalStatus" label="审批状态" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.approvalStatus)">
|
||||
{{ getStatusText(scope.row.approvalStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请假类型" align="center" prop="leaveType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.hrm_leave_type" :value="scope.row.leaveType" />
|
||||
@@ -101,7 +131,6 @@
|
||||
<template slot-scope="scope" v-if="scope.row.approvalStatus === '待审批'">
|
||||
<el-button icon="el-icon-edit" size="mini" @click="handleEdit(scope.row)">修改</el-button>
|
||||
<el-button icon="el-icon-delete" size="mini" @click="handleWithdraw(scope.row)">撤回</el-button>
|
||||
<!-- <el-button type="primary" size="mini" @click="handleApprove(scope.row)">删除</el-button> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -130,7 +159,15 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
// 表单参数
|
||||
form: {},
|
||||
form: {
|
||||
list: [{ // 初始化默认一行请假时段
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveShift: undefined,
|
||||
leaveDays: undefined,
|
||||
leaveReason: undefined
|
||||
}]
|
||||
},
|
||||
loading: false,
|
||||
buttonLoading: false,
|
||||
leaveRequestList: [],
|
||||
@@ -142,37 +179,25 @@ export default {
|
||||
applyType: 'leave',
|
||||
createBy: this.$store.getters.name,
|
||||
},
|
||||
// 表单校验规则【核心新增:完整必填校验】
|
||||
// 表单校验规则【适配数组嵌套校验】
|
||||
rules: {
|
||||
leaveTitle: [{ required: true, message: '请假原因不能为空', trigger: ['blur', 'change'] }],
|
||||
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'change' }],
|
||||
applicantName: [{ required: true, message: '请假人姓名不能为空', trigger: 'change' }],
|
||||
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
|
||||
leaveShift: [{ required: true, message: '请假班次不能为空', trigger: 'change' }],
|
||||
leaveDays: [
|
||||
{ required: true, message: '请假天数不能为空', trigger: ['blur', 'change'] },
|
||||
],
|
||||
applicantDeptName: [{ required: true, message: '审批部门不能为空', trigger: 'change' }],
|
||||
attachmentUrls: [{ required: false }],
|
||||
remark: [{ required: false }],
|
||||
// 数组项的校验规则
|
||||
listItem: {
|
||||
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
|
||||
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
|
||||
leaveShift: [{ required: true, message: '请假班次不能为空', trigger: 'change' }],
|
||||
leaveDays: [{ required: true, message: '请假天数不能为空', trigger: ['blur', 'change'] }],
|
||||
leaveReason: [{ required: true, message: '请假原因不能为空', trigger: ['blur', 'change'] }]
|
||||
}
|
||||
},
|
||||
deptOptions: []
|
||||
}
|
||||
},
|
||||
// 核心新增:监听开始/结束时间变化,自动计算天数
|
||||
watch: {
|
||||
'form.startTime': {
|
||||
handler() {
|
||||
this.calculateLeaveDays()
|
||||
},
|
||||
immediate: false
|
||||
},
|
||||
'form.endTime': {
|
||||
handler() {
|
||||
this.calculateLeaveDays()
|
||||
},
|
||||
immediate: false
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getDeptList();
|
||||
@@ -211,6 +236,24 @@ export default {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 新增请假时段行
|
||||
addLeaveItem() {
|
||||
this.form.list.push({
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveShift: undefined,
|
||||
leaveDays: undefined,
|
||||
leaveReason: undefined
|
||||
});
|
||||
},
|
||||
// 删除指定索引的请假时段行
|
||||
removeLeaveItem(index) {
|
||||
if (this.form.list.length <= 1) {
|
||||
this.$message.warning('至少保留一行请假时段');
|
||||
return;
|
||||
}
|
||||
this.form.list.splice(index, 1);
|
||||
},
|
||||
handleReset() {
|
||||
this.reset()
|
||||
},
|
||||
@@ -218,22 +261,19 @@ export default {
|
||||
reset() {
|
||||
this.form = {
|
||||
leaveId: undefined,
|
||||
leaveTitle: undefined,
|
||||
leaveType: undefined,
|
||||
applicantName: undefined,
|
||||
applicantDeptName: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveShift: undefined,
|
||||
leaveDays: undefined,
|
||||
leaveReason: undefined,
|
||||
approverName: undefined,
|
||||
attachmentUrls: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
delFlag: undefined,
|
||||
remark: undefined
|
||||
remark: undefined,
|
||||
list: [{ // 重置后保留默认一行
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveShift: undefined,
|
||||
leaveDays: undefined,
|
||||
leaveReason: undefined
|
||||
}]
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
@@ -254,34 +294,93 @@ export default {
|
||||
const leaveId = row.applyId
|
||||
getLeaveRequest(leaveId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
const data = response.data;
|
||||
// 编辑时默认填充为单行数据(因为后端是单条记录)
|
||||
this.form = {
|
||||
...data,
|
||||
list: [{
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
leaveShift: data.leaveShift,
|
||||
leaveDays: data.leaveDays,
|
||||
leaveReason: data.leaveReason
|
||||
}]
|
||||
};
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
handleSubmit() {
|
||||
this.form.leaveTitle = `${this.form.applicantName}-${this.form.leaveType}-${this.form.startTime}-${this.form.leaveReason || ''}`
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.leaveId != null) {
|
||||
updateLeaveRequest(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.getList();
|
||||
this.reset()
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addLeaveRequest(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.getList();
|
||||
this.reset()
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
/** 批量提交按钮(核心修改) */
|
||||
async handleSubmit() {
|
||||
try {
|
||||
// 1. 先校验表单
|
||||
const valid = await new Promise((resolve) => {
|
||||
this.$refs["form"].validate((valid) => {
|
||||
resolve(valid);
|
||||
});
|
||||
});
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
this.buttonLoading = true;
|
||||
const { list, ...commonFields } = this.form; // 拆分公共字段和时段列表
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
const failReasons = [];
|
||||
|
||||
// 2. 循环处理每个时段,逐个发送请求
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
// 组装单个请求的数据(公共字段 + 当前时段字段)
|
||||
const singleRequestData = {
|
||||
...commonFields,
|
||||
startTime: item.startTime,
|
||||
endTime: item.endTime,
|
||||
leaveShift: item.leaveShift,
|
||||
leaveDays: item.leaveDays,
|
||||
leaveReason: item.leaveReason,
|
||||
// 生成单条记录的标题
|
||||
leaveTitle: `${commonFields.applicantName}-${commonFields.leaveType}-时段${i+1}-${item.startTime}-${item.leaveReason || ''}`
|
||||
};
|
||||
|
||||
try {
|
||||
if (commonFields.leaveId != null) {
|
||||
// 编辑模式:仅支持修改单条(因为后端是单条记录)
|
||||
if (list.length > 1) {
|
||||
this.$message.warning('编辑模式仅支持单条修改,已自动取第一行数据');
|
||||
await updateLeaveRequest({ ...singleRequestData, leaveId: commonFields.leaveId });
|
||||
successCount++;
|
||||
break; // 编辑时只处理第一条
|
||||
} else {
|
||||
await updateLeaveRequest({ ...singleRequestData, leaveId: commonFields.leaveId });
|
||||
successCount++;
|
||||
}
|
||||
} else {
|
||||
// 新增模式:批量提交多条
|
||||
await addLeaveRequest(singleRequestData);
|
||||
successCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
failReasons.push(`时段${i+1}提交失败:${error.message || '未知错误'}`);
|
||||
// 失败后继续提交下一条,不中断批量操作
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 提交完成后反馈结果
|
||||
if (failCount === 0) {
|
||||
this.$modal.msgSuccess(`批量提交成功!共提交${successCount}条请假申请`);
|
||||
} else {
|
||||
this.$modal.msgWarning(`批量提交完成!成功${successCount}条,失败${failCount}条\n失败原因:${failReasons.join(';')}`);
|
||||
}
|
||||
|
||||
// 4. 刷新列表并重置表单
|
||||
this.getList();
|
||||
this.reset();
|
||||
} catch (error) {
|
||||
this.$modal.msgError('批量提交异常:' + error.message);
|
||||
} finally {
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
},
|
||||
handleWithdraw(row) {
|
||||
this.$modal.confirm('是否确认撤回请假申请编号为"' + row.applyId + '"的数据项?').then(() => {
|
||||
@@ -299,27 +398,56 @@ export default {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 核心新增:自动计算请假天数的方法
|
||||
calculateLeaveDays() {
|
||||
const { startTime, endTime } = this.form;
|
||||
// 获取审批状态对应的标签类型
|
||||
getStatusTagType(status) {
|
||||
const typeMap = {
|
||||
'待审批': 'warning',
|
||||
'已同意': 'success',
|
||||
'已驳回': 'danger',
|
||||
'已撤销': 'info',
|
||||
}
|
||||
return typeMap[status] || 'default'
|
||||
},
|
||||
// 获取审批状态的中文文本
|
||||
getStatusText(status) {
|
||||
const textMap = {
|
||||
'待审批': '待审批',
|
||||
'已同意': '已同意',
|
||||
'已驳回': '已驳回',
|
||||
'已撤销': '已撤销',
|
||||
}
|
||||
return textMap[status] || '未知状态'
|
||||
},
|
||||
// 计算指定行的请假天数
|
||||
calculateLeaveDays(index) {
|
||||
const item = this.form.list[index];
|
||||
// 两个时间都选择后才计算
|
||||
if (startTime && endTime) {
|
||||
if (item.startTime && item.endTime) {
|
||||
// 转成时间戳
|
||||
const start = new Date(startTime).getTime();
|
||||
const end = new Date(endTime).getTime();
|
||||
const start = new Date(item.startTime).getTime();
|
||||
const end = new Date(item.endTime).getTime();
|
||||
// 判断结束时间不能小于开始时间
|
||||
if (end < start) {
|
||||
this.$modal.msgWarning('结束时间不能早于开始时间,请重新选择!');
|
||||
this.form.leaveDays = undefined;
|
||||
item.leaveDays = undefined;
|
||||
return;
|
||||
}
|
||||
// 计算时间差(毫秒) → 转天 → 保留2位小数
|
||||
const diffTime = end - start;
|
||||
const diffDays = (diffTime / (1000 * 60 * 60 * 24)).toFixed(2);
|
||||
// 赋值到天数输入框
|
||||
this.form.leaveDays = diffDays;
|
||||
// 赋值到对应行的天数输入框
|
||||
item.leaveDays = diffDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.leave-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
320
klp-ui/src/views/wms/hrm/components/outLabelPrinter.vue
Normal file
320
klp-ui/src/views/wms/hrm/components/outLabelPrinter.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 点击打印后为表单赋值,打印内容为表单内容。 -->
|
||||
<div ref="printer" class="print-container" v-show="false">
|
||||
<div class="print-content">
|
||||
<div class="print-stub-title">存根</div>
|
||||
<div class="print-stub-content">
|
||||
<div class="print-stub-line">
|
||||
<h4>外出条</h4>
|
||||
<b class="print-stub-year" style="float: right; margin-top: 5px;">
|
||||
<span>{{ printerInfo.startYear }}</span>年
|
||||
</b>
|
||||
</div>
|
||||
<div class="print-stub-apply-info">
|
||||
<div class="print-stub-applicant">请假人:<span>{{ printerInfo.applyName }}</span></div>
|
||||
<div class="print-apply-date">
|
||||
<div class="print-stub-date">
|
||||
<span>{{ printerInfo.startMonth }}</span>月
|
||||
<span>{{ printerInfo.startDay }}</span>日 至
|
||||
</div>
|
||||
<div class="print-stub-days">
|
||||
<span>{{ printerInfo.startMonth }}</span>月
|
||||
<span>{{ printerInfo.startDay }}</span>日 计
|
||||
<span>{{ printerInfo.outHours }}</span>小时
|
||||
</div>
|
||||
</div>
|
||||
<div class="print-stub-reason">事由:<span>{{ printerInfo.reason }}</span></div>
|
||||
<div class="print-stub-reason">外出地点:<span>{{ printerInfo.outPlace }}</span></div>
|
||||
<div class="print-stub-approver">批准人:<span>{{ printerInfo.approverName }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 竖虚线分割 -->
|
||||
<div class="print-vertical-line"></div>
|
||||
<div class="print-document">
|
||||
<div class="print-document-line">
|
||||
<h4>外出条</h4>
|
||||
</div>
|
||||
<div class="print-document-apply-info">
|
||||
<div class="print-document-applicant">请假人:<span>{{ printerInfo.applyName }}</span></div>
|
||||
<div class="print-apply-date">
|
||||
<span>{{ printerInfo.startYear }}</span>年
|
||||
<span>{{ printerInfo.startMonth }}</span>月
|
||||
<span>{{ printerInfo.startDay }}</span>日 至
|
||||
<span>{{ printerInfo.endYear }}</span>年
|
||||
<span>{{ printerInfo.endMonth }}</span>月
|
||||
<span>{{ printerInfo.endDay }}</span>日 计
|
||||
<span>{{ printerInfo.outHours }}</span>小时
|
||||
</div>
|
||||
<div class="print-document-reason">事由:<span>{{ printerInfo.reason }}</span></div>
|
||||
<div class="print-document-reason">外出地点:<span>{{ printerInfo.outPlace }}</span></div>
|
||||
<div class="print-document-approver">批准人:<span>{{ printerInfo.approverName }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="print-stub-sign" src="http://140.143.206.120:10900/klp-oa/files/2025/12/30/1b8230d62d324fe498024b08ae4acc1d.png" alt=""></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import html2canvas from 'html2canvas';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
// 假设你用的是Element UI的Message,若不是可替换为其他提示组件
|
||||
import { Message } from 'element-ui';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
printerInfo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 参考原方法:等待所有资源(图片、字体、二维码等)加载完成
|
||||
async waitForAllResources(container) {
|
||||
return new Promise((resolve) => {
|
||||
// 等待图片加载
|
||||
const images = container.querySelectorAll('img');
|
||||
const imagePromises = Array.from(images).map(img => {
|
||||
if (img.complete) return Promise.resolve();
|
||||
return new Promise(resolve => {
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve; // 加载失败也继续
|
||||
});
|
||||
});
|
||||
|
||||
// 等待字体加载(可选,根据实际需求)
|
||||
const fontPromise = new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// 等待所有资源加载完成
|
||||
Promise.all([...imagePromises, fontPromise]).then(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
// 重构后的打印方法(先生成PDF再打印)
|
||||
async print() {
|
||||
// 1. 获取打印容器DOM
|
||||
const printContainer = this.$refs.printer?.querySelector('.print-content');
|
||||
if (!printContainer) {
|
||||
Message.error('未找到打印容器,无法打印');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Message.info('正在准备打印内容,请稍等...');
|
||||
|
||||
// 2. 定义外出条纸张尺寸(自定义,可根据实际需求调整)
|
||||
// 外出条建议尺寸:宽210mm(A5宽度),高140mm(适配内容高度)
|
||||
const paperWidthMm = 240; // 纸张宽度(毫米)
|
||||
const paperHeightMm = 100; // 纸张高度(毫米)
|
||||
|
||||
// 3. 创建临时隐藏容器,模拟打印布局(参考原方法)
|
||||
const originalParent = printContainer.parentNode;
|
||||
const originalNext = printContainer.nextSibling;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.position = 'fixed';
|
||||
wrapper.style.left = '-100000px'; // 隐藏在可视区域外
|
||||
wrapper.style.top = '0';
|
||||
wrapper.style.width = `${paperWidthMm}mm`; // 固定纸张宽度
|
||||
wrapper.style.height = `${paperHeightMm}mm`; // 固定纸张高度
|
||||
wrapper.style.boxSizing = 'border-box';
|
||||
wrapper.style.backgroundColor = '#ffffff';
|
||||
wrapper.style.overflow = 'hidden';
|
||||
wrapper.style.padding = '10mm'; // 内边距,适配打印内容
|
||||
|
||||
// 设置打印内容的布局,保证居中适配
|
||||
printContainer.style.width = '100%';
|
||||
printContainer.style.height = '100%';
|
||||
printContainer.style.display = 'flex';
|
||||
printContainer.style.alignItems = 'center';
|
||||
printContainer.style.justifyContent = 'center';
|
||||
|
||||
wrapper.appendChild(printContainer);
|
||||
document.body.appendChild(wrapper);
|
||||
|
||||
// 4. 等待资源加载完成(字体、图片等)
|
||||
await this.waitForAllResources(printContainer);
|
||||
// 等待布局稳定
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 5. 用html2canvas生成高清Canvas
|
||||
const containerWidth = printContainer.offsetWidth;
|
||||
const containerHeight = printContainer.offsetHeight;
|
||||
const canvas = await html2canvas(printContainer, {
|
||||
backgroundColor: '#ffffff',
|
||||
scale: 3, // 3倍缩放,保证PDF清晰度
|
||||
useCORS: true,
|
||||
willReadFrequently: true, // 优化Canvas读取性能
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
windowWidth: containerWidth,
|
||||
windowHeight: containerHeight,
|
||||
});
|
||||
|
||||
// 6. 使用pdf-lib生成单页PDF(占满整张纸,精准适配)
|
||||
const mmToPt = 72 / 25.4; // 毫米转PDF的点(pt):1pt = 25.4/72 mm
|
||||
const pageWidthPt = paperWidthMm * mmToPt;
|
||||
const pageHeightPt = paperHeightMm * mmToPt;
|
||||
|
||||
// 创建PDF文档
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
// 嵌入Canvas生成的PNG图片
|
||||
const imgPng = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
|
||||
// 添加PDF页面,设置页面尺寸与纸张一致
|
||||
const page = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
|
||||
|
||||
// 绘制图片,占满整个PDF页面(无边距)
|
||||
page.drawImage(imgPng, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pageWidthPt,
|
||||
height: pageHeightPt
|
||||
});
|
||||
|
||||
// 保存PDF并生成Blob URL
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
const pdfUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 7. 打开PDF窗口打印(兼容弹窗拦截)
|
||||
const printWin = window.open(pdfUrl, '_blank');
|
||||
if (!printWin) {
|
||||
// 弹窗被拦截时,改为下载PDF
|
||||
Message.warning('浏览器弹窗被拦截,已为你下载PDF文件,请手动打开打印');
|
||||
const a = document.createElement('a');
|
||||
a.href = pdfUrl;
|
||||
a.download = `外出条_${new Date().getTime()}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
// 8. 还原DOM结构和样式
|
||||
// 恢复printContainer的原始样式
|
||||
printContainer.style.width = '';
|
||||
printContainer.style.height = '';
|
||||
printContainer.style.display = '';
|
||||
printContainer.style.alignItems = '';
|
||||
printContainer.style.justifyContent = '';
|
||||
// 把printContainer放回原位置
|
||||
if (originalParent) {
|
||||
if (originalNext) {
|
||||
originalParent.insertBefore(printContainer, originalNext);
|
||||
} else {
|
||||
originalParent.appendChild(printContainer);
|
||||
}
|
||||
}
|
||||
// 移除临时容器
|
||||
document.body.removeChild(wrapper);
|
||||
|
||||
} catch (error) {
|
||||
console.error('外出条打印准备失败:', error);
|
||||
Message.error('打印内容准备失败,请重试');
|
||||
|
||||
// 异常时也要还原DOM,避免布局错乱
|
||||
const printContainer = this.$refs.printer?.querySelector('.print-content');
|
||||
if (printContainer) {
|
||||
printContainer.style.width = '';
|
||||
printContainer.style.height = '';
|
||||
printContainer.style.display = '';
|
||||
printContainer.style.alignItems = '';
|
||||
printContainer.style.justifyContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.print-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
// 图章
|
||||
.print-stub-sign {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 60px;
|
||||
}
|
||||
|
||||
.print-content {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
overflow: visible;
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.print-stub-title {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
writing-mode: vertical-rl; // 文字纵向排列
|
||||
}
|
||||
|
||||
.print-vertical-line {
|
||||
width: 1px;
|
||||
height: 280px;
|
||||
border-left: 1px dashed #000;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.print-document,
|
||||
.print-stub-content {
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.print-document-line,
|
||||
.print-stub-line {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
margin: 0; // 清除默认margin,避免布局偏移
|
||||
}
|
||||
|
||||
.print-document > div > div,
|
||||
.print-stub-content > div > div {
|
||||
border: 1px solid #000;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.print-apply-date {
|
||||
height: 54px;
|
||||
line-height: 44px;
|
||||
|
||||
.print-stub-date,
|
||||
.print-stub-days {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,36 +40,36 @@
|
||||
<div class="custom-tabs-nav">
|
||||
<div
|
||||
class="custom-tabs-item"
|
||||
:class="{ active: queryParams.approveStatus === undefined || queryParams.approveStatus === '' }"
|
||||
@click="handleTabClick('approveStatus', '')"
|
||||
:class="{ active: queryParams.approvalStatus === undefined || queryParams.approvalStatus === '' }"
|
||||
@click="handleTabClick('approvalStatus', '')"
|
||||
>
|
||||
全部
|
||||
</div>
|
||||
<div
|
||||
class="custom-tabs-item"
|
||||
:class="{ active: queryParams.approveStatus === 'pending' }"
|
||||
@click="handleTabClick('approveStatus', 'pending')"
|
||||
:class="{ active: queryParams.approvalStatus === '待审批' }"
|
||||
@click="handleTabClick('approvalStatus', '待审批')"
|
||||
>
|
||||
待审批
|
||||
</div>
|
||||
<div
|
||||
class="custom-tabs-item"
|
||||
:class="{ active: queryParams.approveStatus === 'approved' }"
|
||||
@click="handleTabClick('approveStatus', 'approved')"
|
||||
:class="{ active: queryParams.approvalStatus === '已同意' }"
|
||||
@click="handleTabClick('approvalStatus', '已同意')"
|
||||
>
|
||||
已审批
|
||||
已同意
|
||||
</div>
|
||||
<div
|
||||
class="custom-tabs-item"
|
||||
:class="{ active: queryParams.approveStatus === 'rejected' }"
|
||||
@click="handleTabClick('approveStatus', 'rejected')"
|
||||
:class="{ active: queryParams.approvalStatus === '已驳回' }"
|
||||
@click="handleTabClick('approvalStatus', '已驳回')"
|
||||
>
|
||||
已驳回
|
||||
</div>
|
||||
<div
|
||||
class="custom-tabs-item"
|
||||
:class="{ active: queryParams.approveStatus === 'withdrawn' }"
|
||||
@click="handleTabClick('approveStatus', 'withdrawn')"
|
||||
:class="{ active: queryParams.approvalStatus === '已撤销' }"
|
||||
@click="handleTabClick('approvalStatus', '已撤销')"
|
||||
>
|
||||
已撤回
|
||||
</div>
|
||||
@@ -320,10 +320,12 @@ export default {
|
||||
}).then(async () => {
|
||||
this.buttonLoading = true
|
||||
try {
|
||||
const approvalTime = this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
|
||||
// 这里替换为你的审批接口调用
|
||||
await updateApproval({
|
||||
approvalId: row.approvalId,
|
||||
approvalStatus: '已同意',
|
||||
approvalTime: approvalTime,
|
||||
})
|
||||
this.$message.success('审批通过成功!')
|
||||
this.getTodoList() // 重新查询列表
|
||||
@@ -344,10 +346,12 @@ export default {
|
||||
}).then(async ({ value }) => {
|
||||
this.buttonLoading = true
|
||||
try {
|
||||
const approvalTime = this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
|
||||
// 这里替换为你的驳回接口调用
|
||||
await updateApproval({
|
||||
approvalId: row.approvalId,
|
||||
approvalStatus: '已驳回'
|
||||
approvalStatus: '已驳回',
|
||||
approvalTime: approvalTime,
|
||||
})
|
||||
this.$message.success('驳回成功!')
|
||||
this.getTodoList() // 重新查询列表
|
||||
@@ -371,7 +375,7 @@ export default {
|
||||
'待审批': 'warning',
|
||||
'已同意': 'success',
|
||||
'已驳回': 'danger',
|
||||
'已撤消': 'info',
|
||||
'已撤销': 'info',
|
||||
}
|
||||
return typeMap[status] || 'default'
|
||||
},
|
||||
@@ -382,7 +386,7 @@ export default {
|
||||
'待审批': '待审批',
|
||||
'已同意': '已同意',
|
||||
'已驳回': '已驳回',
|
||||
'已撤消': '已撤消',
|
||||
'已撤销': '已撤销',
|
||||
}
|
||||
return textMap[status] || '未知状态'
|
||||
},
|
||||
@@ -411,17 +415,11 @@ export default {
|
||||
|
||||
.filter-container {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
/* padding: 20px; */
|
||||
border-radius: 4px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.filter-container h4 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 自定义tabs样式 */
|
||||
.custom-tabs {
|
||||
margin-bottom: 20px;
|
||||
@@ -438,14 +436,14 @@ export default {
|
||||
.custom-tabs-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-tabs-item {
|
||||
padding: 10px 15px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
@@ -459,9 +457,9 @@ export default {
|
||||
}
|
||||
|
||||
.custom-tabs-item.active {
|
||||
background: #409eff;
|
||||
background: #667996;
|
||||
color: #ffffff;
|
||||
border-color: #409eff;
|
||||
border-color: #667996;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
|
||||
Reference in New Issue
Block a user