Merge remote-tracking branch 'gitee/0.8.X' into 0.8.X

This commit is contained in:
2026-01-19 13:56:15 +08:00
71 changed files with 8433 additions and 235 deletions

View File

@@ -102,10 +102,10 @@
<groupId>com.klp</groupId>
<artifactId>klp-erp</artifactId>
</dependency>
<dependency>
<groupId>com.klp</groupId>
<artifactId>klp-hrm</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.klp</groupId>-->
<!-- <artifactId>klp-hrm</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.klp</groupId>
<artifactId>klp-crm</artifactId>

View File

@@ -0,0 +1,18 @@
import zinc1Request from '@/utils/zinc1Request'
// 获取当前生产中的计划信息
export function getCurrentPlan() {
return zinc1Request({
url: '/business/dashboard/currentPlan',
method: 'get'
})
}
// 获取当前生产卷的关键工艺参数
export function getCurrentProcess() {
return zinc1Request({
url: '/business/dashboard/currentProcess',
method: 'get'
})
}

View File

@@ -0,0 +1,20 @@
import zinc1Request from '@/utils/zinc1Request'
// 生产实绩汇总
export function getReportSummary(params) {
return zinc1Request({
url: '/report/summary',
method: 'get',
params
})
}
// 生产实绩明细
export function getReportDetails(params) {
return zinc1Request({
url: '/report/details',
method: 'get',
params
})
}

View File

@@ -0,0 +1,11 @@
import zinc1Request from '@/utils/zinc1Request'
// 停机记录列表
export function listStoppage(data) {
return zinc1Request({
url: '/stoppage/list',
method: 'post',
data
})
}

View File

@@ -0,0 +1,9 @@
import zinc1Request from '@/utils/zinc1Request'
export function listDeviceEnumAll() {
return zinc1Request({
url: '/api/deviceEnum/all',
method: 'get'
})
}

View File

@@ -0,0 +1,9 @@
import zinc1Request from '@/utils/zinc1Request'
export function getDeviceFieldMetaAll() {
return zinc1Request({
url: '/api/deviceFieldMeta/all',
method: 'get'
})
}

View File

@@ -0,0 +1,20 @@
import zinc1Request from '@/utils/zinc1Request'
// 获取最新N条设备快照
export function listDeviceSnapshotLatest(params) {
return zinc1Request({
url: '/api/deviceSnapshot/latest',
method: 'get',
params
})
}
// 按时间范围查询设备快照
export function listDeviceSnapshotRange(params) {
return zinc1Request({
url: '/api/deviceSnapshot/range',
method: 'get',
params
})
}

View File

@@ -0,0 +1,162 @@
import request from '@/utils/request'
// =========================== 状态定义相关 ===========================
/**
* 获取所有状态定义及其当前值(用于初始化缓存)
* 返回数据结构:
* {
* id: 定义ID (对应VALUE字段编号如id=14对应VALUE14),
* name: 指标名称,
* units: 单位,
* comments: 说明,
* currentValue: 当前值,
* currentInsdate: 当前值时间
* }
*/
export function getAllPlantStateDefines() {
return request({
url: '/pocket/proPlantStateDefine/allWithValues',
method: 'get'
})
}
// =========================== 当前数据相关 ===========================
// 查询设备状态当前数据列表
export function listPlantStateCurrent(query) {
return request({
url: '/pocket/proPlantStateCurrent/selectAll',
method: 'get',
params: query
})
}
// 查询单条设备状态当前数据
export function getPlantStateCurrent(type, insdate) {
return request({
url: '/pocket/proPlantStateCurrent/one',
method: 'get',
params: { type, insdate }
})
}
// 查询设备状态历史数据列表
export function listPlantStateHistory(query) {
return request({
url: '/pocket/proPlantStateHistory/list',
method: 'get',
params: query
})
}
// 查询设备状态历史数据详情
export function getPlantStateHistory(insdate) {
return request({
url: '/pocket/proPlantStateHistory/' + insdate,
method: 'get'
})
}
// 查询停机列表
export function listStoppage(query) {
return request({
url: '/pocket/proStoppage/list',
method: 'get',
params: query
})
}
// 查询停机详情
export function getStoppage(stopId) {
return request({
url: '/pocket/proStoppage/' + stopId,
method: 'get'
})
}
// =========================== 班组信息相关 ===========================
/**
* 获取当前班组信息
* 返回数据结构:
* {
* shift: 班次 (如A/B/C 或 早/中/晚),
* crew: 班组编号,
* seqNum: 序列号,
* sysTime: 系统时间
* }
*/
export function getCurrentShift() {
return request({
url: '/pocket/shiftCurrent/current',
method: 'get'
})
}
// =========================== 生产统计相关 ===========================
/**
* 获取生产统计汇总数据
* @param {string} startDate - 开始日期 (yyyy-MM-dd)
* @param {string} endDate - 结束日期 (yyyy-MM-dd)
*/
export function getProductionSummary(startDate, endDate) {
return request({
url: '/pocket/productionStatistics/summary',
method: 'get',
params: { startDate, endDate }
})
}
/**
* 获取班组产量统计
* @param {string} startDate - 开始日期
* @param {string} endDate - 结束日期
*/
export function getCrewProduction(startDate, endDate) {
return request({
url: '/pocket/productionStatistics/crewProduction',
method: 'get',
params: { startDate, endDate }
})
}
/**
* 获取厚度分布统计
* @param {string} startDate - 开始日期
* @param {string} endDate - 结束日期
*/
export function getThicknessDistribution(startDate, endDate) {
return request({
url: '/pocket/productionStatistics/thicknessDistribution',
method: 'get',
params: { startDate, endDate }
})
}
/**
* 获取宽度分布统计
* @param {string} startDate - 开始日期
* @param {string} endDate - 结束日期
*/
export function getWidthDistribution(startDate, endDate) {
return request({
url: '/pocket/productionStatistics/widthDistribution',
method: 'get',
params: { startDate, endDate }
})
}
/**
* 获取班组绩效统计
* @param {string} startDate - 开始日期
* @param {string} endDate - 结束日期
*/
export function getTeamPerformance(startDate, endDate) {
return request({
url: '/pocket/productionStatistics/teamPerformance',
method: 'get',
params: { startDate, endDate }
})
}

View File

@@ -194,13 +194,14 @@ export function cancelExportCoil(coilId) {
}
// 检查入场钢卷号或当前钢卷号是否合法(是否存在)
export function checkCoilNo({ currentCoilNo, enterCoilNo }) {
export function checkCoilNo({ currentCoilNo, enterCoilNo, coilId }) {
return request({
url: '/wms/materialCoil/checkCoilNoDuplicate',
method: 'get',
params: {
currentCoilNo,
enterCoilNo
enterCoilNo,
coilId
}
})
}
@@ -214,4 +215,14 @@ export function getMaxCoilNo(enterCoilNoPrefix) {
enterCoilNoPrefix
}
})
}
/**
* 查询存在重号的钢卷
*/
export function getDuplicateGroups() {
return request({
url: '/wms/materialCoil/duplicateGroups',
method: 'get'
})
}

View File

@@ -0,0 +1,72 @@
import request from '@/utils/request'
// 查询员工请假申请列表
export function listLeaveRequest(query) {
return request({
url: '/wms/leaveRequest/list',
method: 'get',
params: query
})
}
// 查询员工请假申请详细
export function getLeaveRequest(leaveId) {
return request({
url: '/wms/leaveRequest/' + leaveId,
method: 'get'
})
}
// 新增员工请假申请
export function addLeaveRequest(data) {
return request({
url: '/wms/leaveRequest',
method: 'post',
data: data
})
}
// 修改员工请假申请
export function updateLeaveRequest(data) {
return request({
url: '/wms/leaveRequest',
method: 'put',
data: data
})
}
// 删除员工请假申请
export function delLeaveRequest(leaveId) {
return request({
url: '/wms/leaveRequest/' + leaveId,
method: 'delete'
})
}
// 报表相关接口
// 按请假类型统计
export function getLeaveTypeCount(query) {
return request({
url: '/wms/leaveRequest/report/leaveType',
method: 'get',
params: query
})
}
// 按部门统计
export function getDeptCount(query) {
return request({
url: '/wms/leaveRequest/report/dept',
method: 'get',
params: query
})
}
// 按月份统计
export function getMonthCount(query) {
return request({
url: '/wms/leaveRequest/report/monthly',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,72 @@
import request from '@/utils/request'
// 查询部门报餐主列表
export function listMealReport(query) {
return request({
url: '/wms/mealReport/list',
method: 'get',
params: query
})
}
// 查询部门报餐主详细
export function getMealReport(reportId) {
return request({
url: '/wms/mealReport/' + reportId,
method: 'get'
})
}
// 新增部门报餐主
export function addMealReport(data) {
return request({
url: '/wms/mealReport',
method: 'post',
data: data
})
}
// 修改部门报餐主
export function updateMealReport(data) {
return request({
url: '/wms/mealReport',
method: 'put',
data: data
})
}
// 删除部门报餐主
export function delMealReport(reportId) {
return request({
url: '/wms/mealReport/' + reportId,
method: 'delete'
})
}
// 报表相关接口
// 按餐别统计
export function getMealTypeCount(query) {
return request({
url: '/wms/mealReport/report/mealType',
method: 'get',
params: query
})
}
// 按部门统计
export function getDeptCount(query) {
return request({
url: '/wms/mealReport/report/dept',
method: 'get',
params: query
})
}
// 按日期统计
export function getDateCount(query) {
return request({
url: '/wms/mealReport/report/date',
method: 'get',
params: query
})
}

View File

@@ -161,7 +161,8 @@ export default {
pageSize: 10,
currentCoilNo: null,
grade: null,
selectType: 'raw_material',
selectType: 'product',
status: 0, // 不包含已发货的钢卷
dataType: 1 // 只查询当前数据,不查询历史数据
},
columns: defaultColumns,

View File

@@ -0,0 +1,317 @@
<template>
<div style="display: flex; align-items: center;" v-loading="loading">
<!-- 下拉选择器绑定计算属性做双向绑定保留原有样式+清空功能 -->
<el-select v-model="innerValue" :placeholder="placeholder" clearable filterable style="width: 200px;">
<el-option
v-for="item in dictOptions"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
<!-- 编辑按钮点击打开弹窗 -->
<div
v-if="editable"
@click="openDictDialog"
style="cursor: pointer; height: 24px; width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;"
>
<i class="el-icon-setting"></i>
</div>
<div
v-if="refresh"
@click="handleRefresh"
style="cursor: pointer; height: 24px; width: 24px; margin-top: 4px; display: flex; align-items: center; justify-content: center; border: 1px solid #828991; margin-left: 8px; border-radius: 2px;"
>
<i class="el-icon-refresh"></i>
</div>
<!-- 字典编辑弹窗 -->
<el-dialog
v-if="editable"
:visible.sync="open"
title="字典数据配置"
width="600px"
append-to-body
>
<!-- 快捷新增表单区域仅做新增无编辑功能 -->
<el-form
ref="dictFormRef"
:model="form"
:rules="dictRules"
label-width="68px"
label-position="left"
style="margin-bottom: 20px;"
>
<el-row :gutter="15">
<el-col :span="kisv ? 24 : 12">
<el-form-item label="值/标签" prop="dictValue" v-if="kisv">
<el-input v-model="form.dictValue" placeholder="请输入字典值(标签自动同步)" />
</el-form-item>
<el-form-item label="字典标签" prop="dictLabel" v-else>
<el-input v-model="form.dictLabel" placeholder="请输入字典标签" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="!kisv">
<el-form-item label="字典值" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入字典值" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="margin-bottom: 20px;">
<el-button type="primary" size="small" @click="submitAddDict" :loading="btnLoading">新增字典项</el-button>
<el-button size="small" @click="resetDictForm">清空表单</el-button>
<span style="margin-left: 10px;">提示双击单元格中的文字可以快速编辑</span>
</div>
<!-- 字典列表 核心修改双击单元格激活输入框失焦还原文本 -->
<el-table
:data="dictOptions"
border
stripe
size="mini"
style="width: 100%;"
empty-text="暂无字典数据"
>
<!-- 字典标签列kisv=true隐藏false正常展示 -->
<el-table-column label="字典标签" align="center" v-if="!kisv">
<template #default="scope">
<!-- 双击span触发编辑失焦/回车后变回span -->
<span
v-if="editRowId !== scope.row.dictCode"
@dblclick.stop="handleDbEdit(scope.row)"
style="cursor: pointer;"
>
{{ scope.row.dictLabel }}
</span>
<el-input
v-else
v-model="scope.row.dictLabel"
size="mini"
style="width: 100%;"
@change="handleSaveRow(scope.row)"
auto-focus
/>
</template>
</el-table-column>
<!-- 字典值列kisv=true时标题合并 -->
<el-table-column :label="kisv ? '字典值/标签' : '字典值'" align="center">
<template #default="scope">
<!-- 核心默认文本双击才显示输入框失焦立刻消失 -->
<span
v-if="editRowId !== scope.row.dictCode"
@dblclick.stop="handleDbEdit(scope.row)"
style="cursor: pointer;"
>
{{ scope.row.dictValue }}
</span>
<el-input
v-else
v-model="scope.row.dictValue"
size="mini"
style="width: 100%;"
@input="() => handleKisvTableSync(scope.row)"
@change="handleSaveRow(scope.row)"
auto-focus
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="scope">
<el-button
icon="el-icon-delete"
size="mini"
type="text"
text-danger
@click="handleDelete(scope.row)"
:loading="delBtnLoading === scope.row.dictCode"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
import { getDicts, addData, updateData, delData, listData } from '@/api/system/dict/data'
import { listType } from '@/api/system/dict/type'
export default {
name: 'DictSelectEdit',
props: {
dictType: { type: String, default: '' },
editable: { type: Boolean, default: true },
kisv: { type: Boolean, default: false },
value: { type: String, default: '' },
placeholder: { type: String, default: '请选择' },
refresh: { type: Boolean, default: true },
},
data() {
return {
dictOptions: [],
dictId: '',
loading: false,
btnLoading: false,
delBtnLoading: '',
open: false,
editRowId: '', // 控制当前编辑行,为空则所有单元格都是文本状态
form: {
dictId: '',
dictLabel: '',
dictValue: '',
dictType: '',
sort: 0
},
dictRules: {
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
},
dictFormRef: null
}
},
computed: {
innerValue: {
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
watch: {
dictType: {
async handler(newVal) {
if (newVal) {
this.loading = true
try {
const dictId = await this.getDictId(newVal)
await this.getDictOptions(dictId)
} catch (err) {
console.error('加载字典失败:', err)
} finally {
this.loading = false
}
}
},
immediate: true
},
'form.dictValue': {
handler(val) {
if (this.kisv && val) this.form.dictLabel = val
},
immediate: true
}
},
methods: {
async getDictId(type) {
const res = await listType({ dictType: type })
if (res.rows?.length !== 1) {
this.$message.error('字典类型异常,未查询到对应配置')
return Promise.reject('字典类型异常')
}
this.dictId = res.rows[0].dictId
return this.dictId
},
// 新增:刷新字典数据
async handleRefresh() {
this.loading = true
try {
await this.getDictOptions(this.dictId)
} catch (err) {
console.error('刷新字典失败:', err)
} finally {
this.loading = false
}
},
async getDictOptions(dictId) {
const res = await listData({ dictType: this.dictType, pageSize: 1000 })
this.dictOptions = res.rows || []
return this.dictOptions
},
openDictDialog() {
this.open = true
this.resetDictForm()
this.form.dictId = this.dictId
this.form.dictType = this.dictType
this.editRowId = '' // 打开弹窗重置编辑状态
},
resetDictForm() {
this.form = { dictId: this.dictId, dictLabel: '', dictValue: '', dictType: this.dictType, sort: 0 }
this.$refs.dictFormRef && this.$refs.dictFormRef.clearValidate()
},
handleKisvTableSync(row) {
if (this.kisv && row.dictValue) row.dictLabel = row.dictValue
},
async submitAddDict() {
const valid = await this.$refs.dictFormRef.validate().catch(() => false)
if (!valid) return
this.loading = true
this.btnLoading = true
try {
await addData(this.form)
this.$message.success('字典项新增成功!')
await this.getDictOptions(this.dictId)
this.resetDictForm()
} catch (err) {
this.$message.error('新增失败,请稍后重试')
console.error(err)
} finally {
this.loading = false
this.btnLoading = false
}
},
// ✅ 新增:双击单元格 激活编辑状态 核心方法
handleDbEdit(row) {
// 同一时间只允许编辑一行,双击其他行关闭当前行编辑
this.editRowId = row.dictCode
},
// ✅ 核心完善:失去焦点/回车 保存数据 + 强制关闭编辑态 → 还原成普通单元格
async handleSaveRow(row) {
if (!row.dictLabel || !row.dictValue) {
this.$message.warning('字典标签和字典值不能为空!')
await this.getDictOptions(this.dictId)
this.editRowId = '' // 校验失败,也必须还原单元格
return
}
row.sort = 0
this.loading = true
try {
await updateData(row)
this.$message.success('字典项修改成功!')
} catch (err) {
this.$message.error('修改失败,请稍后重试')
await this.getDictOptions(this.dictId)
console.error(err)
} finally {
this.loading = false
this.editRowId = '' // ✅必加:保存完成,立刻关闭编辑态,变回文本
}
},
async handleDelete(row) {
const confirm = await this.$confirm('确定要删除该字典项吗?删除后不可恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(() => false)
if (!confirm) return
this.loading = true
this.delBtnLoading = row.dictCode
try {
await delData(row.dictCode)
this.$message.success('删除成功!')
await this.getDictOptions(this.dictId)
} catch (err) {
this.$message.error('删除失败,请稍后重试')
console.error(err)
} finally {
this.loading = false
this.delBtnLoading = ''
}
}
}
}
</script>

View File

@@ -1,7 +1,9 @@
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
const ZincTokenKey = 'Zinc-Token'
// L3 Token管理
export function getToken() {
return Cookies.get(TokenKey)
}
@@ -13,3 +15,16 @@ export function setToken(token) {
export function removeToken() {
return Cookies.remove(TokenKey)
}
// Zinc Token管理
export function getZincToken() {
return Cookies.get(ZincTokenKey)
}
export function setZincToken(token) {
return Cookies.set(ZincTokenKey, token)
}
export function removeZincToken() {
return Cookies.remove(ZincTokenKey)
}

View File

@@ -0,0 +1,142 @@
import axios from 'axios'
import { Notification, MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getZincToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams } from "@/utils/klp";
import cache from '@/plugins/cache'
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 对应国际化资源文件后缀
axios.defaults.headers['Content-Language'] = 'zh_CN'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: '/zinc-api',
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getZincToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getZincToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
// 对于盖章请求,添加特殊处理确保 0 值不被过滤
if (config.url && config.url.includes('/stamp/java')) {
console.log('Stamp request - original data:', JSON.stringify(config.data, null, 2))
if (config.data && typeof config.data === 'object') {
// 确保 yPx 和 xPx 即使是 0 也被正确包含
// 创建一个新对象,确保所有值都被正确设置
const cleanData = {
targetFileUrl: String(config.data.targetFileUrl || ''),
stampImageUrl: String(config.data.stampImageUrl || ''),
pageNo: Number(config.data.pageNo) || 1,
xPx: Number(config.data.xPx) || 0,
yPx: Number(config.data.yPx) || 0
}
if (config.data.widthPx !== undefined && config.data.widthPx !== null) {
cleanData.widthPx = Number(config.data.widthPx)
}
if (config.data.heightPx !== undefined && config.data.heightPx !== null) {
cleanData.heightPx = Number(config.data.heightPx)
}
console.log('Stamp request - cleaned data:', JSON.stringify(cleanData, null, 2))
console.log('yPx in cleaned data:', cleanData.yPx, typeof cleanData.yPx)
config.data = cleanData
}
}
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
export default service

View File

@@ -0,0 +1,640 @@
<template>
<div class="page-container">
<div style="display: flex; align-items: center;">
<!-- 时间维度切换Element-UI 选项卡替代uniapp自定义tab -->
<!-- 时间维度切换 -->
<div class="time-tab-bar">
<div v-for="item in timeTabs" :key="item.value" class="time-tab-item"
:class="{ 'time-tab-active': activeTab === item.value }" @click="handleTabChange(item.value)">
{{ item.label }}
</div>
</div>
<!-- 日期选择区Element-UI 日期选择器替代uniapp picker -->
<div class="date-selector">
<!-- 日模式-单日期选择 -->
<el-date-picker v-if="activeTab === 'day'" v-model="startDate" type="date" value-format="yyyy-MM-dd"
placeholder="选择日期" @change="handleDateChange" class="single-date-picker" />
<!-- 月模式-月份范围选择 -->
<div v-else-if="activeTab === 'month'" class="date-range-group">
<el-date-picker v-model="startDate" type="month" value-format="yyyy-MM" placeholder="选择开始月份"
@change="handleStartMonthChange" class="range-date-picker" />
<span class="date-separator"></span>
<el-date-picker v-model="endDate" type="month" value-format="yyyy-MM" placeholder="选择结束月份"
:picker-options="monthPickerOptions" @change="handleEndMonthChange" class="range-date-picker" />
</div>
<!-- 年模式-年份范围选择 -->
<div v-else class="date-range-group">
<el-date-picker v-model="startDate" type="year" value-format="yyyy" placeholder="选择开始年份"
@change="handleStartYearChange" class="range-date-picker" />
<span class="date-separator"></span>
<el-date-picker v-model="endDate" type="year" value-format="yyyy" placeholder="选择结束年份"
@change="handleEndYearChange" class="range-date-picker" />
</div>
</div>
</div>
<!-- 生产汇总数据 -->
<div class="summary-section">
<div class="section-header">
<span class="section-title">生产汇总</span>
<span class="section-date">{{ displayDateRange }}</span>
</div>
<div class="summary-grid">
<div class="summary-card" v-for="(item, index) in summaryData" :key="index">
<span class="summary-label">{{ item.label }}</span>
<div class="summary-value-box">
<span class="summary-value">{{ item.value }}</span>
<span v-if="item.unit" class="summary-unit">{{ item.unit }}</span>
</div>
</div>
</div>
</div>
<el-row :gutter="20">
<el-col :span="12">
<!-- 产量趋势图Echarts容器 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">产量趋势</span>
</div>
<div class="chart-wrapper" ref="productionChart" id="productionChart"></div>
</div>
</el-col>
<el-col :span="12">
<!-- 班组产量对比Echarts容器 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">班组产量对比</span>
</div>
<div class="chart-wrapper" ref="crewChart" id="crewChart"></div>
</div>
</el-col>
</el-row>
<!-- 规格分布Echarts饼图容器 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">规格分布</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<div class="pie-charts-row">
<div class="pie-chart-item">
<span class="pie-title">厚度分布</span>
<div class="chart-wrapper" ref="thicknessPie" id="thicknessPie"></div>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="pie-charts-row">
<div class="pie-chart-item">
<span class="pie-title">宽度分布</span>
<div class="chart-wrapper" ref="widthPie" id="widthPie"></div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
// ✅ 【完全未改动】原API导入保持不变
import {
getProductionSummary,
getCrewProduction,
getThicknessDistribution,
getWidthDistribution
} from '@/api/pocket/plantState'
// 引入Echarts
import * as echarts from 'echarts'
// ✅ 【完全未改动】原工具函数
function getDefaultDate(type = "day") {
const date = new Date();
return formatDate(date, type);
}
function formatDate(date, type) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
switch (type) {
case "day": return `${year}-${month}-${day}`;
case "month": return `${year}-${month}`;
case "year": return `${year}`;
default: return `${year}-${month}-${day}`;
}
}
export default {
name: 'PlantProduction',
data() {
return {
activeTab: "day",
startDate: getDefaultDate(),
endDate: getDefaultDate(),
// Echarts实例存储
productionChart: null,
crewChart: null,
thicknessPie: null,
widthPie: null,
// 月份选择器限制条件
monthPickerOptions: {},
timeTabs: [
{ label: "日", value: "day" },
{ label: "月", value: "month" },
{ label: "年", value: "year" }
],
// ✅ 【完全未改动】业务数据
summaryData: [
{ label: "生产钢卷数", value: 0, unit: "卷" },
{ label: "平均宽度", value: 0, unit: "mm" },
{ label: "平均厚度", value: 0, unit: "mm" },
{ label: "原料总量", value: 0, unit: "t" },
{ label: "成品总量", value: 0, unit: "t" },
{ label: "成材率", value: 0, unit: "%" }
],
// Echarts配色沿用原配色
mainColor: ["#0066cc", "#00b96b"],
columnColor: ["#0066cc", "#409eff", "#66b1ff"],
pieColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"]
};
},
// ✅ 【完全未改动】计算属性
computed: {
maxMonthEnd() {
if (!this.startDate) return "";
const date = new Date(this.startDate);
date.setFullYear(date.getFullYear() + 1);
return formatDate(date, "month");
},
displayDateRange() {
switch (this.activeTab) {
case "day":
return this.startDate;
case "month":
return `${this.startDate}${this.endDate}`;
case "year":
return `${this.startDate}${this.endDate}`;
default:
return "";
}
}
},
watch: {
// 监听月份开始值,更新结束月份的可选范围
startDate(val) {
if (this.activeTab === 'month' && val) {
this.monthPickerOptions = {
disabledDate: (time) => {
const maxDate = new Date(this.maxMonthEnd + '-01')
return time.getTime() > maxDate.getTime()
}
}
}
}
},
mounted() {
// 初始化Echarts图表
this.initCharts()
// 加载业务数据
this.loadProductionData()
// 窗口自适应
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
// 销毁Echarts实例防止内存泄漏
window.removeEventListener('resize', this.resizeCharts)
this.productionChart && this.productionChart.dispose()
this.crewChart && this.crewChart.dispose()
this.thicknessPie && this.thicknessPie.dispose()
this.widthPie && this.widthPie.dispose()
},
methods: {
// ✅ 【逻辑未改动】仅适配Element的tab参数
handleTabChange(tab) {
this.activeTab = tab;
const defaultDate = getDefaultDate();
this.startDate = defaultDate;
this.endDate = tab === "day" ? defaultDate : getDefaultDate(tab);
this.loadProductionData();
},
// ✅ 【逻辑未改动】日期变更事件
handleDateChange(val) {
this.startDate = val;
this.endDate = val;
this.loadProductionData();
},
handleStartMonthChange(val) {
this.startDate = val;
const maxEndDate = new Date(this.startDate);
maxEndDate.setFullYear(maxEndDate.getFullYear() + 1);
const maxEndStr = formatDate(maxEndDate, "month");
if (new Date(this.endDate) > maxEndDate) {
this.endDate = maxEndStr;
}
this.loadProductionData();
},
handleEndMonthChange(val) {
this.endDate = val;
this.loadProductionData();
},
handleStartYearChange(val) {
this.startDate = val;
this.loadProductionData();
},
handleEndYearChange(val) {
this.endDate = val;
this.loadProductionData();
},
// ✅ 【核心逻辑未改动】仅替换uni加载提示为Element
loadProductionData() {
const loading = this.$loading({
lock: true,
text: '加载中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
Promise.all([
this.loadSummaryData(),
this.loadCrewProductionData(),
this.loadSpecDistribution()
]).finally(() => {
loading.close()
})
},
// ✅ ✅ ✅ 【完全未改动】汇总数据加载
loadSummaryData() {
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return getProductionSummary(start, end).then(response => {
if (response.code === 200 && response.data) {
const data = response.data
this.summaryData = [
{ label: "生产钢卷数", value: data.coilCount || 0, unit: "卷" },
{ label: "平均宽度", value: data.avgWidth || 0, unit: "mm" },
{ label: "平均厚度", value: data.avgThick || 0, unit: "mm" },
{ label: "原料总量", value: data.totalEntryWeight || 0, unit: "t" },
{ label: "成品总量", value: data.totalExitWeight || 0, unit: "t" },
{ label: "成材率", value: data.yieldRate || 0, unit: "%" }
]
}
}).catch(error => {
console.error('加载汇总数据失败:', error)
})
},
// ✅ ✅ ✅ 【完全未改动】班组产量加载+图表数据组装
loadCrewProductionData() {
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return getCrewProduction(start, end).then(response => {
if (response.code === 200 && response.data && response.data.length > 0) {
const crewData = response.data
const categories = []
const crewMap = {}
crewData.forEach(item => {
const label = `${item.crew}-${item.shift}`
categories.push(label)
if (!crewMap[item.shift]) {
crewMap[item.shift] = []
}
crewMap[item.shift].push(Number(item.totalWeight) || 0)
})
const series = Object.keys(crewMap).map(shift => ({
name: shift,
data: crewMap[shift],
type: 'bar'
}))
// 渲染图表
this.setCrewChart(series.map(item => item.name), series)
this.setProductionChart(
categories,
crewData.map(item => Number(item.totalWeight) || 0),
crewData.map(item => Number(item.avgThick) || 0)
)
}
}).catch(error => {
console.error('加载班组产量失败:', error)
})
},
// ✅ ✅ ✅ 【完全未改动】规格分布加载
loadSpecDistribution() {
const start = this.formatFullDate(this.startDate, true)
const end = this.formatFullDate(this.endDate, false)
return Promise.all([
getThicknessDistribution(start, end),
getWidthDistribution(start, end)
]).then(([thicknessRes, widthRes]) => {
if (thicknessRes.code === 200 && thicknessRes.data) {
const pieData = thicknessRes.data.map(item => ({
name: item.category,
value: item.count
}))
this.setThicknessPie(pieData)
}
if (widthRes.code === 200 && widthRes.data) {
const pieData = widthRes.data.map(item => ({
name: item.category,
value: item.count
}))
this.setWidthPie(pieData)
}
}).catch(error => {
console.error('加载规格分布失败:', error)
})
},
// ✅ 【完全未改动】日期格式化工具
formatFullDate(dateStr, isStart) {
if (!dateStr) return ''
if (dateStr.length === 10) {
return dateStr
}
if (dateStr.length === 7) {
if (isStart) {
return `${dateStr}-01`
} else {
const [year, month] = dateStr.split('-')
const lastDay = new Date(year, month, 0).getDate()
return `${dateStr}-${String(lastDay).padStart(2, '0')}`
}
}
if (dateStr.length === 4) {
if (isStart) {
return `${dateStr}-01-01`
} else {
return `${dateStr}-12-31`
}
}
return dateStr
},
// ===== Echarts 图表初始化/渲染/自适应 新增方法 =====
initCharts() {
// 产量趋势图-混合图(柱形+折线)
this.productionChart = echarts.init(document.getElementById('productionChart'))
// 班组产量-柱状图
this.crewChart = echarts.init(document.getElementById('crewChart'))
// 厚度分布-饼图
this.thicknessPie = echarts.init(document.getElementById('thicknessPie'))
// 宽度分布-饼图
this.widthPie = echarts.init(document.getElementById('widthPie'))
},
// 产量趋势图渲染
setProductionChart(xData, prodData, thickData) {
const option = {
color: this.mainColor,
grid: { left: 30, right: 30, top: 40, bottom: 30 },
legend: { top: 0, left: 'center' },
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
yAxis: [
{ type: 'value', name: '产量(t)', position: 'left', splitLine: { type: 'dashed' } },
{ type: 'value', name: '平均厚度(mm)', position: 'right', splitLine: { show: false } }
],
series: [
{ name: '产量', type: 'bar', data: prodData, barWidth: 20 },
{ name: '平均厚度', type: 'line', yAxisIndex: 1, data: thickData, smooth: true }
]
}
this.productionChart.setOption(option)
},
// 班组产量柱状图渲染
setCrewChart(xData, seriesData) {
console.log(seriesData, xData)
const option = {
color: this.columnColor,
grid: { left: 30, right: 30, top: 40, bottom: 30 },
legend: { top: 0, left: 'center' },
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
yAxis: { type: 'value', name: '产量(t)', splitLine: { type: 'dashed' } },
series: seriesData
}
this.crewChart.setOption(option)
},
// 厚度饼图渲染
setThicknessPie(pieData) {
this.setPieOption(this.thicknessPie, pieData)
},
// 宽度饼图渲染
setWidthPie(pieData) {
this.setPieOption(this.widthPie, pieData)
},
// 饼图公共配置
setPieOption(chart, data) {
const option = {
color: this.pieColor,
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12 } },
series: [{ name: '分布占比', type: 'pie', radius: ['40%', '70%'], data }]
}
chart.setOption(option)
},
// 图表自适应
resizeCharts() {
this.productionChart && this.productionChart.resize()
this.crewChart && this.crewChart.resize()
this.thicknessPie && this.thicknessPie.resize()
this.widthPie && this.widthPie.resize()
}
}
};
</script>
<style scoped lang="scss">
// PC端样式适配rpx转px(1rpx=0.5px),保留原有布局逻辑
.page-container {
background: #f5f7fa;
padding: 12px;
min-height: calc(100vh - 20px);
}
.time-tab-bar {
display: flex;
width: 240px;
background: #fff;
border-radius: 4px;
padding: 4px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
}
.time-tab-item {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 13px;
color: #606266;
border-radius: 3px;
transition: all 0.2s;
cursor: pointer;
}
.time-tab-active {
background: #0066cc;
color: #fff;
font-weight: 500;
}
// 日期选择区
.date-selector {
background: #fff;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
margin-left: 20px;
border: 1px solid #e4e7ed;
}
.single-date-picker {
width: 100%;
}
.date-range-group {
display: flex;
align-items: center;
gap: 0;
width: 100%;
}
.range-date-picker {
flex: 1;
border-radius: 3px 0 0 3px;
&:last-child {
border-radius: 0 3px 3px 0;
}
}
.date-separator {
font-size: 14px;
color: #000;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
}
// 区块通用样式
.summary-section,
.chart-section {
margin-bottom: 12px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-left: 8px;
border-left: 2px solid #0066cc;
}
.section-title {
font-size: 15px;
font-weight: 500;
color: #303133;
}
.section-date {
font-size: 12px;
color: #909399;
}
// 汇总卡片网格
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.summary-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 14px 10px;
text-align: center;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
}
.summary-label {
display: block;
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.summary-value-box {
display: flex;
align-items: baseline;
justify-content: center;
}
.summary-value {
font-size: 20px;
font-weight: 600;
color: #0066cc;
line-height: 1;
}
.summary-unit {
font-size: 11px;
color: #909399;
margin-left: 3px;
}
// 图表容器
.chart-wrapper {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px 8px;
min-height: 225px;
width: 100%;
}
// 饼图布局
.pie-charts-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.pie-chart-item {
flex: 1;
display: flex;
flex-direction: column;
}
.pie-title {
display: block;
text-align: center;
font-size: 13px;
color: #606266;
font-weight: 500;
margin-bottom: 8px;
padding: 0 8px;
}
</style>

View File

@@ -0,0 +1,837 @@
<template>
<div class="page-container">
<!-- 刷新按钮固定在右下角保留 -->
<div class="refresh-btn-fixed" @click="refreshData">
<span class="refresh-icon" :class="{ 'rotating': isRefreshing }"></span>
</div>
<!-- 快速导航菜单固定在左下角保留全部导航项 -->
<div class="nav-menu-fixed" :class="{ 'nav-expanded': navMenuExpanded }">
<div class="nav-toggle" @click="navMenuExpanded = !navMenuExpanded">
<span class="nav-toggle-icon">{{ navMenuExpanded ? '✕' : '☰' }}</span>
</div>
<div class="nav-items" v-if="navMenuExpanded">
<div class="nav-item" @click="scrollToSection('speed-monitor')">
<span class="nav-label">速度监控</span>
</div>
<div class="nav-item" @click="scrollToSection('exit-speed-chart')">
<span class="nav-label">出口速度趋势</span>
</div>
<div class="nav-item" @click="scrollToSection('temp-chart')">
<span class="nav-label">酸槽温度趋势</span>
</div>
<div class="nav-item" @click="scrollToSection('looper-status')">
<span class="nav-label">活套运行状态</span>
</div>
<div class="nav-item" @click="scrollToSection('tank-concentration')">
<span class="nav-label">酸槽浓度监控</span>
</div>
<div class="nav-item" @click="scrollToSection('force-chart')">
<span class="nav-label">轧制力趋势</span>
</div>
<div class="nav-item" @click="scrollToSection('process-params')">
<span class="nav-label">工艺参数</span>
</div>
<div class="nav-item" @click="scrollToSection('roll-speed')">
<span class="nav-label">轧辊速度监控</span>
</div>
<div class="nav-item" @click="scrollToSection('reduc-rate')">
<span class="nav-label">机架压下率</span>
</div>
<div class="nav-item" @click="scrollToSection('tension-monitor')">
<span class="nav-label">带钢张力监控</span>
</div>
<div class="nav-item" @click="scrollToSection('power-ratio')">
<span class="nav-label">机架功率百分比</span>
</div>
<div class="nav-item" @click="scrollToSection('paint-speed')">
<span class="nav-label">涂装速度监控</span>
</div>
<div class="nav-item" @click="scrollToSection('tlv-params')">
<span class="nav-label">拉矫参数</span>
</div>
<div class="nav-item" @click="scrollToSection('paint-temp-chart')">
<span class="nav-label">烘干温度趋势</span>
</div>
</div>
</div>
<!-- 核心纯实时监控滚动容器 移除tab判断直接展示 -->
<div class="scroll-container">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="status-item">
<span class="status-label">网络状态</span>
<span class="status-value" :class="'status-' + webStatus[0].value">{{ webStatus[0].value }}</span>
</div>
<div class="status-divider"></div>
<div class="status-item">
<span class="status-label">当前班组</span>
<span class="status-value">{{ webStatus[1].value }}</span>
</div>
<div class="status-divider"></div>
<div class="status-item">
<span class="status-label">更新时间</span>
<span class="status-value status-time">{{ lastUpdateTime }}</span>
</div>
</div>
<!-- 速度监控 -->
<div class="section" id="speed-monitor">
<div class="section-title">速度监控</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in speedMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 出口速度趋势 -->
<div class="section" id="exit-speed-chart">
<div class="section-title">出口速度趋势</div>
<div class="chart-box" ref="exitSpeedChart" id="exitSpeedChart"></div>
</div>
<!-- 酸槽温度趋势 -->
<div class="section" id="temp-chart">
<div class="section-title">酸槽温度趋势</div>
<div class="chart-box" ref="tempChart" id="tempChart"></div>
</div>
<!-- 活套运行状态 -->
<div class="section" id="looper-status">
<div class="section-title">活套运行状态</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in looperMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 酸槽浓度监控 -->
<div class="section" id="tank-concentration">
<div class="section-title">酸槽浓度监控</div>
<div class="tank-grid">
<div class="tank-card" v-for="(tank, index) in tankConcentration" :key="index">
<div class="tank-header">{{ tank.name }}</div>
<div class="tank-data">
<div class="tank-row">
<span class="data-label">酸浓度</span>
<span class="data-value">{{ tank.hclCont }} <span class="data-unit">g/L</span></span>
</div>
<div class="tank-divider"></div>
<div class="tank-row">
<span class="data-label">铁盐浓度</span>
<span class="data-value">{{ tank.feCont }} <span class="data-unit">g/L</span></span>
</div>
</div>
</div>
</div>
</div>
<!-- 轧制力趋势 -->
<div class="section" id="force-chart">
<div class="section-title">轧制力趋势</div>
<div class="chart-box" ref="forceChart" id="forceChart"></div>
</div>
<!-- 工艺参数 -->
<div class="section" id="process-params">
<div class="section-title">工艺参数</div>
<div class="metrics-grid-2">
<div class="metric-box" v-for="(item, index) in processMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- ============ 镀锌线数据 ============ -->
<div class="section-divider">
<span class="divider-text">镀锌线监控数据</span>
</div>
<!-- 轧辊速度监控 -->
<div class="section" id="roll-speed">
<div class="section-title">轧辊速度监控</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in rollSpeedMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 机架压下率 -->
<div class="section" id="reduc-rate">
<div class="section-title">机架压下率</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in reducMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 带钢张力监控 -->
<div class="section" id="tension-monitor">
<div class="section-title">带钢张力监控</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in tensionMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 机架功率百分比 -->
<div class="section" id="power-ratio">
<div class="section-title">机架功率百分比</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in powerMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- ============ 涂装线数据 ============ -->
<div class="section-divider">
<span class="divider-text">涂装线监控数据</span>
</div>
<!-- 涂装速度监控 -->
<div class="section" id="paint-speed">
<div class="section-title">涂装速度监控</div>
<div class="metrics-grid-3">
<div class="metric-box" v-for="(item, index) in paintSpeedMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 拉矫参数 -->
<div class="section" id="tlv-params">
<div class="section-title">拉矫参数</div>
<div class="metrics-grid-2">
<div class="metric-box" v-for="(item, index) in tlvMetrics" :key="index">
<span class="metric-name">{{ item.label }}</span>
<span class="metric-value">{{ item.value }}</span>
<span class="metric-unit">{{ item.unit }}</span>
</div>
</div>
</div>
<!-- 烘干温度趋势 -->
<div class="section" id="paint-temp-chart">
<div class="section-title">烘干温度趋势</div>
<div class="chart-box" ref="paintTempChart" id="paintTempChart"></div>
</div>
</div>
</div>
</template>
<script>
// ✅ 原接口导入 完全未改动
import { getAllPlantStateDefines, listPlantStateHistory, getCurrentShift } from '@/api/pocket/plantState'
// ✅ 引入Echarts5.x 兼容Vue2
import * as echarts from 'echarts'
// ✅ 引入axios替换uni.request (PC端网络请求)
import axios from 'axios'
export default {
name: 'RealTimeMonitor',
data() {
return {
webStatus: [
{ label: '网络状态', value: '检测中...' },
{ label: '当前班组', value: '—' }
],
lastUpdateTime: '—', // 最后更新时间
isRefreshing: false, // 是否正在刷新
refreshTimer: null, // 定时器
navMenuExpanded: false, // 导航菜单是否展开
// ✅ 移除了 tabData/currentTab 无用属性
// 速度监控指标ID=1,2,3
speedMetrics: [
{ label: '出口带钢速度', value: '—', unit: 'm/min' },
{ label: '酸洗带钢速度', value: '—', unit: 'm/min' },
{ label: '圆盘剪速度', value: '—', unit: 'm/min' }
],
// 活套状态ID=8,9,10
looperMetrics: [
{ label: '入口活套', value: '—', unit: '%' },
{ label: '出口活套', value: '—', unit: '%' },
{ label: '联机活套', value: '—', unit: '%' }
],
// 酸槽浓度ID=11-16
tankConcentration: [
{ name: '1#酸槽', hclCont: '—', feCont: '—' },
{ name: '2#酸槽', hclCont: '—', feCont: '—' },
{ name: '3#酸槽', hclCont: '—', feCont: '—' }
],
// 其他工艺参数ID=7,17,18,19,20
processMetrics: [
{ label: '漂洗温度', value: '—', unit: '°C' },
{ label: '烘干温度', value: '—', unit: '°C' },
{ label: '漂洗电导率', value: '—', unit: 'g/L' },
{ label: '联机活套张力', value: '—', unit: 'kN' },
{ label: '拉矫机延伸率', value: '—', unit: '%' }
],
// 镀锌线数据
rollSpeedMetrics: [
{ label: '1#机架', value: '—', unit: 'm/min' },
{ label: '2#机架', value: '—', unit: 'm/min' },
{ label: '3#机架', value: '—', unit: 'm/min' },
{ label: '4#机架', value: '—', unit: 'm/min' },
{ label: '5#机架', value: '—', unit: 'm/min' },
{ label: '6#机架', value: '—', unit: 'm/min' }
],
reducMetrics: [
{ label: '1#机架', value: '—', unit: '%' },
{ label: '2#机架', value: '—', unit: '%' },
{ label: '3#机架', value: '—', unit: '%' },
{ label: '4#机架', value: '—', unit: '%' },
{ label: '5#机架', value: '—', unit: '%' },
{ label: '6#机架', value: '—', unit: '%' }
],
tensionMetrics: [
{ label: '0#张力', value: '—', unit: 'kN' },
{ label: '1#张力', value: '—', unit: 'kN' },
{ label: '2#张力', value: '—', unit: 'kN' },
{ label: '3#张力', value: '—', unit: 'kN' },
{ label: '4#张力', value: '—', unit: 'kN' },
{ label: '5#张力', value: '—', unit: 'kN' },
{ label: '6#张力', value: '—', unit: 'kN' }
],
powerMetrics: [
{ label: '1#机架', value: '—', unit: '%' },
{ label: '2#机架', value: '—', unit: '%' },
{ label: '3#机架', value: '—', unit: '%' },
{ label: '4#机架', value: '—', unit: '%' },
{ label: '5#机架', value: '—', unit: '%' },
{ label: '6#机架', value: '—', unit: '%' }
],
// 涂装线数据
paintSpeedMetrics: [
{ label: '出口带钢速度', value: '—', unit: 'm/min' },
{ label: '涂装带钢速度', value: '—', unit: 'm/min' },
{ label: '圆盘剪速度', value: '—', unit: 'm/min' }
],
tlvMetrics: [
{ label: '拉矫延伸率', value: '—', unit: '%' },
{ label: '破磷机插入量1', value: '—', unit: 'mm' },
{ label: '破磷机插入量2', value: '—', unit: 'mm' },
{ label: '破磷机插入量3', value: '—', unit: 'mm' }
],
// Echarts图表实例
exitSpeedChart: null,
tempChart: null,
forceChart: null,
paintTempChart: null,
// 图表配色与原qiun一致
lineColor: ["#0066cc", "#409eff", "#66b1ff"],
forceColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff", "#ecf5ff"],
paintColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
plantStateDefines: [] // 缓存所有的状态定义
};
},
mounted() {
this.loadAllData() // 加载所有数据
this.startAutoRefresh() // 启动自动刷新
this.initEcharts() // 初始化图表
window.addEventListener('resize', this.resizeEcharts) // 窗口自适应
},
beforeDestroy() {
this.stopAutoRefresh() // 页面销毁时清除定时器
window.removeEventListener('resize', this.resizeEcharts)
this.disposeEcharts() // 销毁图表实例 防内存泄漏
},
methods: {
// ✅ 初始化Echarts图表
initEcharts() {
this.exitSpeedChart = echarts.init(document.getElementById('exitSpeedChart'))
this.tempChart = echarts.init(document.getElementById('tempChart'))
this.forceChart = echarts.init(document.getElementById('forceChart'))
this.paintTempChart = echarts.init(document.getElementById('paintTempChart'))
},
// ✅ 图表自适应
resizeEcharts() {
this.exitSpeedChart && this.exitSpeedChart.resize()
this.tempChart && this.tempChart.resize()
this.forceChart && this.forceChart.resize()
this.paintTempChart && this.paintTempChart.resize()
},
// ✅ 销毁图表实例
disposeEcharts() {
this.exitSpeedChart && this.exitSpeedChart.dispose()
this.tempChart && this.tempChart.dispose()
this.forceChart && this.forceChart.dispose()
this.paintTempChart && this.paintTempChart.dispose()
},
// ✅ 渲染折线图通用方法
renderLineChart(chart, xData, seriesData, color, yTitle) {
if(!chart) return
const option = {
color: color,
grid: { left: 30, right: 30, top: 40, bottom: 30 },
tooltip: { trigger: 'axis' },
legend: { top: 0, left: 'center', textStyle: { fontSize: 12 } },
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
yAxis: {
type: 'value',
name: yTitle,
splitLine: { type: 'dashed', color: '#e4e7ed' }
},
series: seriesData.map(item => ({
name: item.name,
type: 'line',
data: item.data,
smooth: true,
lineStyle: { width: 2 },
symbol: 'none'
}))
}
chart.setOption(option, true)
},
// 启动自动刷新每30秒 逻辑完全未改
startAutoRefresh() {
this.refreshTimer = setInterval(() => {
console.log('自动刷新数据...')
this.refreshData(true) // 静默刷新
}, 30000) // 30秒刷新一次
},
// 停止自动刷新 逻辑完全未改
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
}
},
// 加载所有数据(初始化) 逻辑完全未改
loadAllData() {
this.checkNetworkStatus() // 检测网络状态
this.loadCurrentShift() // 加载当前班组
this.initPlantStateDefines() // 加载所有定义
this.updateLastTime() // 更新时间
},
// 刷新数据(手动或自动) ✅ 替换uni弹窗为Element弹窗
refreshData(isSilent = false) {
if (this.isRefreshing) return // 防止重复刷新
this.isRefreshing = true
let loading = null
if (!isSilent) {
loading = this.$loading({ lock: true, text: '刷新中', spinner: 'el-icon-loading' })
}
Promise.all([
this.checkNetworkStatus(),
this.loadCurrentShift(),
this.initPlantStateDefines(isSilent)
]).finally(() => {
this.isRefreshing = false
loading && loading.close()
if (!isSilent) {
this.$message.success('刷新成功')
}
this.updateLastTime()
})
},
// 更新最后刷新时间 逻辑完全未改
updateLastTime() {
const now = new Date()
const hour = String(now.getHours()).padStart(2, '0')
const minute = String(now.getMinutes()).padStart(2, '0')
const second = String(now.getSeconds()).padStart(2, '0')
this.lastUpdateTime = `${hour}:${minute}:${second}`
},
// 检测网络状态 ✅ 替换uni.request为axios
checkNetworkStatus() {
return new Promise((resolve) => {
const startTime = Date.now()
axios.get(`http://140.143.206.120:8080/pocket/proPlantStateDefine/allWithValues`, { timeout: 5000 })
.then(() => {
const responseTime = Date.now() - startTime
if (responseTime < 500) this.webStatus[0].value = '通畅'
else if (responseTime < 2000) this.webStatus[0].value = '卡顿'
else this.webStatus[0].value = '异常'
resolve()
}).catch(() => {
this.webStatus[0].value = '异常'
resolve()
})
})
},
// 加载当前班组信息 逻辑完全未改
loadCurrentShift() {
return getCurrentShift().then(response => {
if (response.code === 200 && response.data) {
const shiftData = response.data
const shiftName = this.getShiftName(shiftData.shift)
const crewName = this.getCrewName(shiftData.crew)
this.webStatus[1].value = `${crewName} / ${shiftName}`
}
}).catch(error => {
console.error('加载班组信息失败:', error)
})
},
// 获取班次名称 逻辑完全未改
getShiftName(shift) {
const shiftMap = { 'A': '早班', 'B': '中班', 'C': '晚班' }
return shiftMap[shift] || shift || '—'
},
// 获取班组名称 逻辑完全未改
getCrewName(crew) {
const crewMap = { 1: '甲', 2: '乙', 3: '丙', 4: '丁' }
return crewMap[crew] || crew || '—'
},
// 初始化:加载所有状态定义及其当前值 ✅ 替换uni弹窗为Element弹窗
initPlantStateDefines(isSilent = false) {
let loading = null
if (!isSilent) {
loading = this.$loading({ lock: true, text: '加载中', spinner: 'el-icon-loading' })
}
return getAllPlantStateDefines().then(response => {
if (response.code === 200 && response.data) {
this.plantStateDefines = response.data
this.updateCurrentMetrics()
return this.loadTempTrend(isSilent)
}
}).finally(() => {
loading && loading.close()
}).catch(error => {
loading && loading.close()
console.error('加载状态定义失败:', error)
})
},
// 更新所有实时指标 逻辑完全未改
updateCurrentMetrics() {
// 速度监控
const exitSpeed = this.getDefineById(1)
const plSpeed = this.getDefineById(2)
const trimSpeed = this.getDefineById(3)
this.speedMetrics = [
{ label: exitSpeed?.comments || '出口带钢速度', value: this.formatValue(exitSpeed?.currentValue), unit: exitSpeed?.units || 'm/min' },
{ label: plSpeed?.comments || '酸洗带钢速度', value: this.formatValue(plSpeed?.currentValue), unit: plSpeed?.units || 'm/min' },
{ label: trimSpeed?.comments || '圆盘剪速度', value: this.formatValue(trimSpeed?.currentValue), unit: trimSpeed?.units || 'm/min' }
]
// 活套状态
const celLooper = this.getDefineById(8)
const cxlLooper = this.getDefineById(9)
const telLooper = this.getDefineById(10)
this.looperMetrics = [
{ label: celLooper?.comments || '入口重套', value: this.formatValue(celLooper?.currentValue), unit: celLooper?.units || '%' },
{ label: cxlLooper?.comments || '出口活套', value: this.formatValue(cxlLooper?.currentValue), unit: cxlLooper?.units || '%' },
{ label: telLooper?.comments || '联机活套', value: this.formatValue(telLooper?.currentValue), unit: telLooper?.units || '%' }
]
// 酸槽浓度
this.tankConcentration = [
{ name: '1#酸槽', hclCont: this.formatValue(this.getDefineById(11)?.currentValue), feCont: this.formatValue(this.getDefineById(12)?.currentValue) },
{ name: '2#酸槽', hclCont: this.formatValue(this.getDefineById(13)?.currentValue), feCont: this.formatValue(this.getDefineById(14)?.currentValue) },
{ name: '3#酸槽', hclCont: this.formatValue(this.getDefineById(15)?.currentValue), feCont: this.formatValue(this.getDefineById(16)?.currentValue) }
]
// 工艺参数
const rinseTemp = this.getDefineById(7)
const windTemp = this.getDefineById(17)
const rinseFlow = this.getDefineById(18)
const telTension = this.getDefineById(19)
const tlvElong = this.getDefineById(20)
this.processMetrics = [
{ label: rinseTemp?.comments || '漂洗温度', value: this.formatValue(rinseTemp?.currentValue), unit: rinseTemp?.units || '°C' },
{ label: windTemp?.comments || '烘干温度', value: this.formatValue(windTemp?.currentValue), unit: windTemp?.units || '°C' },
{ label: rinseFlow?.comments || '漂洗电导率', value: this.formatValue(rinseFlow?.currentValue), unit: rinseFlow?.units || 'g/L' },
{ label: telTension?.comments || '联机活套张力', value: this.formatValue(telTension?.currentValue), unit: telTension?.units || 'kN' },
{ label: tlvElong?.comments || '拉矫机延伸率', value: this.formatValue(tlvElong?.currentValue), unit: tlvElong?.units || '%' }
]
// 镀锌线-轧辊速度
this.rollSpeedMetrics = [
{ label: '1#机架', value: this.formatValue(this.getDefineById(36)?.currentValue), unit: 'm/min' },
{ label: '2#机架', value: this.formatValue(this.getDefineById(37)?.currentValue), unit: 'm/min' },
{ label: '3#机架', value: this.formatValue(this.getDefineById(38)?.currentValue), unit: 'm/min' },
{ label: '4#机架', value: this.formatValue(this.getDefineById(39)?.currentValue), unit: 'm/min' },
{ label: '5#机架', value: this.formatValue(this.getDefineById(40)?.currentValue), unit: 'm/min' },
{ label: '6#机架', value: this.formatValue(this.getDefineById(41)?.currentValue), unit: 'm/min' }
]
// 镀锌线-压下率
this.reducMetrics = [
{ label: '1#机架', value: this.formatValue(this.getDefineById(24)?.currentValue), unit: '%' },
{ label: '2#机架', value: this.formatValue(this.getDefineById(25)?.currentValue), unit: '%' },
{ label: '3#机架', value: this.formatValue(this.getDefineById(26)?.currentValue), unit: '%' },
{ label: '4#机架', value: this.formatValue(this.getDefineById(27)?.currentValue), unit: '%' },
{ label: '5#机架', value: this.formatValue(this.getDefineById(28)?.currentValue), unit: '%' },
{ label: '6#机架', value: this.formatValue(this.getDefineById(29)?.currentValue), unit: '%' }
]
// 镀锌线-张力
this.tensionMetrics = [
{ label: '0#张力', value: this.formatValue(this.getDefineById(42)?.currentValue), unit: 'kN' },
{ label: '1#张力', value: this.formatValue(this.getDefineById(43)?.currentValue), unit: 'kN' },
{ label: '2#张力', value: this.formatValue(this.getDefineById(44)?.currentValue), unit: 'kN' },
{ label: '3#张力', value: this.formatValue(this.getDefineById(45)?.currentValue), unit: 'kN' },
{ label: '4#张力', value: this.formatValue(this.getDefineById(46)?.currentValue), unit: 'kN' },
{ label: '5#张力', value: this.formatValue(this.getDefineById(47)?.currentValue), unit: 'kN' },
{ label: '6#张力', value: this.formatValue(this.getDefineById(48)?.currentValue), unit: 'kN' }
]
// 镀锌线-功率
this.powerMetrics = [
{ label: '1#机架', value: this.formatValue(this.getDefineById(49)?.currentValue), unit: '%' },
{ label: '2#机架', value: this.formatValue(this.getDefineById(50)?.currentValue), unit: '%' },
{ label: '3#机架', value: this.formatValue(this.getDefineById(51)?.currentValue), unit: '%' },
{ label: '4#机架', value: this.formatValue(this.getDefineById(52)?.currentValue), unit: '%' },
{ label: '5#机架', value: this.formatValue(this.getDefineById(53)?.currentValue), unit: '%' },
{ label: '6#机架', value: this.formatValue(this.getDefineById(54)?.currentValue), unit: '%' }
]
// 涂装线-速度
this.paintSpeedMetrics = [
{ label: '出口带钢速度', value: this.formatValue(this.getDefineById(1)?.currentValue), unit: 'm/min' },
{ label: '涂装带钢速度', value: this.formatValue(this.getDefineById(2)?.currentValue), unit: 'm/min' },
{ label: '圆盘剪速度', value: this.formatValue(this.getDefineById(3)?.currentValue), unit: 'm/min' }
]
// 涂装线-拉矫参数
this.tlvMetrics = [
{ label: '拉矫延伸率', value: this.formatValue(this.getDefineById(20)?.currentValue), unit: '%' },
{ label: '破磷机插入量1', value: this.formatValue(this.getDefineById(21)?.currentValue), unit: 'mm' },
{ label: '破磷机插入量2', value: this.formatValue(this.getDefineById(22)?.currentValue), unit: 'mm' },
{ label: '破磷机插入量3', value: this.formatValue(this.getDefineById(23)?.currentValue), unit: 'mm' }
]
},
// 加载历史趋势图数据 ✅ 渲染Echarts图表
loadTempTrend(isSilent = false) {
return listPlantStateHistory({ pageNum: 1, pageSize: 30 }).then(response => {
if (response.code === 200 && response.rows && response.rows.length > 0) {
const categories = []
const tank1Data = [];const tank2Data = [];const tank3Data = [];
const exitSpeedData = [];
const force1Data = [];const force2Data = [];const force3Data = [];const force4Data = [];const force5Data = [];const force6Data = [];
const paintTemp1 = [];const paintTemp2 = [];const paintTemp3 = [];const paintTemp4 = [];const paintTemp5 = [];
response.rows.forEach(item => {
const dateStr = this.formatDate(item.insdate)
categories.push(dateStr)
tank1Data.push(Number(item.value4) || 0)
tank2Data.push(Number(item.value5) || 0)
tank3Data.push(Number(item.value6) || 0)
exitSpeedData.push(Number(item.value1) || 0)
force1Data.push(Number(item.value30) || 0)
force2Data.push(Number(item.value31) || 0)
force3Data.push(Number(item.value32) || 0)
force4Data.push(Number(item.value33) || 0)
force5Data.push(Number(item.value34) || 0)
force6Data.push(Number(item.value35) || 0)
paintTemp1.push(Number(item.value4) || 0)
paintTemp2.push(Number(item.value5) || 0)
paintTemp3.push(Number(item.value6) || 0)
paintTemp4.push(Number(item.value7) || 0)
paintTemp5.push(Number(item.value17) || 0)
})
const reverseCate = categories.reverse()
// 渲染出口速度趋势
this.renderLineChart(this.exitSpeedChart, reverseCate, [{name:'出口带钢速度',data:exitSpeedData.reverse()}], this.lineColor, 'm/min')
// 渲染酸槽温度趋势
this.renderLineChart(this.tempChart, reverseCate, [
{name:'1#酸槽温度',data:tank1Data.reverse()},
{name:'2#酸槽温度',data:tank2Data.reverse()},
{name:'3#酸槽温度',data:tank3Data.reverse()}
], this.lineColor, '°C')
// 渲染轧制力趋势
this.renderLineChart(this.forceChart, reverseCate, [
{name:'1#轧制力',data:force1Data.reverse()},
{name:'2#轧制力',data:force2Data.reverse()},
{name:'3#轧制力',data:force3Data.reverse()},
{name:'4#轧制力',data:force4Data.reverse()},
{name:'5#轧制力',data:force5Data.reverse()},
{name:'6#轧制力',data:force6Data.reverse()}
], this.forceColor, 'kN')
// 渲染烘干温度趋势
this.renderLineChart(this.paintTempChart, reverseCate, [
{name:'1#酸槽温度',data:paintTemp1.reverse()},
{name:'2#酸槽温度',data:paintTemp2.reverse()},
{name:'3#酸槽温度',data:paintTemp3.reverse()},
{name:'漂洗温度',data:paintTemp4.reverse()},
{name:'烘干温度',data:paintTemp5.reverse()}
], this.paintColor, '°C')
}
}).catch(error => {
console.error('加载历史趋势失败:', error)
})
},
// 根据ID获取Define对象 逻辑完全未改
getDefineById(id) {
return this.plantStateDefines.find(item => item.id == id)
},
// 格式化日期 逻辑完全未改
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${hour}:${minute}`
},
// 格式化数值 逻辑完全未改
formatValue(value) {
if (value === null || value === undefined || value === '') return '—'
const num = Number(value)
if (isNaN(num)) return '—'
return num.toFixed(2)
},
// 滚动到指定部分 ✅ PC端原生滚动适配
scrollToSection(sectionId) {
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' })
this.navMenuExpanded = false // 点击后关闭菜单
}
}
};
</script>
<style scoped lang="scss">
// ✅ 所有rpx转px(1rpx=0.5px) PC端完美适配 | 移除tab相关样式
.page-container {
min-height: 100vh;
background: #f5f7fa;
position: relative;
}
/* 刷新按钮(固定在右下角) */
.refresh-btn-fixed {
position: fixed;
right: 16px;
bottom: 60px;
width: 48px;
height: 48px;
background: #0066cc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 102, 204, 0.4);
z-index: 999;
cursor: pointer;
&:active { opacity: 0.8; transform: scale(0.95); }
}
.refresh-icon {
font-size: 24px;
color: #fff;
display: block;
line-height: 1;
&.rotating { animation: rotate 1s linear infinite; }
}
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* 快速导航菜单(固定在左下角) */
.nav-menu-fixed {
position: fixed;
left: 16px;
bottom: 60px;
z-index: 998;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.nav-toggle {
width: 48px;height: 48px;
background: #409eff;
border-radius: 50%;
display: flex;align-items: center;justify-content: center;
box-shadow: 0 4px 10px rgba(64, 158, 255, 0.4);
cursor: pointer;
transition: all 0.3s ease;
&:active { opacity: 0.8; transform: scale(0.95); }
}
.nav-toggle-icon { font-size: 24px; color: #fff; display: block; line-height: 1; }
.nav-items {
display: flex;flex-direction: column;gap:4px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform: translateY(0); } }
.nav-item {
background: #fff;border:1px solid #409eff;border-radius:4px;
padding:8px 12px;cursor: pointer;
box-shadow: 0 2px 6px rgba(64,158,255,0.2);
transition: all 0.2s ease;
&:active { background: #f0f9ff; transform: scale(0.95); }
}
.nav-label { font-size:13px; color:#409eff; font-weight:500; white-space: nowrap; }
/* 滚动容器 - PC端原生滚动 核心修改 */
.scroll-container {
height: calc(100vh - 20px);
padding: 12px;
overflow-y: auto;
scroll-behavior: smooth;
}
/* 顶部状态栏 */
.status-bar {
display: flex;align-items: center;
background: #fff;padding:12px 16px;margin-bottom:12px;
border-radius:4px;border:1px solid #e4e7ed;
}
.status-item { flex:1; display:flex;align-items:center;justify-content:center;gap:8px; }
.status-label { font-size:13px;color:#909399; }
.status-value {
font-size:14px;font-weight:500;color:#303133;
&.status-通畅 { color: #67c23a; }
&.status-卡顿 { color: #e6a23c; }
&.status-异常 { color: #f56c6c; }
&.status-time { color:#909399;font-size:12px; }
}
.status-divider { width:1px;height:20px;background:#e4e7ed; }
/* 区块样式 */
.section { margin-bottom:12px; }
.section-title {
font-size:15px;font-weight:500;color:#303133;
margin-bottom:10px;padding-left:8px;border-left:2px solid #0066cc;
}
/* 分隔符 */
.section-divider {
display:flex;align-items:center;margin:20px 0 12px 0;padding:0 8px;
}
.divider-text {
font-size:14px;font-weight:600;color:#0066cc;
padding:0 6px;background:#f5f7fa;
border-left:2px solid #0066cc;padding-left:8px;
}
/* 指标卡片 - 3列布局 */
.metrics-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap:10px; }
/* 指标卡片 - 2列布局 */
.metrics-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap:10px; }
.metric-box {
background:#fff;border:1px solid #e4e7ed;border-radius:4px;
padding:14px 10px;text-align:center;
}
.metric-name { display:block;font-size:12px;color:#909399;margin-bottom:8px; }
.metric-value { display:block;font-size:24px;font-weight:600;color:#0066cc;margin-bottom:4px;line-height:1; }
.metric-unit { display:block;font-size:11px;color:#909399; }
/* 图表容器 */
.chart-box {
background:#fff;border:1px solid #e4e7ed;border-radius:4px;
padding:12px 8px;height:225px;width:100%;
}
/* 酸槽监控网格 */
.tank-grid { display:grid;grid-template-columns:repeat(3,1fr);gap:10px; }
.tank-card { background:#fff;border:2px solid #0066cc;border-radius:4px;overflow:hidden; }
.tank-header { background:#0066cc;color:#fff;font-size:14px;font-weight:500;padding:10px;text-align:center; }
.tank-data { padding:12px 8px; }
.tank-row { display:flex;flex-direction:column;align-items:center;padding:6px 0; }
.data-label { font-size:11px;color:#909399;margin-bottom:4px; }
.data-value { font-size:18px;font-weight:600;color:#303133; }
.data-unit { font-size:10px;font-weight:400;color:#909399;margin-left:2px; }
.tank-divider { height:1px;background:#e4e7ed;margin:6px 0; }
</style>

View File

@@ -0,0 +1,908 @@
<template>
<div class="page-container">
<div style="display: flex; align-items: center;">
<!-- 时间维度切换 -->
<div class="time-tab-bar">
<div v-for="item in timeTabs" :key="item.value" class="time-tab-item"
:class="{ 'time-tab-active': activeTab === item.value }" @click="handleTabChange(item.value)">
{{ item.label }}
</div>
</div>
<!-- 日期选择区 -->
<div class="date-selector">
<!-- 日模式 -->
<el-date-picker v-if="activeTab === 'day'" v-model="startDate" type="date" value-format="yyyy-MM-dd"
placeholder="选择日期" @change="handleDateChange" class="single-date-picker" />
<!-- 月模式 -->
<div v-else-if="activeTab === 'month'" class="date-range-group">
<el-date-picker v-model="startDate" type="month" value-format="yyyy-MM" placeholder="选择开始月份"
@change="handleStartMonthChange" class="range-date-picker" />
<span class="date-separator"></span>
<el-date-picker v-model="endDate" type="month" value-format="yyyy-MM" placeholder="选择结束月份"
:picker-options="monthPickerOptions" @change="handleEndMonthChange" class="range-date-picker" />
</div>
<!-- 年模式 -->
<div v-else class="date-range-group">
<el-date-picker v-model="startDate" type="year" value-format="yyyy" placeholder="选择开始年份"
@change="handleStartYearChange" class="range-date-picker" />
<span class="date-separator"></span>
<el-date-picker v-model="endDate" type="year" value-format="yyyy" placeholder="选择结束年份"
@change="handleEndYearChange" class="range-date-picker" />
</div>
</div>
</div>
<!-- 停机汇总 -->
<div class="summary-section">
<div class="section-header">
<span class="section-title">停机汇总</span>
<span class="section-date">{{ displayDateRange }}</span>
</div>
<div class="summary-grid">
<div class="summary-card" v-for="(item, index) in summaryData" :key="index">
<span class="summary-label">{{ item.label }}</span>
<div class="summary-value-box">
<span class="summary-value">{{ item.value }}</span>
<span v-if="item.unit" class="summary-unit">{{ item.unit }}</span>
</div>
</div>
</div>
</div>
<el-row :gutter="10">
<el-col :span="12">
<!-- 停机分布 - 班组 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">班组停机分布</span>
</div>
<div v-if="crewPieData.length > 0" class="pie-chart-single" ref="crewPie" id="crewPie"></div>
<div class="empty-chart" v-else>
<span class="empty-icon">📊</span>
<span class="empty-text">此时间段未发生停机</span>
</div>
</div>
</el-col>
<el-col :span="12">
<!-- 停机分布 - 类型 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">停机类型分布</span>
</div>
<div v-if="typePieData.length > 0" class="pie-chart-single" ref="typePie" id="typePie"></div>
<div class="empty-chart" v-else>
<span class="empty-icon">📊</span>
<span class="empty-text">此时间段未发生停机</span>
</div>
</div>
</el-col>
</el-row>
<!-- 停机详细列表日视图 -->
<div class="detail-section" v-if="activeTab === 'day'">
<div class="section-header">
<span class="section-title">停机详情</span>
</div>
<div class="detail-list">
<div v-if="tableData.length === 0" class="empty-state">
<span class="empty-text">暂无停机记录</span>
</div>
<el-table v-else :data="tableData">
<el-table-column label="时间范围" prop="time"></el-table-column>
<el-table-column label="持续时间" prop="duration"></el-table-column>
<el-table-column label="机组" prop="machine"></el-table-column>
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
</el-table>
</div>
</div>
<!-- 停机趋势图/年视图 -->
<div class="chart-section" v-else>
<div class="section-header">
<span class="section-title">停机趋势</span>
</div>
<div class="chart-wrapper trend-chart" ref="trendChart" id="trendChart"></div>
</div>
</div>
</template>
<script>
// ✅【完全未改动】原API导入
import { listStoppage } from '@/api/pocket/plantState'
// 引入Echarts5.x 兼容Vue2
import * as echarts from 'echarts'
// ✅【完全未改动】原独立工具函数
function getDefaultDate(type = "day") {
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
switch (type) {
case "day":
return `${year}-${month}-${day}`;
case "month":
return `${year}-${month}`;
case "year":
return `${year}`;
default:
return `${year}-${month}-${day}`;
}
}
function getLastMonth() {
const date = new Date();
date.setMonth(date.getMonth() - 1);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${year}-${month}`;
}
function formatDate(date, type) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
switch (type) {
case "day":
return `${year}-${month}-${day}`;
case "month":
return `${year}-${month}`;
case "year":
return `${year}`;
default:
return `${year}-${month}-${day}`;
}
}
export default {
name: 'PlantStoppage',
data() {
return {
// ✅【完全未改动】业务数据
activeTab: "day",
startDate: getDefaultDate(),
endDate: getDefaultDate(),
timeTabs: [
{ label: "日", value: "day" },
{ label: "月", value: "month" },
{ label: "年", value: "year" }
],
summaryData: [
{ label: "停机时间", value: 0, unit: "min" },
{ label: "停机次数", value: 0, unit: "次" },
{ label: "作业率", value: 0, unit: "%" }
],
// Echarts图表实例
trendChart: null,
crewPie: null,
typePie: null,
// 月份选择器限制条件
monthPickerOptions: {},
// 趋势图临时存储X轴数据
trendXData: [],
// ✅【完全未改动】原图表配色
mainColor: ["#0066cc", "#f56c6c"],
pieColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
crewPieData: [],
typePieData: [],
tableData: [],
// ✅【新增】resize防抖定时器解决窗口缩放卡顿
resizeTimer: null
};
},
// ✅【完全未改动】计算属性
computed: {
maxMonthEnd() {
if (!this.startDate) return "";
const date = new Date(this.startDate);
date.setFullYear(date.getFullYear() + 1);
return formatDate(date, "month");
},
displayDateRange() {
switch (this.activeTab) {
case "day":
return this.startDate;
case "month":
return `${this.startDate}${this.endDate}`;
case "year":
return `${this.startDate}${this.endDate}`;
default:
return "";
}
}
},
watch: {
// 监听开始月份,更新结束月份可选范围
startDate(val) {
if (this.activeTab === 'month' && val) {
this.monthPickerOptions = {
disabledDate: (time) => {
const maxDate = new Date(this.maxMonthEnd + '-01')
return time.getTime() > maxDate.getTime()
}
}
}
},
// ✅【新增】监听tab切换自动触发图表自适应
activeTab() {
this.$nextTick(() => {
this.resizeEcharts()
})
}
},
mounted() {
// 初始化Echarts图表
this.initEcharts()
// 加载业务数据
this.loadStoppageData()
// 窗口自适应监听
window.addEventListener('resize', this.resizeEcharts)
},
beforeDestroy() {
// ✅【修复】安全销毁图表实例+清除监听+清除定时器,防止内存泄漏
window.removeEventListener('resize', this.resizeEcharts)
clearTimeout(this.resizeTimer)
if (this.trendChart && !this.trendChart.isDisposed()) {
this.trendChart.dispose()
this.trendChart = null
}
if (this.crewPie && !this.crewPie.isDisposed()) {
this.crewPie.dispose()
this.crewPie = null
}
if (this.typePie && !this.typePie.isDisposed()) {
this.typePie.dispose()
this.typePie = null
}
},
methods: {
// ✅【逻辑未改动】仅适配Element传参
handleTabChange(tab) {
this.activeTab = tab;
if (tab === "day") {
const today = getDefaultDate();
this.startDate = today;
this.endDate = today;
} else if (tab === "month") {
this.startDate = getLastMonth();
this.endDate = getDefaultDate("month");
} else {
const currentYear = getDefaultDate("year");
this.startDate = currentYear;
this.endDate = currentYear;
}
this.loadStoppageData();
},
handleDateChange(val) {
this.startDate = val;
this.endDate = val;
this.loadStoppageData();
},
handleStartMonthChange(val) {
this.startDate = val;
const maxEndDate = new Date(this.startDate);
maxEndDate.setFullYear(maxEndDate.getFullYear() + 1);
const maxEndStr = formatDate(maxEndDate, "month");
if (new Date(this.endDate) > maxEndDate) {
this.endDate = maxEndStr;
}
this.loadStoppageData();
},
handleEndMonthChange(val) {
this.endDate = val;
this.loadStoppageData();
},
handleStartYearChange(val) {
this.startDate = val;
this.loadStoppageData();
},
handleEndYearChange(val) {
this.endDate = val;
this.loadStoppageData();
},
// ✅【核心逻辑完全未改动】仅替换uni加载提示为Element + 新增清空图表逻辑
loadStoppageData() {
const loading = this.$loading({
lock: true,
text: '加载中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const start = this.formatFullDate(this.startDate, true)
let end = this.formatFullDate(this.endDate, false)
if (this.activeTab === 'month' && this.endDate && this.endDate.length === 7) {
const today = new Date()
const todayYear = today.getFullYear()
const todayMonth = today.getMonth() + 1
const [endYear, endMonth] = this.endDate.split('-').map(Number)
if (endYear === todayYear && endMonth === todayMonth) {
const todayDay = today.getDate()
end = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
}
}
const queryParams = {
pageNum: 1,
pageSize: 9999,
startDate: start,
endDate: end
}
console.log('停机查询参数:', queryParams)
listStoppage(queryParams).then(response => {
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 {
console.log('暂无停机数据')
this.tableData = []
this.summaryData = [
{ label: '停机时间', value: 0, unit: 'min' },
{ label: '停机次数', value: 0, unit: '次' },
{ label: '作业率', value: 100, unit: '%' }
]
this.crewPieData = []
this.typePieData = []
this.trendXData = []
// ✅【修复】清空所有图表数据
this.clearChart(this.trendChart)
this.renderPieChart('crew', [])
this.renderPieChart('type', [])
}
}).catch(error => {
loading.close()
console.error('加载停机数据失败:', error)
this.$message.error('加载失败,请稍后重试')
})
},
// ✅【完全未改动】原所有工具方法
formatDateTime(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
getTotalAvailableMinutes() {
const start = new Date(this.formatFullDate(this.startDate, true))
const end = new Date(this.formatFullDate(this.endDate, false))
const diffTime = end - start
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
return diffDays * 1440
},
calculateWorkRate(stopDuration, totalAvailableMinutes) {
if (!totalAvailableMinutes || totalAvailableMinutes === 0) {
return 100
}
const workRate = ((totalAvailableMinutes - stopDuration) / totalAvailableMinutes) * 100
return Math.max(0, Math.min(100, workRate)).toFixed(2)
},
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const month = date.getMonth() + 1
const day = date.getDate()
return `${month}/${day}`
},
formatDateByMonth(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const month = date.getMonth() + 1
return `${month}`
},
secondsToMinutes(seconds) {
if (!seconds || seconds === 0) return 0
return Math.round(Number(seconds) / 60)
},
formatFullDate(dateStr, isStart) {
if (!dateStr) return ''
if (dateStr.length === 10) {
return dateStr
}
if (dateStr.length === 7) {
if (isStart) {
return `${dateStr}-01`
} else {
const [year, month] = dateStr.split('-')
const lastDay = new Date(year, month, 0).getDate()
return `${dateStr}-${String(lastDay).padStart(2, '0')}`
}
}
if (dateStr.length === 4) {
if (isStart) {
return `${dateStr}-01-01`
} else {
return `${dateStr}-12-31`
}
}
return dateStr
},
// ✅【逻辑未改动】仅修改图表渲染方式
buildTrendChart(stoppageData) {
if (!stoppageData || stoppageData.length === 0) {
console.log('无法构建趋势图:数据为空')
this.trendXData = []
return
}
const dateMap = {}
const isYearView = this.activeTab === 'year'
stoppageData.forEach(item => {
if (!item.startDate) return
let key
if (isYearView) {
key = this.formatDateByMonth(item.startDate)
} else {
key = this.formatDate(item.startDate)
}
if (!dateMap[key]) {
dateMap[key] = { duration: 0, count: 0 }
}
const durationMinutes = this.secondsToMinutes(item.duration)
dateMap[key].duration += durationMinutes
dateMap[key].count += 1
})
let categories = []
if (isYearView) {
categories = Array.from({ length: 12 }, (_, i) => `${i + 1}`)
} else {
const startStr = this.formatFullDate(this.startDate, true)
let endStr = this.formatFullDate(this.endDate, false)
const today = new Date()
const todayYear = today.getFullYear()
const todayMonth = today.getMonth() + 1
if (this.endDate && this.endDate.length === 7) {
const [endYear, endMonth] = this.endDate.split('-').map(Number)
if (endYear === todayYear && endMonth === todayMonth) {
const todayDay = today.getDate()
endStr = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
}
}
const start = new Date(startStr)
const end = new Date(endStr)
const dateList = []
const currentDate = new Date(start)
while (currentDate <= end) {
const month = currentDate.getMonth() + 1
const day = currentDate.getDate()
const dateKey = `${month}/${day}`
dateList.push(dateKey)
currentDate.setDate(currentDate.getDate() + 1)
}
categories = dateList
}
if (categories.length === 0) {
console.log('无法构建趋势图:无有效日期')
this.trendXData = []
return
}
this.trendXData = categories
const durationData = []
const rateData = []
categories.forEach(key => {
const data = dateMap[key] || { duration: 0, count: 0 }
durationData.push(data.duration)
let totalMinutes
if (isYearView) {
const year = parseInt(this.startDate) || new Date().getFullYear()
const monthIndex = parseInt(key.replace('月', '')) - 1
const daysInMonth = new Date(year, monthIndex + 1, 0).getDate()
totalMinutes = daysInMonth * 1440
} else {
totalMinutes = 1440
}
const rate = this.calculateWorkRate(data.duration, totalMinutes)
rateData.push(Number(rate))
})
// 渲染趋势混合图
this.renderTrendChart(categories, durationData, rateData)
},
// ===== ✅【全部修复】Echarts 图表初始化/渲染/自适应 核心方法 =====
// 安全初始化单个图表实例【新增核心方法】
initSingleChart(chartId) {
const dom = document.getElementById(chartId)
console.log('获取dom', dom)
if (!dom) return null
const chartInstance = echarts.init(dom)
return chartInstance
},
// 安全清空图表内容【新增核心方法】
clearChart(chartInstance) {
if (chartInstance && !chartInstance.isDisposed()) {
chartInstance.clear()
}
},
// 初始化图表(懒初始化占位)
initEcharts() {
this.trendChart = this.initSingleChart('trendChart')
this.crewPie = this.initSingleChart('crewPie')
this.typePie = this.initSingleChart('typePie')
},
// 渲染停机趋势图 (柱状+折线 双Y轴)【修复完整版】
renderTrendChart(xData, durData, rateData) {
this.trendChart = this.initSingleChart('trendChart')
if (!this.trendChart) return
if (xData.length === 0) {
this.clearChart(this.trendChart)
return
}
const option = {
color: this.mainColor,
grid: { left: 30, right: 30, top: 40, bottom: 60 },
legend: { top: 0, left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: xData,
axisLine: { show: false },
axisLabel: { rotate: 60, fontSize: 12 }
},
yAxis: [
{ type: 'value', name: '停机时间(min)', position: 'left', splitLine: { type: 'dashed', color: '#e4e7ed' } },
{ type: 'value', name: '作业率(%)', position: 'right', splitLine: { show: false } }
],
series: [
{ name: '停机时间', type: 'bar', data: durData, barWidth: 40 },
{ name: '作业率', type: 'line', yAxisIndex: 1, data: rateData, smooth: true }
]
}
this.trendChart.setOption(option, true)
},
// 渲染饼图 (班组/类型 通用)【修复完整版】
renderPieChart(type, data) {
const chartId = type === 'crew' ? 'crewPie' : 'typePie'
if (type === 'crew') {
this.crewPie = this.initSingleChart(chartId)
} else {
this.typePie = this.initSingleChart(chartId)
}
console.log('渲染饼图数据:', type, data, chartId)
const chart = type === 'crew' ? this.crewPie : this.typePie
console.log('获取到的图表实例:', chart)
if (!chart) return
if (data.length === 0) {
this.clearChart(chart)
return
}
const option = {
color: this.pieColor,
tooltip: { trigger: 'item', formatter: '{b}: {c}min ({d}%)' },
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12, color: '#666' } },
series: [{
name: type === 'crew' ? '班组停机' : '类型停机',
type: 'pie',
radius: ['40%', '70%'],
data: data,
label: { show: true, fontSize: 12 }
}]
}
chart.setOption(option, true)
},
// 图表自适应 - 防抖+安全校验【修复完整版】
resizeEcharts: function () {
clearTimeout(this.resizeTimer)
this.resizeTimer = setTimeout(() => {
if (this.trendChart && !this.trendChart.isDisposed()) this.trendChart.resize()
if (this.crewPie && !this.crewPie.isDisposed()) this.crewPie.resize()
if (this.typePie && !this.typePie.isDisposed()) this.typePie.resize()
}, 200)
}
}
};
</script>
<style scoped lang="scss">
// ✅ PC端适配rpx转px(1rpx=0.5px) 保留原布局+样式+间距 + 修复图表高度问题
.page-container {
background: #f5f7fa;
padding: 12px;
min-height: calc(100vh - 20px);
}
.time-tab-bar {
display: flex;
width: 240px;
background: #fff;
border-radius: 4px;
padding: 4px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
}
.time-tab-item {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 13px;
color: #606266;
border-radius: 3px;
transition: all 0.2s;
cursor: pointer;
}
.time-tab-active {
background: #0066cc;
color: #fff;
font-weight: 500;
}
.date-selector {
background: #fff;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
margin-left: 20px;
border: 1px solid #e4e7ed;
}
.single-date-picker {
width: 100%;
}
.date-range-group {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 0;
width: 100%;
}
.range-date-picker {
flex: 1;
}
.date-separator {
font-size: 14px;
color: #000;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
}
.summary-section,
.chart-section,
.detail-section {
margin-bottom: 12px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-left: 8px;
border-left: 2px solid #0066cc;
}
.section-title {
font-size: 15px;
font-weight: 500;
color: #303133;
}
.section-date {
font-size: 12px;
color: #909399;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.summary-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 14px 10px;
text-align: center;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
}
.summary-label {
display: block;
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.summary-value-box {
display: flex;
align-items: baseline;
justify-content: center;
}
.summary-value {
font-size: 20px;
font-weight: 600;
color: #0066cc;
line-height: 1;
}
.summary-unit {
font-size: 11px;
color: #909399;
margin-left: 3px;
}
.chart-wrapper {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px 8px;
min-height: 225px;
width: 100%;
}
// ✅【修复】趋势图高度强制生效
.trend-chart {
height: 250px !important;
min-height: 250px;
}
// ✅【修复】饼图容器增加固定高度,解决高度塌陷
.pie-chart-single {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 16px 0;
height: 260px !important;
min-height: 240px;
width: 100%;
}
// ✅【修复】空图表容器高度和饼图一致
.empty-chart {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 50px 0;
height: 260px !important;
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
}
.empty-icon {
font-size: 40px;
opacity: 0.3;
}
.empty-text {
font-size: 14px;
color: #909399;
}
.detail-list {
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.detail-item {
padding: 12px;
border-bottom: 1px solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.detail-time {
font-size: 13px;
color: #606266;
}
.detail-duration {
font-size: 14px;
color: #0066cc;
font-weight: 600;
}
.detail-info {
display: flex;
align-items: baseline;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
}
.detail-label {
font-size: 12px;
color: #909399;
min-width: 60px;
}
.detail-text {
font-size: 13px;
color: #303133;
flex: 1;
}
.empty-state {
padding: 50px 0;
text-align: center;
}
</style>

View File

@@ -0,0 +1,526 @@
<template>
<div class="page-container">
<!-- 月份选择器 -->
<div class="month-selector">
<div class="month-arrow" @click="changeMonth(-1)" :class="{ disabled: isMinMonth }">
<span></span>
</div>
<el-date-picker v-model="selectedMonth" type="month" value-format="yyyy-MM" placeholder="选择月份"
@change="handleMonthChange" class="month-display" />
<div class="month-arrow" @click="changeMonth(1)" :class="{ disabled: isMaxMonth }">
<span></span>
</div>
</div>
<el-row :gutter="20">
<el-col :span="12">
<!-- 班组产量对比 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">产量对比</span>
</div>
<div class="chart-wrapper" ref="outputChart" id="outputChart"></div>
</div>
</el-col>
<el-col :span="12">
<!-- 质量指标对比 -->
<div class="chart-section">
<div class="section-header">
<span class="section-title">质量指标</span>
</div>
<div class="chart-wrapper" ref="radarChart" id="radarChart"></div>
</div>
</el-col>
</el-row>
<!-- 班组详细数据 -->
<el-row :gutter="20">
<el-col :span="12">
<!-- 班组绩效排名 -->
<div class="ranking-section">
<div class="section-header">
<span class="section-title">绩效排名</span>
</div>
<div class="ranking-list">
<div class="ranking-item" v-for="(item, index) in teamRankingData" :key="index"
:class="'rank-' + (index + 1)">
<div class="rank-badge">{{ index + 1 }}</div>
<div class="rank-info">
<span class="team-name">{{ item.team }}</span>
<span class="team-shift">{{ item.shift }}</span>
</div>
<div class="rank-score">
<span class="score-value">{{ item.score }}</span>
<span class="score-label"></span>
</div>
</div>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="detail-section">
<div class="section-header">
<span class="section-title">详细数据</span>
</div>
<div class="detail-table">
<el-table :data="tableData">
<el-table-column prop="team" label="班组" />
<el-table-column prop="output" label="产量(t)" />
<el-table-column prop="yieldRate" label="成材率" />
<el-table-column prop="passRate" label="合格率" />
<el-table-column prop="score" label="综合评分" />
</el-table>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
// ✅【完全未改动】原API导入
import { getTeamPerformance } from '@/api/pocket/plantState'
// 引入Echarts5.x 兼容Vue2
import * as echarts from 'echarts'
// ✅【完全未改动】原工具函数
function getCurrentMonth() {
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${year}-${month}`;
}
export default {
name: 'TeamPerformance',
data() {
return {
selectedMonth: getCurrentMonth(),
startDate: "2020-01",
endDate: getCurrentMonth(),
// ✅【完全未改动】表格列配置
columns: [
{ title: '班组', key: 'team' },
{ title: '产量(t)', key: 'output' },
{ title: '成材率', key: 'yieldRate' },
{ title: '合格率', key: 'passRate' },
{ title: '综合评分', key: 'score' }
],
tableData: [],
teamRankingData: [
{ team: '甲班', shift: '早班', score: 96.8 },
{ team: '乙班', shift: '中班', score: 95.5 },
{ team: '丙班', shift: '晚班', score: 94.2 },
{ team: '丁班', shift: '早班', score: 93.6 }
],
// Echarts图表实例
outputChart: null,
radarChart: null,
// ✅【保留原配色】
columnColor: ["#0066cc"],
radarColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff"]
};
},
// ✅【完全未改动】计算属性
computed: {
isMinMonth() {
return this.selectedMonth === this.startDate;
},
isMaxMonth() {
return this.selectedMonth === this.endDate;
},
formattedMonth() {
const [year, month] = this.selectedMonth.split("-");
return `${year}${month}`;
}
},
mounted() {
// 初始化Echarts图表
this.initCharts()
// 加载业务数据
this.loadTeamPerformance();
// 窗口自适应监听
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
// 销毁图表实例,防止内存泄漏
window.removeEventListener('resize', this.resizeCharts)
this.outputChart && this.outputChart.dispose()
this.radarChart && this.radarChart.dispose()
},
methods: {
// ✅【逻辑未改动】仅适配Element的参数格式
handleMonthChange(val) {
this.selectedMonth = val;
this.loadTeamPerformance();
},
// ✅【完全未改动】月份左右切换逻辑
changeMonth(step) {
const [year, month] = this.selectedMonth.split("-").map(Number);
const targetDate = new Date(year, month - 1 + step);
const targetMonth = `${targetDate.getFullYear()}-${(targetDate.getMonth() + 1).toString().padStart(2, "0")}`;
if (this.compareMonths(targetMonth, this.startDate) < 0) return;
if (this.compareMonths(targetMonth, this.endDate) > 0) return;
this.selectedMonth = targetMonth;
this.loadTeamPerformance();
},
// ✅【完全未改动】月份比较工具
compareMonths(a, b) {
const [aYear, aMonth] = a.split("-").map(Number);
const [bYear, bMonth] = b.split("-").map(Number);
if (aYear !== bYear) return aYear - bYear;
return aMonth - bMonth;
},
// ✅【核心逻辑完全未改动】加载数据 + 修复1个BUG + 替换加载提示
loadTeamPerformance() {
const loading = this.$loading({
lock: true,
text: '加载中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const startDate = `${this.selectedMonth}-01`
const [year, month] = this.selectedMonth.split('-')
const lastDay = new Date(year, month, 0).getDate()
const endDate = `${this.selectedMonth}-${String(lastDay).padStart(2, '0')}`
console.log('查询班组绩效:', startDate, endDate)
getTeamPerformance(startDate, endDate).then(response => {
loading.close()
if (response.code === 200 && response.data && response.data.length > 0) {
const teamData = response.data
console.log('班组绩效数据:', teamData)
// 排名数据取前4名
this.teamRankingData = teamData.slice(0, 4).map(item => ({
team: item.crew,
shift: item.shift,
score: Number(item.score) || 0
}))
// ✅【修复原代码BUG】item.teamName 改为 item.crew (接口实际返回的字段)
this.tableData = teamData.map(item => ({
team: item.crew,
output: Number(item.output) || 0,
yieldRate: `${Number(item.yieldRate) || 0}%`,
passRate: `${Number(item.passRate) || 0}%`,
score: Number(item.score) || 0
}))
// 提取图表所需数据
const categories = teamData.map(item => item.crew)
const outputData = teamData.map(item => Number(item.output) || 0)
const radarSeries = teamData.map(item => ({
name: item.crew,
value: [
Number(this.normalizeValue(item.output, 1500)),
Number(item.yieldRate) || 0,
Number(item.passRate) || 0,
Number(item.avgThickQuality) || 0,
Number(item.avgShapeQuality) || 0
],
areaStyle: {
color: '#409eff70'
}
}))
// 渲染图表
this.renderOutputChart(categories, outputData)
this.renderRadarChart(radarSeries)
} else {
console.log('暂无班组绩效数据')
this.tableData = []
this.teamRankingData = []
// 清空图表
this.renderOutputChart([], [])
this.renderRadarChart([])
}
}).catch(error => {
loading.close()
console.error('加载班组绩效失败:', error)
this.$message.error('加载失败,请稍后重试')
})
},
// ✅【完全未改动】数值归一化工具
normalizeValue(value, max) {
return Math.min(100, (Number(value) / max) * 100).toFixed(2)
},
// ===== Echarts 图表初始化/渲染/自适应 新增方法 =====
initCharts() {
this.outputChart = echarts.init(document.getElementById('outputChart'))
this.radarChart = echarts.init(document.getElementById('radarChart'))
},
// 渲染产量对比柱状图
renderOutputChart(xData, yData) {
if (!this.outputChart) return
const emptyOption = {
grid: { left: 30, right: 30, top: 40, bottom: 30 },
title: { text: '暂无产量数据', left: 'center', textStyle: { color: '#909399' } }
}
if (!xData.length) {
this.outputChart.setOption(emptyOption)
return
}
const option = {
color: this.columnColor,
grid: { left: 30, right: 30, top: 20, bottom: 30 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
yAxis: {
type: 'value',
name: '产量(t)',
splitLine: { type: 'dashed', color: '#e4e7ed' }
},
series: [{ name: '产量', type: 'bar', data: yData, barWidth: 30 }]
}
this.outputChart.setOption(option, true)
},
// 渲染质量指标雷达图
renderRadarChart(seriesData) {
if (!this.radarChart) return
const emptyOption = {
grid: { left: 30, right: 30, top: 40, bottom: 30 },
title: { text: '暂无质量指标数据', left: 'center', textStyle: { color: '#909399' } }
}
if (!seriesData.length) {
this.radarChart.setOption(emptyOption)
return
}
const option = {
color: this.radarColor,
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12, color: '#666' } },
tooltip: { trigger: 'item' },
radar: {
indicator: [
{ name: '产量指标', max: 100 },
{ name: '成材率', max: 100 },
{ name: '合格率', max: 100 },
{ name: '厚度质量', max: 100 },
{ name: '板形质量', max: 100 }
],
splitLine: { lineStyle: { type: 'dashed', color: '#e4e7ed' } },
splitArea: { areaStyle: { opacity: 0.2 } }
},
series: [{ type: 'radar', data: seriesData }]
}
console.log('雷达图数据:', option)
this.radarChart.setOption(option, true)
},
// 图表自适应窗口大小
resizeCharts() {
this.outputChart && this.outputChart.resize()
this.radarChart && this.radarChart.resize()
}
}
};
</script>
<style scoped lang="scss">
// ✅ PC端完美适配rpx转px(1rpx=0.5px) 保留原布局+样式+间距+配色 无改动
.page-container {
background: #f5f7fa;
padding: 12px;
min-height: calc(100vh - 20px);
}
/* 月份选择器 */
.month-selector {
display: flex;
align-items: center;
background: #fff;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 12px;
border: 1px solid #e4e7ed;
}
.month-arrow {
padding: 6px 10px;
font-size: 14px;
color: #0066cc;
font-weight: 600;
cursor: pointer;
&.disabled {
color: #dcdfe6;
cursor: not-allowed;
}
}
.month-display {
flex: 1;
text-align: center;
}
/* 区块样式 */
.ranking-section,
.chart-section,
.detail-section {
margin-bottom: 12px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-left: 8px;
border-left: 2px solid #0066cc;
}
.section-title {
font-size: 15px;
font-weight: 500;
color: #303133;
}
/* 排名列表 */
.ranking-list {
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.ranking-item {
display: flex;
align-items: center;
padding: 4px;
border-bottom: 1px solid #f5f7fa;
&:last-child {
border-bottom: none;
}
&.rank-1 .rank-badge {
background: #0066cc;
}
&.rank-2 .rank-badge {
background: #409eff;
}
&.rank-3 .rank-badge {
background: #66b1ff;
}
}
.rank-badge {
width: 24px;
height: 24px;
border-radius: 50%;
background: #a0cfff;
color: #fff;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.rank-info {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.team-name {
font-size: 15px;
color: #303133;
font-weight: 500;
}
.team-shift {
font-size: 12px;
color: #909399;
}
.rank-score {
display: flex;
align-items: baseline;
gap: 2px;
}
.score-value {
font-size: 24px;
color: #0066cc;
font-weight: 600;
line-height: 1;
}
.score-label {
font-size: 11px;
color: #909399;
}
/* 图表容器 */
.chart-wrapper {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px 8px;
min-height: 300px;
width: 100%;
}
/* 详细数据表格 */
.detail-table {
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
.table-header {
display: flex;
background: #0066cc;
color: #fff;
}
.header-cell {
color: #fff !important;
font-weight: 500;
}
.table-row {
display: flex;
border-bottom: 1px solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.table-cell {
flex: 1;
padding: 12px 6px;
text-align: center;
font-size: 13px;
color: #303133;
}
/* 空状态 */
.empty-state {
padding: 50px 0;
text-align: center;
}
.empty-text {
font-size: 14px;
color: #909399;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="实时监控" name="1">
</el-tab-pane>
<el-tab-pane label="生产统计" name="2">
</el-tab-pane>
<el-tab-pane label="停机统计" name="3">
</el-tab-pane>
<el-tab-pane label="班组绩效" name="4">
</el-tab-pane>
</el-tabs>
<div>
<div v-if="activeTab === '1'">
<realTimeMonitor></realTimeMonitor>
</div>
<div v-if="activeTab === '2'">
<productStatistic></productStatistic>
</div>
<div v-if="activeTab === '3'">
<shutdownStatistic></shutdownStatistic>
</div>
<div v-if="activeTab === '4'">
<TeamPerformance></TeamPerformance>
</div>
</div>
</div>
</template>
<script>
import TeamPerformance from './components/team-performance.vue'
import ShutdownStatistic from './components/shutdown-statistic.vue';
import ProductStatistic from './components/product-statistic.vue';
import RealTimeMonitor from './components/real-time-monitoring.vue';
export default {
data() {
return {
activeTab: '1'
}
},
components: {
TeamPerformance,
ShutdownStatistic,
ProductStatistic,
RealTimeMonitor
}
}
</script>
<style></style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="report-page" style="padding: 20px;">
<!-- 汇总统计信息 el-descriptions 展示 -->
<el-card shadow="hover" class="summary-card" style="margin-bottom: 20px;">
<div slot="header" class="clearfix">
<span style="font-size: 16px; font-weight: bold;">生产报表-数据汇总</span>
</div>
<el-descriptions
:data="productStatistic"
title=""
border
column="4"
size="middle"
label-width="140px"
>
<el-descriptions-item label="总出口宽度(mm)">{{ productStatistic.totalExitWidth || 0 }}</el-descriptions-item>
<el-descriptions-item label="总出口长度(m)">{{ formatNum(productStatistic.totalExitLength) }}</el-descriptions-item>
<el-descriptions-item label="总理论重量(kg)">{{ formatNum(productStatistic.totalTheoryWeight) }}</el-descriptions-item>
<el-descriptions-item label="总实际重量(kg)">{{ formatNum(productStatistic.totalActualWeight) }}</el-descriptions-item>
<el-descriptions-item label="总出口厚度(mm)">{{ formatNum(productStatistic.totalExitThickness) }}</el-descriptions-item>
<el-descriptions-item label="平均出口宽度(mm)">{{ formatNum(productStatistic.avgExitWidth) }}</el-descriptions-item>
<el-descriptions-item label="平均出口长度(m)">{{ formatNum(productStatistic.avgExitLength) }}</el-descriptions-item>
<el-descriptions-item label="平均理论重量(kg)">{{ formatNum(productStatistic.avgTheoryWeight) }}</el-descriptions-item>
<el-descriptions-item label="平均实际重量(kg)">{{ formatNum(productStatistic.avgActualWeight) }}</el-descriptions-item>
<el-descriptions-item label="平均出口厚度(mm)">{{ formatNum(productStatistic.avgExitThickness) }}</el-descriptions-item>
<el-descriptions-item label="总来料重量(kg)">{{ formatNum(productStatistic.totalEntryWeight) }}</el-descriptions-item>
<el-descriptions-item label="总卷数">{{ productStatistic.coilCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="成材率" span="4" label-width="140px">
<span style="color: #1890ff;">{{ formatRate(productStatistic.yieldRate) }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 明细数据 el-table 展示 -->
<el-card shadow="hover" class="detail-card">
<div slot="header" class="clearfix">
<span style="font-size: 16px; font-weight: bold;">生产报表-明细数据</span>
</div>
<el-table
:data="reportDetails"
border
stripe
size="small"
v-loading="tableLoading"
element-loading-text="加载中..."
style="width: 100%;"
highlight-current-row
>
<el-table-column prop="exitMatId" label="出口物料编码" align="center" />
<el-table-column prop="entryMatId" label="入口物料编码" align="center" />
<el-table-column prop="groupNo" label="班组号" align="center" width="60" />
<el-table-column prop="shiftNo" label="班次号" align="center" width="60" />
<el-table-column prop="steelGrade" label="钢种" align="center" width="80" />
<el-table-column prop="exitWidth" label="出口宽度(mm)" align="center">
<template #default="scope">{{ formatNum(scope.row.exitWidth) }}</template>
</el-table-column>
<el-table-column prop="exitLength" label="出口长度(m)" align="center">
<template #default="scope">{{ formatNum(scope.row.exitLength) }}</template>
</el-table-column>
<el-table-column prop="exitThickness" label="出口厚度(mm)" align="center">
<template #default="scope">{{ formatNum(scope.row.exitThickness) }}</template>
</el-table-column>
<el-table-column prop="theoryWeight" label="理论重量(kg)" align="center">
<template #default="scope">{{ formatNum(scope.row.theoryWeight) }}</template>
</el-table-column>
<el-table-column prop="actualWeight" label="实际重量(kg)" align="center">
<template #default="scope">{{ formatNum(scope.row.actualWeight) }}</template>
</el-table-column>
<el-table-column prop="onlineTime" label="上线时间" align="center" width="190">
<template #default="scope">{{ formatTime(scope.row.onlineTime) }}</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" align="center" width="190">
<template #default="scope">{{ formatTime(scope.row.endTime) }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { getReportSummary, getReportDetails } from '@/api/lines/zinc/report'
export default {
data() {
return {
productStatistic: {}, // 汇总数据
reportDetails: [], // 明细数据
tableLoading: false, // 表格加载状态
}
},
methods: {
// 数字格式化保留2位小数空值显示0
formatNum(num) {
return num ? Number(num).toFixed(2) : '0.00'
},
// 成材率格式化保留2位小数+百分比
formatRate(rate) {
return rate ? (Number(rate) * 100).toFixed(2) + '%' : '0.00%'
},
// 时间格式化处理ISO时间/空值/null统一格式
formatTime(time) {
if (!time) return '-'
const date = new Date(time)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hh = date.getHours().toString().padStart(2, '0')
const mm = date.getMinutes().toString().padStart(2, '0')
const ss = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hh}:${mm}:${ss}`
},
// 获取汇总数据
async getReportSummary() {
try {
const res = await getReportSummary()
this.productStatistic = res || {}
} catch (err) {
this.$message.error('获取汇总数据失败!')
console.error(err)
}
},
// 获取明细数据
async getReportDetails() {
this.tableLoading = true
try {
const res = await getReportDetails()
this.reportDetails = res || []
} catch (err) {
this.$message.error('获取明细数据失败!')
console.error(err)
} finally {
this.tableLoading = false
}
}
},
mounted() {
// 页面加载时调用两个接口
this.getReportSummary()
this.getReportDetails()
}
}
</script>
<style scoped>
.report-page {
background: #f5f7fa;
min-height: calc(100vh - 120px);
}
.summary-card, .detail-card {
background: #fff;
}
</style>

View File

@@ -0,0 +1,5 @@
<template>
<div>
实时监控
</div>
</template>

View File

@@ -0,0 +1,165 @@
<template>
<div class="stoppage-page" style="padding: 20px;">
<!-- 月份筛选查询区域 -->
<div class="search-box" style="margin: 16px 0; display: flex; align-items: center;">
<el-date-picker
v-model="month"
type="month"
placeholder="请选择月份"
value-format="yyyy-MM"
style="width: 200px;"
@change="handleMonthChange"
/>
<el-button type="primary" icon="el-icon-search" style="margin-left: 10px;" @click="handleQuery">查询</el-button>
</div>
<!-- 停机统计表格 全部修改适配真实JSON数据字段 -->
<el-table
:data="stoppageList"
border
stripe
size="small"
v-loading="loading"
element-loading-text="数据加载中..."
style="width: 100%;"
highlight-current-row
>
<el-table-column prop="stopType" label="停机类型" align="center" width="140" />
<el-table-column prop="startDate" label="停机开始时间" align="center" width="200" />
<el-table-column prop="endDate" label="停机结束时间" align="center" width="200" />
<el-table-column prop="duration" label="停机时长" align="center" width="120">
<template #default="scope">
{{ formatDuration(scope.row.duration) }}
</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>
</template>
<script>
import { listStoppage } from '@/api/lines/zinc/stoppage'
export default {
data() {
return {
stoppageList: [], // 停机统计列表
total: 0, // 数据总条数
loading: false, // 加载状态
month: '', // 选中的月份 yyyy-MM格式
queryParams: {
startDate: '', // 开始时间 yyyy-MM-dd✅ 与后端字段名一致
endDate: '' // 结束时间 yyyy-MM-dd✅ 与后端字段名一致
}
}
},
created() {
// 初始化:默认选中当前月份,并生成当月的起止时间
this.initDefaultMonth()
},
mounted() {
// 页面加载时,默认查询当月停机数据
this.listStoppage()
},
methods: {
// 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式
formatDuration(minutes) {
if (minutes === null || minutes === undefined || minutes === '') return '—'
const totalMinutes = Math.floor(Number(minutes))
if (isNaN(totalMinutes) || totalMinutes < 0) return '—'
const days = Math.floor(totalMinutes / (24 * 60))
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
const mins = Math.floor(totalMinutes % 60)
let result = ''
if (days > 0) {
result += `${days}`
}
if (hours > 0 || days > 0) {
result += `${hours}小时`
}
if (mins > 0 || result === '') {
result += `${mins}分钟`
}
return result || '0分钟'
},
/** 初始化默认月份和起止时间 */
initDefaultMonth() {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
this.month = `${year}-${month}`
// 根据当前月份生成时间范围
this.setMonthTimeRange(this.month)
},
/** 选中月份 → 生成 当月1号00:00:00 至 下个月1号00:00:00 的时间格式 */
setMonthTimeRange(month) {
if (!month) return
// 开始时间:选中月份的 1号 00:00:00
this.queryParams.startDate = `${month}-01`
// 解析选中的月份为日期对象
const selectMonth = new Date(month + '-01')
// 下个月1号月份+1日期为1号
const nextMonth = new Date(selectMonth.setMonth(selectMonth.getMonth() + 1))
const nextYear = nextMonth.getFullYear()
const nextMonthNum = (nextMonth.getMonth() + 1).toString().padStart(2, '0')
// 结束时间:下个月 1号 00:00:00
this.queryParams.endDate = `${nextYear}-${nextMonthNum}-01`
},
/** 月份选择器切换事件 */
handleMonthChange(val) {
this.setMonthTimeRange(val)
},
/** 查询按钮点击事件 */
handleQuery() {
this.listStoppage()
},
/** 核心:查询停机统计列表数据,带时间筛选参数 */
async listStoppage() {
this.loading = true
try {
const params = { ...this.queryParams }
const res = await listStoppage(params)
// 适配后端返回格式:数组/分页对象都兼容
this.stoppageList = res.data || res || []
this.total = res.total || this.stoppageList.length
} catch (err) {
this.$message.error('查询停机统计数据失败!')
console.error('停机统计查询异常:', err)
this.stoppageList = []
this.total = 0
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.stoppage-page {
background: #f5f7fa;
min-height: calc(100vh - 40px);
}
.search-box {
background: #fff;
padding: 10px 16px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="实时监控" name="1">
</el-tab-pane>
<el-tab-pane label="生产统计" name="2">
</el-tab-pane>
<el-tab-pane label="停机统计" name="3">
</el-tab-pane>
<!-- <el-tab-pane label="班组绩效" name="4">
</el-tab-pane> -->
</el-tabs>
<div>
<div v-if="activeTab === '1'">
<realTimeMonitor></realTimeMonitor>
</div>
<div v-if="activeTab === '2'">
<productStatistic></productStatistic>
</div>
<div v-if="activeTab === '3'">
<shutdownStatistic></shutdownStatistic>
</div>
<!-- <div v-if="activeTab === '4'">
<TeamPerformance></TeamPerformance>
</div> -->
</div>
</div>
</template>
<script>
// import TeamPerformance from './components/team-performance.vue'
import ShutdownStatistic from './components/shutdown-statistic.vue';
import ProductStatistic from './components/product-statistic.vue';
import RealTimeMonitor from './components/real-time-monitoring.vue';
export default {
data() {
return {
activeTab: '1'
}
},
components: {
// TeamPerformance,
ShutdownStatistic,
ProductStatistic,
RealTimeMonitor
}
}
</script>
<style></style>

View File

@@ -5,7 +5,11 @@
<el-col :span="24">
<div class="section-card material-section">
<div class="section-header">
<h3 class="section-title">查找钢卷并修正</h3>
<h3 class="section-title">
查找钢卷并修正
<span class="section-desc">绿色边框表示当前钢卷灰色边框表示历史钢卷, 蓝色边框表示已发货钢卷</span>
</h3>
<el-button size="mini" icon="el-icon-refresh" @click="getMaterialCoil">刷新</el-button>
</div>
@@ -31,13 +35,13 @@
<i class="el-icon-box"></i>
<p>暂无待领物料</p>
</div>
<div v-for="(item, index) in materialCoilList" :key="item.coilId || index" class="material-card"
:style="{ border: item.abnormalCount > 0 ? '1px solid red' : ' 1px solid #e4e7ed' }">
:style="getBorderStyle(item)">
<div class="card-header">
<div class="header-left">
<el-tag type="info" size="small" class="coil-no-tag">{{ item.currentCoilNo }}</el-tag>
<!-- <span class="material-type">{{ item.materialType || '原料' }}</span> -->
<el-tag type="info" size="small" class="coil-no-tag" :style="getBorderStyle(item)">{{ getTypeLabel(item) }}</el-tag>
<span class="material-type">{{ item.materialType || '原料' }}</span>
<el-popover v-if="item.rawMaterial || item.product" placement="top" width="280" trigger="hover"
popper-class="material-params-popover">
<div class="material-params-content">
@@ -178,10 +182,12 @@
</div>
</div>
<div class="card-footer">
<div class="card-footer" v-if="item.dataType != 10">
<el-button type="primary" icon="el-icon-check" size="mini" @click="handleCorrectMaterial(item)"
:loading="item.picking" class="action-btn">修正</el-button>
<el-button type="danger" icon="el-icon-alarm-clock" :plain="item.abnormalCount == 0" size="mini"
<el-button type="danger" icon="el-icon-delete" size="mini" @click="hanleDeleteMaterial(item)"
:loading="item.picking" class="action-btn">删除</el-button>
<el-button type="warning" icon="el-icon-alarm-clock" :plain="item.abnormalCount == 0" size="mini"
@click="handleAddAbnormal(item)" :loading="item.cancelling" class="action-btn">
异常
<span v-if="item.abnormalCount > 0">({{ item.abnormalCount }})</span>
@@ -237,7 +243,7 @@
</el-dialog>
<el-dialog title="钢卷信息修正" :visible.sync="correctVisible" width="600px">
<el-form ref="form" :model="form" label-width="100px">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="form.enterCoilNo" placeholder="请输入入场钢卷号" :disabled="form.coilId" />
</el-form-item>
@@ -260,7 +266,7 @@
<el-option key="乙" label="乙" value="乙" />
</el-select>
</el-form-item>
<el-form-item label="材料类型" prop="materialType" v-if="!form.coilId">
<el-form-item label="材料类型" prop="materialType">
<el-select v-model="form.materialType" placeholder="请选择材料类型" @change="handleMaterialTypeChange">
<el-option label="成品" value="成品" />
<el-option label="原料" value="原料" />
@@ -275,16 +281,8 @@
</el-form-item>
<el-form-item label="质量状态" prop="qualityStatus">
<el-select v-model="form.qualityStatus" placeholder="请选择质量状态" style="width: 100%">
<!-- <el-option label="A+" value="A+" />
<el-option label="A" value="A" />
<el-option label="A-" value="A-" />
<el-option label="B+" value="B+" />
<el-option label="B" value="B" />
<el-option label="B-" value="B-" />
<el-option label="C+" value="C+" />
<el-option label="C" value="C" />
<el-option label="C-" value="C-" /> -->
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
@@ -339,9 +337,9 @@
</template>
<script>
import { listMaterialCoil, updateMaterialCoilSimple } from '@/api/wms/coil'
import { listMaterialCoil, updateMaterialCoilSimple, checkCoilNo, delMaterialCoil } from '@/api/wms/coil'
import { listUser } from '@/api/system/user'
import { listPendingAction, startProcess, cancelAction, delPendingAction, addPendingAction } from '@/api/wms/pendingAction'
import { listPendingAction, startProcess, cancelAction, delPendingAction } from '@/api/wms/pendingAction'
import { parseTime } from '@/utils/klp'
import ProductInfo from '@/components/KLPService/Renderer/ProductInfo'
import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo'
@@ -354,7 +352,7 @@ import RawMaterialSelect from "@/components/KLPService/RawMaterialSelect";
export default {
name: 'DoPage',
dicts: ['action_type', 'coil_abnormal_code', 'coil_abnormal_position', 'coil_abnormal_degree'],
dicts: ['action_type', 'coil_abnormal_code', 'coil_abnormal_position', 'coil_abnormal_degree', 'coil_quality_status'],
props: {
label: {
type: String,
@@ -374,7 +372,6 @@ export default {
ProductSelect,
RawMaterialSelect,
},
dicts: ['coil_quality_status'],
data() {
return {
// 物料列表相关
@@ -385,8 +382,8 @@ export default {
materialQueryParams: {
pageNum: 1,
pageSize: 20,
dataType: 1,
status: 0,
// dataType: 1,
// status: 0,
enterCoilNo: null,
currentCoilNo: null
},
@@ -417,6 +414,77 @@ export default {
degree: null,
remark: null
},
// 表单校验
rules: {
planId: [
{ required: true, message: "请选择收货计划", trigger: "change" }
],
enterCoilNo: [
{ required: true, message: "入场钢卷号不能为空", trigger: "blur" },
// 自定义校验必须是8位的阿拉伯数字
{
validator: (rule, value, callback) => {
if (!/^\d{8}$/.test(value)) {
callback(new Error('入场钢卷号必须是8位的阿拉伯数字'));
} else {
callback();
}
}, trigger: 'blur'
},
{
validator: (rule, value, callback) => {
if (this.form.coilId) {
// 新增时触发校验
console.log('新增时触发校验');
callback();
} else {
checkCoilNo({ enterCoilNo: value }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'enter' || duplicateType === 'both') {
// alert('入场钢卷号重复,请重新输入');
callback(new Error('入场钢卷号重复,请重新输入'));
} else {
callback();
}
})
}
}, trigger: 'blur'
},
],
currentCoilNo: [
{ required: true, message: "当前钢卷号不能为空", trigger: "blur" },
// 远程校验,当前钢卷号不能重复
{
validator: (rule, value, callback) => {
checkCoilNo({ currentCoilNo: value, coilId: this.form.coilId }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'current' || duplicateType === 'both') {
// alert('当前钢卷号重复,请重新输入');
callback(new Error('当前钢卷号重复,请重新输入'));
} else {
callback();
}
})
}, trigger: 'blur'
},
],
materialType: [
{ required: true, message: "材料类型不能为空", trigger: "change" }
],
itemId: [
{ required: true, message: "物品ID不能为空", trigger: "blur" }
],
itemType: [
{ required: true, message: "物品类型不能为空", trigger: "change" }
],
// 净重和毛重
netWeight: [
{ required: true, message: "净重不能为空", trigger: "blur" }
],
grossWeight: [
{ required: true, message: "毛重不能为空", trigger: "blur" }
],
},
form: {},
correctVisible: false,
buttonLoading: false,
@@ -433,7 +501,7 @@ export default {
})
return acidAction ? parseInt(acidAction.value) : null
},
// 动态显示标签
// 动态显示标签
getItemLabel() {
if (this.form.materialType === '成品') {
return '产品类型';
@@ -500,6 +568,41 @@ export default {
},
methods: {
parseTime,
getBorderStyle(row) {
console.log(row);
// 已发货
if (row.status == 1) {
return {border: '1.5px solid #007bff'}
}
// 历史钢卷
if (row.dataType == 0) {
return {border: '1.5px solid #6c757d'}
}
// 当前钢卷
if (row.dataType == 1) {
return {border: '1.5px solid #28a745'}
}
// 还未入库的钢卷
if (row.dataType == 10) {
return {border: '1.5px solid #ffc107'}
}
return {border: '1.5px solid #e4e7ed'}
},
getTypeLabel(row) {
if (row.status == 1) {
return '已发货'
}
if (row.dataType == 0) {
return '历史钢卷'
}
if (row.dataType == 1) {
return '当前钢卷'
}
if (row.dataType == 10) {
return '还未入库的钢卷'
}
return '未知'
},
/** 查询用户列表 */
getUsers() {
listUser({ pageNum: 1, pageSize: 1000 }).then(response => {
@@ -555,36 +658,6 @@ export default {
this.materialQueryParams.currentCoilNo = null
this.handleMaterialQuery()
},
/** 领料操作 */
handlePickMaterial(row) {
if (!this.acidRollingActionType) {
this.$message.error(`未找到${this.label}操作类型,请检查字典配置`)
return
}
this.$set(row, 'picking', true)
const pendingData = {
coilId: row.coilId,
currentCoilNo: row.currentCoilNo,
actionType: this.acidRollingActionType,
actionStatus: 0, // 待处理
sourceType: 'manual', // 手动创建
warehouseId: row.warehouseId,
priority: 0, // 默认普通优先级
remark: `PC端领料创建-${this.label}`
}
this.$modal.confirm(`是否确认从${row.warehouseName || '仓库'}领料${row.currentCoilNo || '物料'}`).then(() => {
// 用户点击确认后执行的操作
return addPendingAction(pendingData)
}).then(response => {
this.$message.success('领料成功,已创建待操作任务')
this.getPendingAction() // 刷新待操作列表
}).finally(() => {
this.$set(row, 'picking', false)
})
},
// ========== 待操作列表相关方法 ==========
/** 查询待操作列表 */
@@ -612,15 +685,6 @@ export default {
this.actionLoading = false
})
},
/** 待操作搜索 */
handleActionQuery() {
this.actionQueryParams.pageNum = 1
// 确保始终使用酸连轧工序的 actionType
if (this.acidRollingActionType) {
this.actionQueryParams.actionType = this.acidRollingActionType
}
this.getPendingAction()
},
handleCorrectMaterial(row) {
this.form = {
...row,
@@ -632,26 +696,35 @@ export default {
},
/** 提交按钮 */
submitForm() {
this.buttonLoading = true;
updateMaterialCoilSimple(this.form).then(_ => {
this.$modal.msgSuccess("修正成功");
this.correctVisible = false;
this.getMaterialCoil();
}).finally(() => {
this.buttonLoading = false;
});
this.$refs['form'].validate(valid => {
if (!valid) {
this.$message.error('请填写完整信息')
return
}
this.buttonLoading = true;
updateMaterialCoilSimple(this.form).then(_ => {
this.$modal.msgSuccess("修正成功");
this.correctVisible = false;
this.getMaterialCoil();
}).finally(() => {
this.buttonLoading = false;
});
})
},
/** 重置待操作搜索 */
resetActionQuery() {
this.resetForm('actionQueryForm')
this.actionQueryParams.currentCoilNo = null
this.actionQueryParams.actionStatus = null
// 确保始终使用酸连轧工序的 actionType
if (this.acidRollingActionType) {
this.actionQueryParams.actionType = this.acidRollingActionType
}
this.handleActionQuery()
/** 删除按钮操作 */
hanleDeleteMaterial(row) {
const coilIds = row.coilId;
this.$modal.confirm('是否确认删除钢卷物料编号为"' + coilIds + '"的数据项,会同时清理刚钢卷相关的待操作记录且无法恢复!!!是否继续删除?').then(() => {
this.loading = true;
return delMaterialCoil(coilIds);
}).then(() => {
this.loading = false;
this.getMaterialCoil();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 处理操作 - 跳转到对应页面 */
handleProcess(row) {
@@ -811,6 +884,11 @@ export default {
color: #303133;
}
.section-desc {
font-size: 12px;
color: #909399;
}
.material-section {
.section-header {
border-bottom-color: #409eff;
@@ -842,7 +920,7 @@ export default {
// 卡片网格容器
.card-grid-container {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 8px;
max-height: calc(100vh - 320px);
overflow-y: auto;

View File

@@ -220,7 +220,9 @@ export default {
// 钢卷选择器筛选参数
coilFilters: {
dataType: 1,
materialType: '成品'
materialType: '成品',
selectType: 'product',
status: 0,
},
coilSelectorVisible: false,
loading: false,

View File

@@ -61,8 +61,7 @@
<el-row v-if="form.materialType === '成品'">
<el-col :span="24">
<el-form-item label="质量状态" prop="qualityStatus">
<el-select v-model="form.qualityStatus" placeholder="请选择质量状态" style="width: 100%"
:disabled="readonly">
<el-select v-model="form.qualityStatus" placeholder="请选择质量状态" style="width: 100%">
<!-- <el-option label="A+" value="A+" />
<el-option label="A" value="A" />
<el-option label="A-" value="A-" />
@@ -79,8 +78,7 @@
</el-col>
<el-col :span="24">
<el-form-item label="切边要求" prop="trimmingRequirement">
<el-select v-model="form.trimmingRequirement" placeholder="请选择切边要求" style="width: 100%"
:disabled="readonly">
<el-select v-model="form.trimmingRequirement" placeholder="请选择切边要求" style="width: 100%">
<el-option label="净边料" value="净边料" />
<el-option label="毛边料" value="毛边料" />
</el-select>
@@ -94,8 +92,7 @@
</el-col>
<el-col :span="24">
<el-form-item label="包装要求" prop="packagingRequirement">
<el-select v-model="form.packagingRequirement" placeholder="请选择包装要求" style="width: 100%"
:disabled="readonly">
<el-select v-model="form.packagingRequirement" placeholder="请选择包装要求" style="width: 100%">
<el-option label="裸包" value="裸包" />
<el-option label="普包" value="普包" />
<el-option label="简包" value="简包" />
@@ -204,7 +201,7 @@
@click="openReceiptModal(scope.row)" v-loading="buttonLoading">签收</el-button>
<el-button v-if="scope.row.actionStatus == 0 || scope.row.actionStatus == 1" type="danger"
@click="handleReject(scope.row)" v-loading="buttonLoading">拒签</el-button>
<el-button v-if="scope.row.actionStatus == 3" type="warning" v-loading="buttonLoading"
<el-button type="warning" v-loading="buttonLoading"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
@@ -285,8 +282,7 @@
<div v-else>请先选择材料类型</div>
</el-form-item>
<el-form-item label="质量状态" prop="qualityStatus">
<el-select v-model="editCoil.form.qualityStatus" placeholder="请选择质量状态" style="width: 100%"
:disabled="readonly">
<el-select v-model="editCoil.form.qualityStatus" placeholder="请选择质量状态" style="width: 100%">
<!-- <el-option label="A+" value="A+" />
<el-option label="A" value="A" />
<el-option label="A-" value="A-" />
@@ -302,21 +298,19 @@
</el-form-item>
<el-form-item label="切边要求" prop="trimmingRequirement">
<el-select v-model="editCoil.form.trimmingRequirement" placeholder="请选择切边要求" style="width: 100%"
:disabled="readonly">
<el-select v-model="editCoil.form.trimmingRequirement" placeholder="请选择切边要求" style="width: 100%">
<el-option label="净边料" value="净边料" />
<el-option label="毛边料" value="毛边料" />
</el-select>
</el-form-item>
<el-form-item label="打包状态" prop="packingStatus">
<el-input v-model="editCoil.form.packingStatus" placeholder="请输入打包状态" :disabled="readonly">
<el-input v-model="editCoil.form.packingStatus" placeholder="请输入打包状态">
</el-input>
</el-form-item>
<el-form-item label="包装要求" prop="packagingRequirement">
<el-select v-model="editCoil.form.packagingRequirement" placeholder="请选择包装要求" style="width: 100%"
:disabled="readonly">
<el-select v-model="editCoil.form.packagingRequirement" placeholder="请选择包装要求" style="width: 100%">
<el-option label="裸包" value="裸包" />
<el-option label="普包" value="普包" />
<el-option label="简包" value="简包" />
@@ -344,7 +338,7 @@
</template>
<script>
import { getMaterialCoil, listMaterialCoil, updateMaterialCoilSimple, getMaxCoilNo, checkCoilNo } from '@/api/wms/coil'
import { getMaterialCoil, listMaterialCoil, updateMaterialCoilSimple, getMaxCoilNo, checkCoilNo, delMaterialCoil } from '@/api/wms/coil'
import { listPendingAction, delPendingAction, updatePendingAction } from '@/api/wms/pendingAction'
import MaterialSelect from "@/components/KLPService/MaterialSelect";
import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect";
@@ -421,15 +415,21 @@ export default {
// 远程校验,检查钢卷号是否存在
{
validator: (rule, value, callback) => {
checkCoilNo({ enterCoilNo: value }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'enter' || duplicateType === 'both') {
// alert('入场钢卷号重复,请重新输入');
callback(new Error('入场钢卷号重复,请重新输入'));
} else {
callback();
}
})
if (this.form.coilId) {
// 新增时触发校验
console.log('新增时触发校验');
callback();
} else {
checkCoilNo({ enterCoilNo: value }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'enter' || duplicateType === 'both') {
// alert('入场钢卷号重复,请重新输入');
callback(new Error('入场钢卷号重复,请重新输入'));
} else {
callback();
}
})
}
}, trigger: 'blur'
},
],
@@ -438,7 +438,7 @@ export default {
// 远程校验,当前钢卷号不能重复
{
validator: (rule, value, callback) => {
checkCoilNo({ currentCoilNo: value }).then(res => {
checkCoilNo({ currentCoilNo: value, coilId: this.form.coilId }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'current' || duplicateType === 'both') {
// alert('当前钢卷号重复,请重新输入');
@@ -607,9 +607,11 @@ export default {
handleCoil(COIL_ACTIONS.RECEIVE, this.form, this.form.planId)
.then(res => {
this.$modal.msgSuccess("入库成功");
this.form = {
itemId: null,
itemType: 'raw_material',
materialType: '原料',
warehouseId: '1988150044862377986', // 酸连轧原料库
netWeight: null,
grossWeight: null,
@@ -618,8 +620,9 @@ export default {
trimmingRequirement: null,
packingStatus: null,
packagingRequirement: null,
planId: this.todayPlanId,
planId: this.form.planId,
}
this.getMaxCoilNoByPrefix()
this.getList()
}).finally(() => {
this.buttonLoading = false;
@@ -698,12 +701,18 @@ export default {
},
handleDelete(row) {
console.log(row.coilId)
this.$modal.confirm("确认删除吗?", "删除确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.buttonLoading = true;
if (row.coilId) {
delMaterialCoil(row.coilId).catch(err => {
this.$message.error(err.message || '删除钢卷失败')
})
}
delPendingAction(row.actionId).then(response => {
this.$modal.msgSuccess("删除成功");
this.getList();
@@ -725,7 +734,8 @@ export default {
getMaxCoilNo(prefix).then(res => {
console.log(res)
// 扣掉最后一位
this.form.enterCoilNo = res.data.maxEnterCoilNo.slice(0, -1);
this.$set(this.form, 'enterCoilNo', res.data.maxEnterCoilNo.slice(0, -1));
this.$set(this.form, 'currentCoilNo', res.data.maxEnterCoilNo.slice(0, -1));
})
},
// 检查钢卷号是否合法(后端检查)

View File

@@ -1,5 +1,5 @@
<template>
<BasePage :qrcode="qrcode" :querys="querys" :labelType="labelType" :hideType="hideType" />
<BasePage :qrcode="qrcode" :querys="querys" :labelType="labelType" :hideType="hideType" :showControl="showControl" />
</template>
<script>
@@ -9,12 +9,13 @@ export default {
components: {
BasePage
},
data() {
data() {
return {
qrcode: true,
querys: {
dataType: 0,
},
showControl: true,
labelType: '2',
hideType: false
}

View File

@@ -0,0 +1,26 @@
<template>
<BasePage :qrcode="qrcode" :querys="querys" :labelType="labelType" :hideWarehouseQuery="hideWarehouseQuery" :hideType="hideType" />
</template>
<script>
import BasePage from './panels/base.vue';
export default {
components: {
BasePage
},
data() {
return {
qrcode: false,
querys: {
dataType: 1,
warehouseIds: '1988150150521090049,1988150487185289217,1988150750390448129,1988150971895836674',
// materialType: '废品'
},
hideWarehouseQuery: true,
labelType: '2',
hideType: false,
}
}
}
</script>

View File

@@ -22,8 +22,8 @@
<el-form-item label="实际库区" prop="actualWarehouseId" v-if="!hideWarehouseQuery">
<actual-warehouse-select v-model="queryParams.actualWarehouseId" placeholder="请选择仓库/库区/库位"
style="width: 100%; display: inline-block;" clearable
:canSelectDisabled="true" :canSelectLevel2="true" :clearInput="false" :showEmpty="true"/>
style="width: 100%; display: inline-block;" clearable :canSelectDisabled="true" :canSelectLevel2="true"
:clearInput="false" :showEmpty="true" />
</el-form-item>
<el-form-item label="产品名称" prop="itemName">
@@ -91,7 +91,8 @@
</el-table-column>
<!-- <el-table-column label="厂家卷号" align="center" prop="supplierCoilNo" /> -->
<el-table-column label="逻辑库位" align="center" prop="warehouseName" v-if="!hideWarehouseQuery" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" v-if="!hideWarehouseQuery && !showExportTime" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName"
v-if="!hideWarehouseQuery && !showExportTime" />
<!-- <el-table-column label="物料类型" align="center" prop="materialType" /> -->
<el-table-column label="产品类型" align="center" width="250">
<template slot-scope="scope">
@@ -111,7 +112,8 @@
</el-table-column>
<el-table-column label="发货人" v-if="showExportTime" align="center" prop="exportByName" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.exportBy" placeholder="请选择发货人" filterable @change="handleExportByNameChange(scope.row)">
<el-select v-model="scope.row.exportBy" placeholder="请选择发货人" filterable
@change="handleExportByNameChange(scope.row)">
<el-option v-for="item in userList" :key="item.userName" :value="item.userName" :label="item.nickName" />
</el-select>
</template>
@@ -139,7 +141,8 @@
<el-table-column v-if="showGrade" label="质量状态" align="center" prop="qualityStatus">
<template slot-scope="scope">
<el-select v-model="scope.row.qualityStatus" placeholder="请选择质量状态" @change="handleGradeChange(scope.row)">
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :value="item.value" :label="item.label" />
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :value="item.value"
:label="item.label" />
</el-select>
</template>
</el-table-column>
@@ -190,8 +193,8 @@
<warehouse-select v-model="form.warehouseId" placeholder="请选择仓库/库区/库位" style="width: 100%;" clearable />
</el-form-item>
<el-form-item label="实际库区" prop="actualWarehouseId">
<actual-warehouse-select v-model="form.actualWarehouseId" :clearInput="form.coilId != null" placeholder="请选择实际库区" style="width: 100%;"
clearable />
<actual-warehouse-select v-model="form.actualWarehouseId" :clearInput="form.coilId != null"
placeholder="请选择实际库区" style="width: 100%;" clearable />
</el-form-item>
<el-form-item label="班组" prop="team">
<el-select v-model="form.team" placeholder="请选择班组" style="width: 100%">
@@ -199,7 +202,7 @@
<el-option key="乙" label="乙" value="乙" />
</el-select>
</el-form-item>
<el-form-item label="材料类型" prop="materialType" v-if="!form.coilId">
<el-form-item label="材料类型" prop="materialType">
<el-select v-model="form.materialType" placeholder="请选择材料类型" @change="handleMaterialTypeChange">
<el-option label="成品" value="成品" />
<el-option label="原料" value="原料" />
@@ -214,7 +217,8 @@
</el-form-item>
<el-form-item label="质量状态" prop="qualityStatus">
<el-select v-model="form.qualityStatus" placeholder="请选择质量状态" style="width: 100%">
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
@@ -407,7 +411,7 @@ export default {
{ label: 'C+', value: 'C+' },
{ label: 'C', value: 'C' },
{ label: 'C-', value: 'C-' },
{ label: 'D+', value: 'D+' },
{ label: 'D+', value: 'D+' },
{ label: 'D', value: 'D' },
{ label: 'D-', value: 'D-' },
],
@@ -503,22 +507,22 @@ export default {
// 仅在新增的时候校验
{
validator: (rule, value, callback) => {
if (this.form.coilId) {
// 修改时会有coilId不触发校验
console.log('修改时会有coilId不触发校验');
callback();
} else {
// 没有coilId则为新增 触发校验
checkCoilNo({ currentCoilNo: value }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'current' || duplicateType === 'both') {
// alert('当前钢卷号重复,请重新输入');
callback(new Error('当前钢卷号重复,请重新输入'));
} else {
callback();
}
})
}
// if (this.form.coilId) {
// // 修改时会有coilId不触发校验
// console.log('修改时会有coilId不触发校验');
// callback();
// } else {
// 没有coilId则为新增 触发校验
checkCoilNo({ currentCoilNo: value, coilId: this.form.coilId }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'current' || duplicateType === 'both') {
// alert('当前钢卷号重复,请重新输入');
callback(new Error('当前钢卷号重复,请重新输入'));
} else {
callback();
}
})
// }
}, trigger: 'blur'
}
],
@@ -633,7 +637,7 @@ export default {
endTime: this.queryParams.updateTime?.[1],
}
// 如果没有设置itemType则设置为raw_material
query.selectType = this.querys.materialType === '成品' ? 'product' : 'raw_material';
query.selectType = this.querys.materialType === '原料' ? 'raw_material' : 'product';
listMaterialCoil(query).then(response => {
if (this.querys.warehouseId != 111) {
// 排除掉111仓库的
@@ -645,16 +649,6 @@ export default {
this.loading = false;
});
},
/** 状态改变时触发 */
// handleStatusChange(row) {
// updateMaterialCoilSimple(row).then(res => {
// this.$message.success('状态更新成功');
// this.getList(); // 刷新列表
// }).catch(err => {
// console.error('状态更新失败:', err);
// this.$message.error('状态更新失败,请重试');
// });
// },
/** 追溯按钮操作 */
handleTrace(row) {
this.traceOpen = true;

View File

@@ -0,0 +1,300 @@
<template>
<div class="coil-duplicate-page" v-loading="loading">
<el-card shadow="hover" border>
<!-- Tab标签页 区分两个重号组 -->
<el-tabs v-model="activeTab" type="card">
<!-- 标签页一当前卷号重号组 -->
<el-tab-pane label="当前卷号重号组" name="current">
<!-- 搜索区域 + 刷新按钮 -->
<div class="search-bar">
<el-input v-model="currentSearchKey" placeholder="请输入卷号/规格/材质/厂家查询" clearable size="small"
@change="handleCurrentSearch" class="search-input" />
<el-button icon="el-icon-search" size="small" type="primary" plain @click="handleCurrentSearch"
:loading="loading" class="refresh-btn">查询</el-button>
<el-button icon="el-icon-refresh" size="small" type="primary" plain @click="handleCurrentRefresh"
:loading="loading" class="refresh-btn">刷新</el-button>
</div>
<!-- 重号组折叠面板 -->
<el-collapse v-model="currentOpenAllKeys" :accordion="false" style="margin-top:10px;" class="collapse-panel">
<el-collapse-item v-for="(item, index) in currentTableData" :key="`current-${index}`"
:name="`current-${index}`">
<!-- 折叠面板标题 -->
<template slot="title">
<span class="red-text">当前卷号{{ item.currentCoilNo }}</span>
<span class="count-text"> {{ item.coils.length }} 条重号钢卷数据</span>
</template>
<!-- 钢卷表格 -->
<el-table :data="item.coils" border stripe size="small" highlight-current-row style="width: 100%;"
empty-text="该重号组暂无钢卷数据">
<el-table-column prop="enterCoilNo" label="入厂卷号" align="center" min-width="120" />
<el-table-column prop="currentCoilNo" label="当前卷号" align="center" min-width="120" />
<el-table-column prop="productSpecification" label="规格" align="center" />
<el-table-column prop="manufacturer" label="厂家" align="center" />
<el-table-column prop="productMaterial" label="材质" align="center" />
<el-table-column prop="grossWeight" label="毛重(t)" align="center" width="90" />
<el-table-column prop="netWeight" label="净重(t)" align="center" width="90" />
<el-table-column prop="materialType" label="物料类型" align="center" />
<!-- <el-table-column prop="qualityStatus" label="质量状态" align="center" />
<el-table-column prop="trimmingRequirement" label="切边要求" align="center" />
<el-table-column prop="packagingRequirement" label="包装要求" align="center" /> -->
<el-table-column prop="createTime" label="创建时间" align="center" min-width="160" />
<el-table-column prop="updateTime" label="更新时间" align="center" min-width="160" />
</el-table>
</el-collapse-item>
</el-collapse>
<!-- 空数据兜底 -->
<div v-if="currentTableData.length === 0" class="empty-data">暂无当前卷号重号的钢卷数据</div>
</el-tab-pane>
<!-- 标签页二入厂卷号重号组 -->
<el-tab-pane label="入厂卷号重号组" name="enter">
<!-- 搜索区域 + 刷新按钮 -->
<div class="search-bar">
<el-input v-model="enterSearchKey" placeholder="请输入卷号/规格/材质/厂家查询" clearable size="small"
@change="handleEnterSearch" class="search-input" />
<el-button icon="el-icon-search" size="small" type="primary" plain @click="handleEnterSearch"
:loading="loading" class="refresh-btn">查询</el-button>
<el-button icon="el-icon-refresh" size="small" type="primary" plain @click="handleEnterRefresh"
:loading="loading" class="refresh-btn">刷新</el-button>
</div>
<!-- 重号组折叠面板 -->
<el-collapse v-model="enterOpenAllKeys" :accordion="false" style="margin-top:10px;" class="collapse-panel">
<el-collapse-item v-for="(item, index) in enterTableData" :key="`enter-${index}`" :name="`enter-${index}`">
<!-- 折叠面板标题 -->
<template slot="title">
<span class="red-text">入厂卷号{{ item.enterCoilNo }}</span>
<span class="count-text"> {{ item.coils.length }} 条重号钢卷数据</span>
</template>
<!-- 钢卷表格 -->
<el-table :data="item.coils" border stripe size="small" highlight-current-row style="width: 100%;"
empty-text="该重号组暂无钢卷数据">
<el-table-column prop="enterCoilNo" label="入厂卷号" align="center" min-width="120" />
<el-table-column prop="currentCoilNo" label="当前卷号" align="center" min-width="120" />
<el-table-column prop="productSpecification" label="规格" align="center" />
<el-table-column prop="manufacturer" label="厂家" align="center" />
<el-table-column prop="productMaterial" label="材质" align="center" />
<el-table-column prop="grossWeight" label="毛重(t)" align="center" width="90" />
<el-table-column prop="netWeight" label="净重(t)" align="center" width="90" />
<el-table-column prop="materialType" label="物料类型" align="center" />
<!-- <el-table-column prop="qualityStatus" label="质量状态" align="center" />
<el-table-column prop="trimmingRequirement" label="切边要求" align="center" />
<el-table-column prop="packagingRequirement" label="包装要求" align="center" /> -->
<el-table-column prop="createTime" label="创建时间" align="center" min-width="160" />
<el-table-column prop="updateTime" label="更新时间" align="center" min-width="160" />
</el-table>
</el-collapse-item>
</el-collapse>
<!-- 空数据兜底 -->
<div v-if="enterTableData.length === 0" class="empty-data">暂无入厂卷号重号的钢卷数据</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
import { getDuplicateGroups } from '@/api/wms/coil'
export default {
name: 'CoilDuplicateList',
data() {
return {
// 基础数据-保留原始分组结构
currentNoGroups: [],
enterNoGroups: [],
loading: false,
activeTab: 'current',
// 当前卷号重号组 - 仅保留搜索配置,删除所有分页相关变量
currentSearchKey: '',
currentTableData: [],
// 入厂卷号重号组 - 仅保留搜索配置,删除所有分页相关变量
enterSearchKey: '',
enterTableData: []
}
},
computed: {
// 当前卷号折叠面板默认全开
currentOpenAllKeys() {
return this.currentTableData.map((_, index) => `current-${index}`)
},
// 入厂卷号折叠面板默认全开
enterOpenAllKeys() {
return this.enterTableData.map((_, index) => `enter-${index}`)
}
},
mounted() {
this.getRepeatedGroups()
},
methods: {
/**
* 统一格式化钢卷数据 - 处理所有嵌套字段判空,生成扁平字段,数值兜底
* @param {Object} coil 原始钢卷对象
* @returns {Object} 格式化后的钢卷对象
*/
formatCoilData(coil) {
const product = coil.product || {}
const rawMaterial = coil.rawMaterial || {}
const warehouse = coil.warehouse || {}
return {
...coil,
productSpecification: product.specification || rawMaterial.specification || '-',
productMaterial: product.material || rawMaterial.material || '-',
manufacturer: product.manufacturer || rawMaterial.manufacturer || '-',
warehouseName: warehouse.warehouseName || '-',
grossWeight: coil.grossWeight || '-',
netWeight: coil.netWeight || '-',
materialType: coil.materialType || '-',
qualityStatus: coil.qualityStatus || '-',
trimmingRequirement: coil.trimmingRequirement || '-',
packagingRequirement: coil.packagingRequirement || '-'
}
},
/**
* 格式化重号组数据 - 对组内所有钢卷执行格式化
* @param {Array} groups 原始重号组数组
* @returns {Array} 格式化后的重号组数组
*/
formatGroupData(groups) {
return groups.map(group => ({
...group,
coils: group.coils?.map(coil => this.formatCoilData(coil)) || []
}))
},
/**
* 查询存在重号的钢卷 - 核心请求方法
*/
async getRepeatedGroups() {
this.loading = true
try {
const res = await getDuplicateGroups()
this.currentNoGroups = this.formatGroupData(res.data.currentGroups || [])
this.enterNoGroups = this.formatGroupData(res.data.enterGroups || [])
// 初始化搜索过滤
this.handleCurrentSearch()
this.handleEnterSearch()
} catch (err) {
this.$message.error('查询重号钢卷数据失败!')
console.error(err)
} finally {
this.loading = false
}
},
/**
* 当前卷号重号组 - 搜索过滤逻辑(精简版,无分页)
*/
handleCurrentSearch() {
const key = this.currentSearchKey.trim().toLowerCase()
if (!key) {
this.currentTableData = [...this.currentNoGroups]
} else {
this.currentTableData = this.currentNoGroups.filter(group => {
const groupNo = (group.currentCoilNo || '').toLowerCase()
const hasMatchCoil = group.coils.some(item => {
const enterNo = (item.enterCoilNo || '').toLowerCase()
const currentNo = (item.currentCoilNo || '').toLowerCase()
const spec = (item.productSpecification || '').toLowerCase()
const material = (item.productMaterial || '').toLowerCase()
const manufacturer = (item.manufacturer || '').toLowerCase()
// const warehouse = (item.warehouseName || '').toLowerCase()
const matType = (item.materialType || '').toLowerCase()
return enterNo.includes(key) || currentNo.includes(key) || spec.includes(key) || material.includes(key) || manufacturer.includes(key) || matType.includes(key)
})
return groupNo.includes(key) || hasMatchCoil
})
}
},
/**
* 入厂卷号重号组 - 搜索过滤逻辑(精简版,无分页)
*/
handleEnterSearch() {
const key = this.enterSearchKey.trim().toLowerCase()
if (!key) {
this.enterTableData = [...this.enterNoGroups]
} else {
this.enterTableData = this.enterNoGroups.filter(group => {
const groupNo = (group.enterCoilNo || '').toLowerCase()
const hasMatchCoil = group.coils.some(item => {
const enterNo = (item.enterCoilNo || '').toLowerCase()
const currentNo = (item.currentCoilNo || '').toLowerCase()
const spec = (item.productSpecification || '').toLowerCase()
const material = (item.productMaterial || '').toLowerCase()
const manufacturer = (item.manufacturer || '').toLowerCase()
// const warehouse = (item.warehouseName || '').toLowerCase()
const matType = (item.materialType || '').toLowerCase()
return enterNo.includes(key) || currentNo.includes(key) || spec.includes(key) || material.includes(key) || manufacturer.includes(key) || matType.includes(key)
})
return groupNo.includes(key) || hasMatchCoil
})
}
},
// 刷新按钮方法 - 重置搜索+重新请求最新数据
handleCurrentRefresh() {
this.currentSearchKey = ''
this.getRepeatedGroups()
},
handleEnterRefresh() {
this.enterSearchKey = ''
this.getRepeatedGroups()
}
}
}
</script>
<style scoped lang="scss">
.coil-duplicate-page {
padding: 15px;
box-sizing: border-box;
.search-bar {
text-align: right;
.search-input {
width: 300px;
}
.refresh-btn {
margin-left: 8px;
}
}
// 折叠面板样式优化
.collapse-panel {
::v-deep .el-collapse-item__header {
font-weight: 600;
padding: 8px 15px;
}
.red-text {
color: #E6A23C;
font-weight: bold;
}
.count-text {
color: #666;
margin-left: 10px;
}
}
// 空数据样式
.empty-data {
text-align: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
}
</style>

View File

@@ -230,7 +230,7 @@
</template>
<script>
import { getMaterialCoil, updateMaterialCoil, getMaterialCoilTrace } from '@/api/wms/coil';
import { getMaterialCoil, updateMaterialCoil, getMaterialCoilTrace, checkCoilNo } from '@/api/wms/coil';
import { completeAction } from '@/api/wms/pendingAction';
import { listWarehouse } from '@/api/wms/warehouse';
import { listRawMaterialWithBom } from '@/api/wms/rawMaterial';
@@ -292,7 +292,22 @@ export default {
},
rules: {
currentCoilNo: [
{ required: true, message: '请输入当前钢卷号', trigger: 'blur' }
{ required: true, message: '请输入当前钢卷号', trigger: 'blur' },
// 仅在新增的时候校验
{
validator: (rule, value, callback) => {
// 没有coilId则为新增 触发校验
checkCoilNo({ currentCoilNo: value, coilId: this.updateForm.coilId }).then(res => {
const { duplicateType } = res.data;
if (duplicateType === 'current' || duplicateType === 'both') {
// alert('当前钢卷号重复,请重新输入');
callback(new Error('当前钢卷号重复,请重新输入'));
} else {
callback();
}
})
}, trigger: 'blur'
}
],
team: [
{ required: true, message: '请输入班组', trigger: 'blur' }

View File

@@ -36,7 +36,7 @@
</el-table>
<!-- 分页 -->
<pagination v-if="!rangeMode" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</template>
@@ -68,8 +68,9 @@ export default {
currentCoilNo: null,
grade: null,
materialType: '成品',
selectType: 'raw_material',
dataType: 1 // 只查询当前数据,不查询历史数据
selectType: 'product',
dataType: 1, // 只查询当前数据,不查询历史数据
// status: 0 // 只查询未发货的数据
},
columns: [
{
@@ -122,7 +123,7 @@ export default {
width: '100'
},
{
label: '库区',
label: '逻辑库区',
align: 'center',
prop: 'warehouseName',
width: '120',

View File

@@ -223,6 +223,8 @@ export default {
queryParamsWithSalesId: {
selectType: 'product',
materialType: '成品',
dataType: 1,
status: 0,
saleId: undefined,
pageNum: 1,
pageSize: 10
@@ -231,6 +233,8 @@ export default {
queryParamsWithUnAssignedId: {
selectType: 'product',
materialType: '成品',
dataType: 1,
status: 0,
saleId: -1,
pageNum: 1,
pageSize: 10

View File

@@ -142,8 +142,8 @@ export default {
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 06:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 06:00:00`
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
list: [],
queryParams: {

View File

@@ -0,0 +1,48 @@
<template>
<div class="dept-chart" ref="chartRef"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'LeaveDeptChart',
props: {
chartData: {
type: Object,
default: () => ({ xAxis: [], countData: [], dayData: [] })
}
},
watch: {
chartData: { deep: true, handler: 'initChart' }
},
mounted() { this.initChart(); window.addEventListener('resize', this.resizeChart); },
beforeDestroy() { window.removeEventListener('resize', this.resizeChart); this.chart?.dispose(); },
data() { return { chart: null }; },
methods: {
initChart() {
const dom = this.$refs.chartRef;
if (!dom) return;
this.chart = echarts.init(dom);
const option = {
title: { text: '按部门请假统计', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis' },
legend: { data: ['请假记录数', '请假总天数'], top: 30 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: [{ type: 'category', data: this.chartData.xAxis }],
yAxis: [{ type: 'value' }],
series: [
{ name: '请假记录数', type: 'bar', data: this.chartData.countData, barWidth: '30%' },
{ name: '请假总天数', type: 'bar', data: this.chartData.dayData, barWidth: '30%' }
]
};
this.chart.setOption(option);
},
resizeChart() { this.chart?.resize(); }
}
};
</script>
<style scoped>
.dept-chart { width: 100%; height: 100%; }
</style>

View File

@@ -0,0 +1,38 @@
<template>
<el-table :data="tableData" :loading="loading" highlight-current-row border stripe style="width: 100%"
:row-key="'leaveId'" :tree-props="{ children: 'children' }" default-expand-all>
<el-table-column prop="userName" label="请假人" align="center" width="120" />
<el-table-column label="请假总次数" align="center" prop="leaveCount"></el-table-column>
<el-table-column label="请假总时长(天)" align="center" prop="leaveDays"></el-table-column>
<el-table-column prop="leaveType" label="请假类型" align="center" width="120" />
<el-table-column prop="leaveDays" label="请假时长(天)" align="center" width="120" />
<el-table-column prop="leaveReason" label="请假理由" align="center" show-overflow-tooltip />
<el-table-column prop="startTime" label="开始时间" align="center" show-overflow-tooltip />
<el-table-column prop="endTime" label="结束时间" align="center" show-overflow-tooltip />
<el-table-column prop="leaveShift" label="请假班次" align="center" show-overflow-tooltip />
</el-table>
</template>
<script>
export default {
name: 'LeaveRecordTable',
props: {
tableData: {
type: Array,
default: () => [
{ userName: '张三', leaveCount: 2, leaveDays: 3.5, children: [] },
{ userName: '李四', leaveCount: 1, leaveDays: 1.5, children: [] },
]
},
loading: { type: Boolean, default: false }
},
};
</script>
<style scoped>
/* ✅ 完全正确Vue2的深度选择器就是 ::v-deep不用改 */
::v-deep .el-table {
--el-table-header-text-color: #333;
--el-table-row-hover-bg-color: #fafafa;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="trend-chart" ref="chartRef"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'LeaveTrendChart',
props: {
chartData: {
type: Object,
default: () => ({ xAxis: [], countData: [], dayData: [] })
}
},
watch: {
chartData: {
deep: true,
handler() {
this.initChart();
}
}
},
mounted() {
this.initChart();
window.addEventListener('resize', this.resizeChart);
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeChart);
this.chart && this.chart.dispose();
},
data() {
return {
chart: null
};
},
methods: {
initChart() {
const dom = this.$refs.chartRef;
if (!dom) return;
this.chart = echarts.init(dom);
const option = {
title: { text: '按时间请假统计', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['请假记录数', '请假总天数'], top: 30 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: [{ type: 'category', data: this.chartData.xAxis }],
yAxis: [{ type: 'value' }],
series: [
{ name: '请假记录数', type: 'line', data: this.chartData.countData, smooth: true },
{ name: '请假总天数', type: 'line', data: this.chartData.dayData, smooth: true }
]
};
this.chart.setOption(option);
},
resizeChart() {
this.chart && this.chart.resize();
}
}
};
</script>
<style scoped>
.trend-chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="type-chart" ref="chartRef"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'LeaveTypeChart',
props: {
chartData: {
type: Object,
default: () => ({ countPie: [], dayPie: [] })
}
},
watch: {
chartData: { deep: true, handler: 'initChart' }
},
mounted() { this.initChart(); window.addEventListener('resize', this.resizeChart); },
beforeDestroy() { window.removeEventListener('resize', this.resizeChart); this.chart?.dispose(); },
data() { return { chart: null }; },
methods: {
initChart() {
const dom = this.$refs.chartRef;
if (!dom) return;
this.chart = echarts.init(dom);
const option = {
title: { text: '按请假类型统计', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'item' },
legend: { top: 30 },
grid: { left: '0%', right: '0%', bottom: '0%', containLabel: true },
series: [
{
name: '请假记录数占比',
type: 'pie',
radius: ['20%', '40%'],
center: ['25%', '60%'],
data: this.chartData.countPie,
label: { show: true, fontSize: 12 }
},
{
name: '请假天数占比',
type: 'pie',
radius: ['20%', '40%'],
center: ['75%', '60%'],
data: this.chartData.dayPie,
label: { show: true, fontSize: 12 }
}
]
};
this.chart.setOption(option);
},
resizeChart() { this.chart?.resize(); }
}
};
</script>
<style scoped>
.type-chart { width: 100%; height: 100%; }
</style>

View File

@@ -0,0 +1,256 @@
<template>
<div class="leave-statistics-container">
<!-- 1. 时间筛选区域 -->
<div class="search-form">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="请假时间">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始时间"
end-placeholder="结束时间" value-format="yyyy-MM-dd" style="width: 260px"></el-date-picker>
</el-form-item>
<el-form-item label="请假人">
<dict-select v-model="queryParams.applicantName" dict-type="hrm_leave_employee" placeholder="请选择请假人"
style="width: 260px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<el-button icon="el-icon-download" type="warning" plain @click="handleExport">导出</el-button>
</el-form-item>
</el-form>
</div>
<!-- 2. 图表展示区域 - 横向三个图表组件 -->
<div class="chart-box">
<div class="chart-item">
<leave-trend-chart :chart-data="trendChartData" />
</div>
<div class="chart-item">
<leave-dept-chart :chart-data="deptChartData" />
</div>
<div class="chart-item">
<leave-type-chart :chart-data="typeChartData" />
</div>
</div>
<!-- 3. 请假记录表格区域 - 封装的折叠表格组件 -->
<div class="table-box">
<leave-record-table :table-data="tableGroupData" :loading="loading" />
</div>
</div>
</template>
<script>
import { listLeaveRequest } from "@/api/wms/leaveRequest";
// 引入4个封装的子组件
import LeaveTrendChart from './components/LeaveTrendChart';
import LeaveDeptChart from './components/LeaveDeptChart';
import LeaveTypeChart from './components/LeaveTypeChart';
import LeaveRecordTable from './components/LeaveRecordTable';
import DictSelect from '@/components/DictSelect';
export default {
name: 'LeaveStatistics',
components: {
LeaveTrendChart,
LeaveDeptChart,
LeaveTypeChart,
LeaveRecordTable,
DictSelect
},
data() {
return {
loading: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 9999, // 查全部数据用于图表统计
startTime: '',
endTime: '',
},
leaveIds: '', // 请假人ID列表
dateRange: [], // 时间筛选绑定值
leaveRequestList: [], // 原始请假数据列表
// 图表渲染数据
trendChartData: { xAxis: [], countData: [], dayData: [] }, // 折线图数据
deptChartData: { xAxis: [], countData: [], dayData: [] }, // 柱状图数据
typeChartData: { countPie: [], dayPie: [] }, // 饼图数据
tableGroupData: [] // 表格分组数据(按请假人聚合)
};
},
mounted() {
// 初始化默认时间:当月第一天 至 当前时间
this.initDefaultTime();
// 加载数据
this.getList();
},
methods: {
// 初始化默认时间 - 当月1号 00:00:00 至 当前时间 yyyy-MM-dd HH:mm:ss
initDefaultTime() {
const now = new Date();
// 当月第一天
const monthFirst = new Date(now.getFullYear(), now.getMonth(), 1);
const startTime = this.formatDateTime(monthFirst);
const endTime = this.formatDateTime(now);
this.dateRange = [startTime, endTime];
// this.queryParams.startTime = startTime;
// this.queryParams.endTime = endTime;
},
// 时间格式化yyyy-MM-dd HH:mm:ss
formatDateTime(date) {
const y = date.getFullYear();
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const d = date.getDate().toString().padStart(2, '0');
const h = date.getHours().toString().padStart(2, '0');
const mm = date.getMinutes().toString().padStart(2, '0');
const s = date.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d}`;
},
// 查询按钮事件
handleQuery() {
if (this.dateRange && this.dateRange.length === 2) {
// this.queryParams.startTime = this.dateRange[0];
// this.queryParams.endTime = this.dateRange[1];
}
this.getList();
},
// 重置按钮事件
resetQuery() {
this.dateRange = [];
this.initDefaultTime();
this.getList();
},
// 调取接口获取数据
getList() {
this.loading = true;
listLeaveRequest(this.queryParams).then(res => {
this.leaveRequestList = res.rows || [];
// 提取所有请假人ID
this.leaveIds = this.leaveRequestList.map(item => item.leaveId).join(',');
// 核心:数据处理,为图表和表格格式化数据
this.formatAllData();
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
handleExport() {
// 导出逻辑
if (!this.leaveIds) {
this.$message.warning('请先查询数据');
return;
}
this.download('/wms/leaveRequest/export', {
leaveIds: this.leaveIds
}, '请假记录.xlsx');
},
// 格式化所有图表+表格数据
formatAllData() {
const list = this.leaveRequestList;
// ========== 1. 处理【按时间汇总】折线图数据 ==========
const timeObj = {};
list.forEach(item => {
const day = item.startTime?.split(' ')[0]; // 按日期聚合
if (!day) return;
if (!timeObj[day]) {
timeObj[day] = { count: 0, days: 0 };
}
timeObj[day].count += 1; // 记录数+1
timeObj[day].days += Number(item.leaveDays) || 0; // 请假天数累加
});
this.trendChartData.xAxis = Object.keys(timeObj).sort(); // 日期排序
this.trendChartData.countData = this.trendChartData.xAxis.map(k => timeObj[k].count);
this.trendChartData.dayData = this.trendChartData.xAxis.map(k => timeObj[k].days);
// ========== 2. 处理【按部门汇总】柱状图数据 ==========
const deptObj = {};
list.forEach(item => {
const dept = item.applicantDeptName || '未知部门';
if (!deptObj[dept]) {
deptObj[dept] = { count: 0, days: 0 };
}
deptObj[dept].count += 1;
deptObj[dept].days += Number(item.leaveDays) || 0;
});
this.deptChartData.xAxis = Object.keys(deptObj);
this.deptChartData.countData = this.deptChartData.xAxis.map(k => deptObj[k].count);
this.deptChartData.dayData = this.deptChartData.xAxis.map(k => deptObj[k].days);
// ========== 3. 处理【按请假类型汇总】饼图数据 ==========
const typeObj = {};
list.forEach(item => {
const type = item.leaveType || '未知类型';
if (!typeObj[type]) {
typeObj[type] = { count: 0, days: 0 };
}
typeObj[type].count += 1;
typeObj[type].days += Number(item.leaveDays) || 0;
});
this.typeChartData.countPie = Object.keys(typeObj).map(k => ({ name: k, value: typeObj[k].count }));
this.typeChartData.dayPie = Object.keys(typeObj).map(k => ({ name: k, value: typeObj[k].days }));
console.log(this.tableGroupData, '按请假人分组表格数据');
// ========== 4. 处理【按请假人分组】表格折叠数据 ==========
const userObj = {};
list.forEach(item => {
const userName = item.applicantName || '未知人员';
if (!userObj[userName]) {
userObj[userName] = [];
}
userObj[userName].push({
...item,
leaveTime: `${item.startTime} ~ ${item.endTime}`, // 拼接请假时间段
leaveDays: parseInt(item.leaveDays || 0) // 转换为整数
});
});
this.tableGroupData = Object.keys(userObj).map(user => ({
leaveId: user,
userName: user,
leaveCount: userObj[user].length || 0, // 请假次数
leaveDays: parseInt(userObj[user].reduce((sum, item) => sum + item.leaveDays, 0)), // 请假总时长(整数)
children: userObj[user] // 折叠子数据
}));
console.log(this.tableGroupData, '按请假人分组表格数据');
}
}
};
</script>
<style scoped lang="scss">
.leave-statistics-container {
padding: 20px;
box-sizing: border-box;
}
.search-form {
margin-bottom: 20px;
}
.chart-box {
display: flex;
gap: 15px;
margin-bottom: 20px;
height: 320px;
.chart-item {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
box-sizing: border-box;
}
}
.table-box {
width: 100%;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,371 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="60px">
<el-form-item label="请假类型" prop="leaveType">
<el-select v-model="queryParams.leaveType" placeholder="请选择请假类型" clearable>
<el-option v-for="dict in dict.type.hrm_leave_type" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="请假人" prop="applicantName">
<dict-select v-model="queryParams.applicantName" dict-type="hrm_leave_employee"
placeholder="请选择请假人姓名"></dict-select>
</el-form-item>
<el-form-item label="部门名称" prop="applicantDeptName">
<dict-select v-model="queryParams.applicantDeptName" dict-type="hrm_department"
placeholder="请选择请假人部门名称"></dict-select>
</el-form-item>
<el-form-item label="请假班次" prop="leaveShift">
<el-select v-model="queryParams.leaveShift" placeholder="请选择请假班次" clearable>
<el-option v-for="dict in dict.type.hrm_leave_shift" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
@click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="leaveRequestList" @selection-change="handleSelectionChange">
<el-table-column label="请假类型" align="center" prop="leaveType">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_type" :value="scope.row.leaveType" />
</template>
</el-table-column>
<el-table-column label="请假人姓名" align="center" prop="applicantName">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_employee" :value="scope.row.applicantName" />
</template>
</el-table-column>
<el-table-column label="部门名称" align="center" prop="applicantDeptName">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_department" :value="scope.row.applicantDeptName" />
</template>
</el-table-column>
<el-table-column label="请假标题" align="center" prop="leaveTitle" show-overflow-tooltip />
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="请假班次" align="center" prop="leaveShift">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_shift" :value="scope.row.leaveShift" />
</template>
</el-table-column>
<el-table-column label="具体原因" align="center" prop="leaveReason" show-overflow-tooltip />
<el-table-column label="请假天数" align="center" prop="leaveDays" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 添加或修改员工请假申请对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="请假类型" prop="leaveType">
<el-select v-model="form.leaveType" placeholder="请选择请假类型">
<el-option v-for="dict in dict.type.hrm_leave_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="请假人姓名" prop="applicantName">
<dict-select v-model="form.applicantName" dict-type="hrm_leave_employee" placeholder="请选择请假人姓名"></dict-select>
</el-form-item>
<el-form-item label="部门名称" prop="applicantDeptName">
<dict-select v-model="form.applicantDeptName" dict-type="hrm_department" 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="请输入内容" />
</el-form-item>
<!-- <el-form-item label="请假标题" prop="leaveTitle">
<el-input v-model="form.leaveTitle" placeholder="请输入请假标题" />
</el-form-item> -->
<el-form-item label="附件" prop="attachmentUrls">
<FileUpload v-model="form.attachmentUrls" :max-count="1" :show-file-list="true" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listLeaveRequest, getLeaveRequest, delLeaveRequest, addLeaveRequest, updateLeaveRequest } from "@/api/wms/leaveRequest";
import FileUpload from '@/components/FileUpload'
import DictSelect from '@/components/DictSelect'
export default {
name: "LeaveRequest",
dicts: ['hrm_leave_shift', 'hrm_leave_type', 'hrm_department', 'hrm_leave_employee'],
components: {
FileUpload,
DictSelect
},
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 员工请假申请表格数据
leaveRequestList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
leaveType: undefined,
applicantName: undefined,
applicantDeptName: undefined,
leaveShift: undefined,
},
// 表单参数
form: {},
// 表单校验规则【核心新增:完整必填校验】
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'] },
],
leaveReason: [
{ required: true, message: '具体原因不能为空', trigger: ['blur', 'change'] },
]
}
};
},
// 核心新增:监听开始/结束时间变化,自动计算天数
watch: {
'form.startTime': {
handler() {
this.calculateLeaveDays()
},
immediate: false
},
'form.endTime': {
handler() {
this.calculateLeaveDays()
},
immediate: false
},
},
created() {
this.getList();
},
methods: {
/** 查询员工请假申请列表 */
getList() {
this.loading = true;
listLeaveRequest(this.queryParams).then(response => {
this.leaveRequestList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
leaveId: undefined,
leaveTitle: undefined,
leaveType: undefined,
applicantName: undefined,
applicantDeptName: undefined,
startTime: undefined,
endTime: undefined,
leaveShift: undefined,
leaveDays: undefined,
leaveReason: undefined,
attachmentUrls: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined,
remark: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.leaveId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加员工请假申请";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const leaveId = row.leaveId || this.ids
getLeaveRequest(leaveId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改员工请假申请";
});
},
/** 提交按钮 */
submitForm() {
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.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addLeaveRequest(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const leaveIds = row.leaveId || this.ids;
this.$modal.confirm('是否确认删除员工请假申请编号为"' + leaveIds + '"的数据项?').then(() => {
this.loading = true;
return delLeaveRequest(leaveIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('wms/leaveRequest/export', {
...this.queryParams
}, `leaveRequest_${new Date().getTime()}.xlsx`)
},
// 核心新增:自动计算请假天数的方法
calculateLeaveDays() {
const { startTime, endTime } = this.form;
// 两个时间都选择后才计算
if (startTime && endTime) {
// 转成时间戳
const start = new Date(startTime).getTime();
const end = new Date(endTime).getTime();
// 判断结束时间不能小于开始时间
if (end < start) {
this.$modal.msgWarning('结束时间不能早于开始时间,请重新选择!');
this.form.leaveDays = undefined;
return;
}
// 计算时间差(毫秒) → 转天 → 保留2位小数
const diffTime = end - start;
const diffDays = (diffTime / (1000 * 60 * 60 * 24)).toFixed(2);
// 赋值到天数输入框
this.form.leaveDays = diffDays;
}
}
}
};
</script>

View File

@@ -0,0 +1,369 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="60px">
<el-form-item label="餐别" prop="mealType">
<el-select v-model="queryParams.mealType" placeholder="请选择餐别" clearable>
<el-option
v-for="dict in dict.type.hrm_meal_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="用餐日期" prop="reportDate">
<el-date-picker
v-model="queryParams.reportDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择用餐日期"
clearable
/>
</el-form-item>
<el-form-item label="部门名称" prop="deptName">
<DictSelect dictType="hrm_department" v-model="queryParams.deptName" placeholder="请选择部门名称"></DictSelect>
</el-form-item>
<el-form-item label="报餐人" prop="reportUserName">
<DictSelect dictType="hrm_leave_employee" v-model="queryParams.reportUserName" placeholder="请选择报餐人姓名"></DictSelect>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 新增表格上方的人数合计统计组件 el-description -->
<el-descriptions size="small" border class="mb8" :column="3">
<el-descriptions-item label="堂食人数合计">{{ totalDineIn }}</el-descriptions-item>
<el-descriptions-item label="打包人数合计">{{ totalTakeout }}</el-descriptions-item>
<el-descriptions-item label="用餐总人数合计">{{ totalAll }}</el-descriptions-item>
</el-descriptions>
<el-table v-loading="loading" :data="mealReportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="reportId" v-if="false"/>
<el-table-column label="用餐日期" align="center" prop="reportDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.reportDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="餐别" align="center" prop="mealType">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_meal_type" :value="scope.row.mealType"/>
</template>
</el-table-column>
<el-table-column label="部门名称" align="center" prop="deptName">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_department" :value="scope.row.deptName"/>
</template>
</el-table-column>
<el-table-column label="用餐总人数" align="center" prop="totalPeople" />
<el-table-column label="堂食人数" align="center" prop="dineInPeople" />
<el-table-column label="打包人数" align="center" prop="takeoutPeople" />
<el-table-column label="报餐人姓名" align="center" prop="reportUserName">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_employee" :value="scope.row.reportUserName"/>
</template>
</el-table-column>
<el-table-column label="报餐时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改部门报餐主对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用餐日期" prop="reportDate">
<el-date-picker clearable
v-model="form.reportDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择用餐日期">
</el-date-picker>
</el-form-item>
<el-form-item label="餐别" prop="mealType">
<el-select v-model="form.mealType" placeholder="请选择餐别">
<el-option
v-for="dict in dict.type.hrm_meal_type"
:key="dict.value"
:label="dict.label"
:value="parseInt(dict.value)"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="部门名称" prop="deptName">
<DictSelect dictType="hrm_department" v-model="form.deptName" placeholder="请选择部门名称"></DictSelect>
</el-form-item>
<!-- 优化增加number类型禁止输入非数字限制最小值0 -->
<el-form-item label="堂食人数" prop="dineInPeople">
<el-input v-model="form.dineInPeople" placeholder="请输入堂食人数" type="number" min="0" />
</el-form-item>
<el-form-item label="打包人数" prop="takeoutPeople">
<el-input v-model="form.takeoutPeople" placeholder="请输入打包人数" type="number" min="0" />
</el-form-item>
<el-form-item label="用餐总人数" prop="totalPeople">
<el-input v-model="form.totalPeople" placeholder="请输入用餐总人数" disabled />
</el-form-item>
<el-form-item label="报餐人姓名" prop="reportUserName">
<DictSelect dictType="hrm_leave_employee" v-model="form.reportUserName" placeholder="请选择报餐人姓名"></DictSelect>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMealReport, getMealReport, delMealReport, addMealReport, updateMealReport } from "@/api/wms/mealReport";
import DictSelect from "@/components/DictSelect";
export default {
name: "MealReport",
dicts: ['hrm_meal_type', 'hrm_leave_employee', 'hrm_department'],
components: { DictSelect },
data() {
// 用餐日期默认选中今天
// 格式化日期为yyyy-MM-dd
function addZero(num) {
return num < 10 ? '0' + num : num;
}
const now = new Date()
const nowYear = now.getFullYear()
const nowMonth = addZero(now.getMonth() + 1)
const nowDay = addZero(now.getDate())
const nowTime = `${nowYear}-${nowMonth}-${nowDay}`
return {
buttonLoading: false,
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
mealReportList: [],
title: "",
open: false,
queryParams: {
pageNum: 1,
pageSize: 20,
reportDate: nowTime,
mealType: undefined,
deptName: undefined,
reportUserName: undefined,
status: undefined,
},
form: {},
// 合计统计的三个变量 ✅新增
totalDineIn: 0, // 堂食合计
totalTakeout: 0, // 打包合计
totalAll: 0, // 总人数合计
// 表单校验规则
rules: {
reportDate: [{ required: true, message: '用餐日期不能为空', trigger: 'change' }],
mealType: [{ required: true, message: '餐别不能为空', trigger: 'change' }],
deptName: [{ required: true, message: '部门名称不能为空', trigger: 'change' }],
dineInPeople: [{ required: true, message: '堂食人数不能为空', trigger: 'blur' }],
takeoutPeople: [{ required: true, message: '打包人数不能为空', trigger: 'blur' }],
totalPeople: [{ required: true, message: '用餐总人数不能为空', trigger: 'blur' }],
reportUserName: [{ required: true, message: '报餐人姓名不能为空', trigger: 'change' }]
}
};
},
created() {
this.getList();
},
watch: {
'form.dineInPeople': {
handler(val) {
this.calcTotalPeople();
},
immediate: true
},
'form.takeoutPeople': {
handler(val) {
this.calcTotalPeople();
},
immediate: true
}
},
methods: {
/** 查询部门报餐主列表 */
getList() {
this.loading = true;
listMealReport(this.queryParams).then(response => {
this.mealReportList = response.rows;
this.total = response.total;
this.loading = false;
this.calcTableSum(); // ✅新增:获取表格数据后,立即计算合计
});
},
// ✅ 核心新增:计算表格中堂食、打包、总人数的合计方法
calcTableSum(){
let dineIn = 0;
let takeout = 0;
let all = 0;
// 遍历表格数据累加,处理空值/非数字避免NaN
this.mealReportList.forEach(item => {
dineIn += item.dineInPeople ? Number(item.dineInPeople) : 0;
takeout += item.takeoutPeople ? Number(item.takeoutPeople) : 0;
all += item.totalPeople ? Number(item.totalPeople) : 0;
});
// 赋值到统计变量
this.totalDineIn = dineIn;
this.totalTakeout = takeout;
this.totalAll = all;
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
reportId: undefined,
reportDate: undefined,
mealType: undefined,
deptName: undefined,
totalPeople: undefined,
dineInPeople: undefined,
takeoutPeople: undefined,
reportUserName: undefined,
status: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined,
remark: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.reportId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加部门报餐主";
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0');
this.form.reportDate = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const reportId = row.reportId || this.ids
getMealReport(reportId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改部门报餐主";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.reportId != null) {
updateMealReport(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addMealReport(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const reportIds = row.reportId || this.ids;
this.$modal.confirm('是否确认删除部门报餐主编号为"' + reportIds + '"的数据项?').then(() => {
this.loading = true;
return delMealReport(reportIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('wms/mealReport/export', { ...this.queryParams }, `mealReport_${new Date().getTime()}.xlsx`)
},
// 计算单行总人数
calcTotalPeople() {
const dine = this.form.dineInPeople ? Number(this.form.dineInPeople) : 0;
const take = this.form.takeoutPeople ? Number(this.form.takeoutPeople) : 0;
this.form.totalPeople = dine + take;
}
}
};
</script>

View File

@@ -0,0 +1,405 @@
<template>
<div class="app-container">
<!-- 请假申请统计模块 -->
<el-row :gutter="20">
<!-- 请假类型统计-饼图+柱状图 占比+数量 -->
<el-col :span="8">
<div class="chart-title">请假类型分布统计</div>
<div id="leaveTypeChart" class="chart-container"></div>
</el-col>
<!-- 请假部门统计-横向柱状图 -->
<el-col :span="8">
<div class="chart-title">各部门请假情况统计</div>
<div id="leaveDeptChart" class="chart-container"></div>
</el-col>
<!-- 餐别统计-饼图 堂食/打包占比 -->
<el-col :span="8">
<div class="chart-title">餐别-堂食/打包占比</div>
<div id="mealTypePieChart" class="chart-container"></div>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 请假月度趋势统计-组合图 次数+总天数 -->
<el-col :span="12">
<div class="chart-title">月度请假趋势统计 (次数/总天数)</div>
<div id="leaveMonthChart" class="chart-container"></div>
</el-col>
<!-- 报餐日期趋势统计 -->
<el-col :span="12">
<div class="chart-title">日报餐人数趋势统计</div>
<div id="mealDateChart" class="chart-container"></div>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 餐别统计-柱状图 用餐人数+报餐次数 -->
<el-col :span="12">
<div class="chart-title">各餐别用餐人数&报餐次数</div>
<div id="mealTypeBarChart" class="chart-container"></div>
</el-col>
<!-- 报餐部门统计-柱状图 -->
<el-col :span="12">
<div class="chart-title">各部门报餐汇总统计</div>
<div id="mealDeptChart" class="chart-container"></div>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getLeaveTypeCount, getDeptCount as getLeaveDeptCount, getMonthCount as getLeaveMonthCount } from '@/api/wms/leaveRequest'
import { getMealTypeCount, getDeptCount as getMealDeptCount, getDateCount as getMealDateCount } from '@/api/wms/mealReport'
export default {
name: 'Report',
data() {
return {
// echarts实例对象
leaveTypeChart: null,
leaveDeptChart: null,
leaveMonthChart: null,
mealTypePieChart: null,
mealTypeBarChart: null,
mealDeptChart: null,
mealDateChart: null,
// 各接口数据存储
leaveTypeData: [],
leaveDeptData: [],
leaveMonthData: [],
mealTypeData: [],
mealDeptData: [],
mealDateData: []
}
},
mounted() {
// 页面加载完成后初始化所有图表+请求数据
this.initAllCharts()
this.fetchAllData()
// 窗口大小改变时,图表自适应
window.addEventListener('resize', this.resizeAllCharts)
},
beforeDestroy() {
// 销毁图表实例,防止内存泄漏
window.removeEventListener('resize', this.resizeAllCharts)
this.disposeAllCharts()
},
methods: {
// 初始化所有图表容器
initAllCharts() {
this.leaveTypeChart = echarts.init(document.getElementById('leaveTypeChart'))
this.leaveDeptChart = echarts.init(document.getElementById('leaveDeptChart'))
this.leaveMonthChart = echarts.init(document.getElementById('leaveMonthChart'))
this.mealTypePieChart = echarts.init(document.getElementById('mealTypePieChart'))
this.mealTypeBarChart = echarts.init(document.getElementById('mealTypeBarChart'))
this.mealDeptChart = echarts.init(document.getElementById('mealDeptChart'))
this.mealDateChart = echarts.init(document.getElementById('mealDateChart'))
},
// 请求所有接口数据
fetchAllData() {
Promise.all([
this.getLeaveTypeCount({}),
this.getLeaveDeptCount({}),
this.getLeaveMonthCount({}),
this.getMealTypeCount({}),
this.getMealDeptCount({}),
this.getMealDateCount({})
]).then(() => {
// 所有数据请求完成后绘制图表
this.renderLeaveTypeChart()
this.renderLeaveDeptChart()
this.renderLeaveMonthChart()
this.renderMealTypePieChart()
this.renderMealTypeBarChart()
this.renderMealDeptChart()
this.renderMealDateChart()
})
},
// 图表自适应窗口大小
resizeAllCharts() {
this.leaveTypeChart?.resize()
this.leaveDeptChart?.resize()
this.leaveMonthChart?.resize()
this.mealTypePieChart?.resize()
this.mealTypeBarChart?.resize()
this.mealDeptChart?.resize()
this.mealDateChart?.resize()
},
// 销毁所有图表实例
disposeAllCharts() {
this.leaveTypeChart?.dispose()
this.leaveDeptChart?.dispose()
this.leaveMonthChart?.dispose()
this.mealTypePieChart?.dispose()
this.mealTypeBarChart?.dispose()
this.mealDeptChart?.dispose()
this.mealDateChart?.dispose()
},
// ======================== 请假相关接口及图表绘制 ========================
// 请假类型统计
getLeaveTypeCount(query) {
return getLeaveTypeCount(query).then(res => {
this.leaveTypeData = res.data || []
return res
})
},
// 绘制请假类型饼图
renderLeaveTypeChart() {
const data = this.leaveTypeData.map(item => ({
name: item.type,
value: item.count,
days: item.total_days
}))
const option = {
tooltip: { trigger: 'item', formatter: '{b}: <br/>请假次数: {c}次 <br/>请假总天数: {d}天' },
legend: { orient: 'vertical', top: 'middle', right: 10 },
series: [
{
name: '请假类型',
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
color: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399'],
label: { show: false, position: 'center' },
emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } },
labelLine: { show: false },
data: data
}
]
}
this.leaveTypeChart.setOption(option)
},
// 部门请假统计
getLeaveDeptCount(query) {
return getLeaveDeptCount(query).then(res => {
this.leaveDeptData = res.data || []
return res
})
},
// 绘制部门请假横向柱状图
renderLeaveDeptChart() {
const xData = this.leaveDeptData.map(item => item.dept_name)
const countData = this.leaveDeptData.map(item => item.count)
const dayData = this.leaveDeptData.map(item => Number(item.total_days))
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: 0, right: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'value', axisLabel: { formatter: '{value}' } },
yAxis: { type: 'category', data: xData },
series: [
{ name: '请假次数', type: 'bar', barWidth: '40%', data: countData, itemStyle: { color: '#409EFF' } },
{ name: '请假天数', type: 'bar', barWidth: '40%', data: dayData, itemStyle: { color: '#F56C6C' } }
]
}
this.leaveDeptChart.setOption(option)
},
// 月份请假统计
getLeaveMonthCount(query) {
return getLeaveMonthCount(query).then(res => {
this.leaveMonthData = res.data || []
return res
})
},
// 绘制月度请假趋势组合图
renderLeaveMonthChart() {
const xData = this.leaveMonthData.map(item => item.month)
const countData = this.leaveMonthData.map(item => item.count)
const dayData = this.leaveMonthData.map(item => Number(item.total_days))
const option = {
tooltip: { trigger: 'axis' },
legend: { top: 0, right: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: xData },
yAxis: [{ type: 'value', name: '请假次数' }, { type: 'value', name: '请假天数', right: '15%' }],
series: [
{ name: '请假次数', type: 'bar', yAxisIndex: 0, data: countData, itemStyle: { color: '#409EFF' } },
{ name: '请假总天数', type: 'line', yAxisIndex: 1, data: dayData, itemStyle: { color: '#F56C6C' }, lineStyle: { width: 3 }, symbol: 'circle' }
]
}
this.leaveMonthChart.setOption(option)
},
// ======================== 报餐相关接口及图表绘制 ========================
// 餐别统计
getMealTypeCount(query) {
return getMealTypeCount(query).then(res => {
this.mealTypeData = res.data || []
return res
})
},
// 绘制餐别-堂食打包占比饼图
renderMealTypePieChart() {
let totalDineIn = 0
let totalTakeout = 0
this.mealTypeData.forEach(item => {
totalDineIn += Number(item.total_dine_in)
totalTakeout += Number(item.total_takeout)
})
const option = {
tooltip: { trigger: 'item', formatter: '{b}: {c}人 ({d}%)' },
legend: { orient: 'vertical', bottom: 0, left: 'center' },
series: [
{
name: '用餐方式',
type: 'pie',
radius: ['30%', '70%'],
center: ['50%', '40%'],
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
color: ['#67C23A', '#E6A23C'],
data: [
{ name: '堂食人数', value: totalDineIn },
{ name: '打包人数', value: totalTakeout }
]
}
]
}
this.mealTypePieChart.setOption(option)
},
// 绘制餐别人数+次数柱状图
renderMealTypeBarChart() {
const xData = this.mealTypeData.map(item => item.meal_name)
const peopleData = this.mealTypeData.map(item => Number(item.total_people))
const countData = this.mealTypeData.map(item => item.report_count)
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: 0, right: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: xData },
yAxis: { type: 'value' },
series: [
{
name: '总用餐人数',
type: 'bar',
data: peopleData,
itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#409EFF' }, { offset: 1, color: '#66B1FF' }]) }
},
{
name: '报餐次数',
type: 'line',
data: countData,
itemStyle: { color: '#F56C6C' },
lineStyle: { width: 3 },
symbol: 'circle',
symbolSize: 8
}
]
}
this.mealTypeBarChart.setOption(option)
},
// 报餐部门统计
getMealDeptCount(query) {
return getMealDeptCount(query).then(res => {
this.mealDeptData = res.data || []
return res
})
},
// 绘制部门报餐柱状图
renderMealDeptChart() {
const xData = this.mealDeptData.map(item => item.dept_name)
const peopleData = this.mealDeptData.map(item => Number(item.total_people))
const dineInData = this.mealDeptData.map(item => Number(item.total_dine_in))
const takeoutData = this.mealDeptData.map(item => Number(item.total_takeout))
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: 0, right: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: xData },
yAxis: { type: 'value', name: '人数' },
series: [
{ name: '总人数', type: 'bar', data: peopleData, itemStyle: { color: '#409EFF' } },
{ name: '堂食', type: 'bar', data: dineInData, itemStyle: { color: '#67C23A' } },
{ name: '打包', type: 'bar', data: takeoutData, itemStyle: { color: '#E6A23C' } }
]
}
this.mealDeptChart.setOption(option)
},
// 报餐日期统计
getMealDateCount(query) {
return getMealDateCount(query).then(res => {
this.mealDateData = res.data || []
return res
})
},
// 绘制日报餐趋势图
renderMealDateChart() {
const xData = this.mealDateData.map(item => item.report_date)
const peopleData = this.mealDateData.map(item => Number(item.total_people))
const dineInData = this.mealDateData.map(item => Number(item.total_dine_in))
const takeoutData = this.mealDateData.map(item => Number(item.total_takeout))
const option = {
tooltip: { trigger: 'axis' },
legend: { top: 0, right: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: xData },
yAxis: { type: 'value', name: '人数' },
series: [
{ name: '总用餐人数', type: 'line', data: peopleData, itemStyle: { color: '#409EFF' }, lineStyle: { width: 3 } },
{ name: '堂食人数', type: 'line', data: dineInData, itemStyle: { color: '#67C23A' }, lineStyle: { width: 2 } },
{ name: '打包人数', type: 'line', data: takeoutData, itemStyle: { color: '#E6A23C' }, lineStyle: { width: 2 } }
]
}
this.mealDateChart.setOption(option)
}
}
}
</script>
<style scoped>
/* 页面整体样式 */
.app-container {
padding: 20px;
}
.page-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
/* 卡片样式 */
.mb15 {
margin-bottom: 15px;
}
.card-header {
font-size: 16px;
font-weight: bold;
color: #333;
}
/* 图表标题 */
.chart-title {
font-size: 14px;
font-weight: 500;
color: #666;
background-color: #fff;
margin-top: 10px;
text-align: center;
}
/* 图表容器基础样式 */
.chart-container {
width: 100%;
height: 300px;
border: 2px solid #fff;
}
/* 大图表样式 */
.big-chart {
height: 400px;
}
</style>

View File

@@ -129,14 +129,15 @@ export default {
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 06:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 06:00:00`
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
list: [],
queryParams: {
pageNum: 1,
pageSize: 9999,
status: 1,
dataType: 1,
byExportTimeStart: startTime,
byExportTimeEnd: endTime,
selectType: 'product',

View File

@@ -138,8 +138,8 @@ export default {
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 06:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 06:00:00`
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
list: [],
queryParams: {
@@ -148,7 +148,7 @@ export default {
// status: 1,
byCreateTimeStart: startTime,
byCreateTimeEnd: endTime,
selectType: 'product',
selectType: 'raw_material',
enterCoilNo: '',
currentCoilNo: '',
warehouseId: '',

View File

@@ -19,8 +19,11 @@
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="逻辑库位" prop="endTime">
<warehouse-select v-model="queryParams.warehouseId" placeholder="请选择仓库/库区/库位"
style="width: 100%; display: inline-block; width: 200px;" clearable />
<!-- <warehouse-select v-model="queryParams.warehouseId" placeholder="请选择仓库/库区/库位"
style="width: 100%; display: inline-block; width: 200px;" clearable /> -->
<el-select v-model="warehouseIds" collapse-tags multiple placeholder="请选择逻辑库位" style="width: 200px;">
<el-option v-for="item in warehouseOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产品名称" prop="endTime">
<el-input style="width: 200px;" v-model="queryParams.itemName" placeholder="请输入产品名称" clearable
@@ -90,9 +93,6 @@
<script>
import { listCoilWithIds } from "@/api/wms/coil";
import {
listPendingAction,
} from '@/api/wms/pendingAction';
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
@@ -112,7 +112,7 @@ export default {
},
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer'],
data() {
// 工具函数:个位数补零,保证格式统一(比如 9 → 095 → 05
// 工具函数:个位数补零,保证格式统一(比如 9 → 095 → 05
const addZero = (num) => num.toString().padStart(2, '0')
const now = new Date() // 当前本地北京时间
@@ -131,8 +131,8 @@ export default {
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 06:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 06:00:00`
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
list: [],
queryParams: {
@@ -151,6 +151,40 @@ export default {
itemManufacturer: '',
},
loading: false,
warehouseIds: [
'1988150099140866050',
'1988150263284953089',
'1988150545175736322',
'1988150150521090049',
],
warehouseOptions: [
{ label: '酸连轧成品库', value: '1988150099140866050' },
{ label: '镀锌原料库', value: '1988150263284953089' },
{ label: '脱脂原料库', value: '1988150545175736322' },
{ label: '酸连轧纵剪分条原料库', value: '1988150150521090049' },
],
warehouseQueryMap: {
'1988150099140866050': {
selectType: 'product',
createBy: 'suanzhakuguan',
warehouseId: '1988150099140866050'
},
'1988150263284953089': {
selectType: 'raw_material',
createBy: 'suanzhakuguan',
warehouseId: '1988150263284953089'
},
'1988150545175736322': {
selectType: 'raw_material',
createBy: 'suanzhakuguan',
warehouseId: '1988150545175736322'
},
'1988150150521090049': {
selectType: 'raw_material',
createBy: 'suanzhakuguan',
warehouseId: '1988150150521090049'
},
}
}
},
computed: {
@@ -169,46 +203,61 @@ export default {
methods: {
getList() {
this.loading = true
Promise.all([
// 酸连轧成品库
listCoilWithIds({
...this.queryParams,
pageSize: 9999,
pageNum: 1,
dataType: 1,
createBy: 'suanzhakuguan',
warehouseId: '1988150099140866050'
}),
// 镀锌原料库
listCoilWithIds({
...this.queryParams,
pageSize: 9999,
pageNum: 1,
dataType: 1,
createBy: 'suanzhakuguan',
warehouseId: '1988150263284953089'
}),
// 脱脂原料库
listCoilWithIds({
...this.queryParams,
pageSize: 9999,
pageNum: 1,
dataType: 1,
createBy: 'suanzhakuguan',
warehouseId: '1988150545175736322'
}),
// 酸连轧纵剪分条原料库1988150150521090049
listCoilWithIds({
...this.queryParams,
pageSize: 9999,
pageNum: 1,
dataType: 1,
createBy: 'suanzhakuguan',
warehouseId: '1988150150521090049'
}),
]).then(([res1, res2, res3, res4]) => {
console.log(res1, res2, res3, res4)
const list = [...res1.rows, ...res2.rows, ...res3.rows, ...res4.rows]
Promise.all(
this.warehouseIds.map(warehouseId => {
const params = this.warehouseQueryMap[warehouseId]
return listCoilWithIds({
...this.queryParams,
pageSize: 9999,
pageNum: 1,
// dataType: 1,
...params
})
})
// [
// // 酸连轧成品库
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// createBy: 'suanzhakuguan',
// warehouseId: '1988150099140866050'
// }),
// // 镀锌原料库
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// selectType: 'raw_material',
// createBy: 'suanzhakuguan',
// warehouseId: '1988150263284953089'
// }),
// // 脱脂原料库
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// selectType: 'raw_material',
// createBy: 'suanzhakuguan',
// warehouseId: '1988150545175736322'
// }),
// // 酸连轧纵剪分条原料库1988150150521090049
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// selectType: 'raw_material',
// createBy: 'suanzhakuguan',
// warehouseId: '1988150150521090049'
// }),
// ]
).then((resList) => {
console.log(resList)
const list = resList.flatMap(res => res.rows)
// 按照createTime 降序排序
this.list = list.sort(
(a, b) => new Date(b.createTime) - new Date(a.createTime)

View File

@@ -0,0 +1,243 @@
<template>
<div class="app-container" v-loading="loading">
<el-row>
<el-form label-width="80px" inline>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker style="width: 200px;" v-model="queryParams.byCreateTimeStart" 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 style="width: 200px;" v-model="queryParams.byCreateTimeEnd" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择结束时间"></el-date-picker>
</el-form-item>
<el-form-item label="入场钢卷号" prop="endTime">
<el-input style="width: 200px; display: inline-block;" v-model="queryParams.enterCoilNo"
placeholder="请输入入场钢卷号" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="当前钢卷号" prop="endTime">
<el-input style="width: 200px;" v-model="queryParams.currentCoilNo" placeholder="请输入当前钢卷号" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="逻辑库位" prop="endTime">
<!-- <warehouse-select v-model="queryParams.warehouseId" placeholder="请选择仓库/库区/库位"
style="width: 100%; display: inline-block; width: 200px;" clearable /> -->
<el-select v-model="warehouseIds" collapse-tags multiple placeholder="请选择逻辑库位" style="width: 200px;">
<el-option v-for="item in warehouseOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产品名称" prop="endTime">
<el-input style="width: 200px;" v-model="queryParams.itemName" placeholder="请输入产品名称" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="规格" prop="endTime">
<memo-input style="width: 200px;" v-model="queryParams.itemSpecification" storageKey="coilSpec"
placeholder="请选择规格" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材质" prop="endTime">
<muti-select style="width: 200px;" v-model="queryParams.itemMaterial" :options="dict.type.coil_material"
placeholder="请选择材质" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="厂家" prop="endTime">
<muti-select style="width: 200px;" v-model="queryParams.itemManufacturer"
:options="dict.type.coil_manufacturer" placeholder="请选择厂家" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item prop="endTime">
<el-button type="primary" @click="getList">查询</el-button>
<el-button type="primary" @click="exportData">导出</el-button>
</el-form-item>
</el-form>
</el-row>
<el-descriptions title="统计信息" :column="3" border>
<el-descriptions-item label="总钢卷数量">{{ summary.totalCount }}</el-descriptions-item>
<el-descriptions-item label="总重">{{ summary.totalWeight }}t</el-descriptions-item>
<el-descriptions-item label="均重">{{ summary.avgWeight }}t</el-descriptions-item>
</el-descriptions>
<el-descriptions title="明细信息" :column="3" border>
</el-descriptions>
<el-table :data="list" border height="calc(100vh - 320px)">
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.enterCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.currentCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" />
<el-table-column label="逻辑库位" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="产品类型" align="center" width="250">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row.product" />
<RawMaterialInfo v-else-if="scope.row.itemType === 'raw_material'" :material="scope.row.rawMaterial" />
</template>
</el-table-column>
<el-table-column label="重量 (吨)" align="center" prop="netWeight" />
<el-table-column label="长度 (米)" align="center" prop="length" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="出库状态" align="center" prop="status">
<!-- 0在库1已出库 -->
<template slot-scope="scope">
{{ scope.row.status === 0 ? '在库' : '已出库' }}
</template>
</el-table-column>
<el-table-column label="更新人" align="center" prop="updateByName" />
<el-table-column label="更新时间" align="center" prop="updateTime" />
</el-table>
</div>
</template>
<script>
import { listCoilWithIds } from "@/api/wms/coil";
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
import MemoInput from "@/components/MemoInput";
import MutiSelect from "@/components/MutiSelect";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect";
export default {
components: {
ProductInfo,
RawMaterialInfo,
CoilNo,
MemoInput,
MutiSelect,
WarehouseSelect,
},
dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer'],
data() {
// 工具函数:个位数补零,保证格式统一(比如 9 → 095 → 05
const addZero = (num) => num.toString().padStart(2, '0')
const now = new Date() // 当前本地北京时间
// 核心:获取【昨天】的日期对象(自动处理跨月/跨年,无边界问题)
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
// 昨天的年、月、日(补零格式化)
const yesYear = yesterday.getFullYear()
const yesMonth = addZero(yesterday.getMonth() + 1)
const yesDay = addZero(yesterday.getDate())
// 今天的年、月、日(补零格式化)
const nowYear = now.getFullYear()
const nowMonth = addZero(now.getMonth() + 1)
const nowDay = addZero(now.getDate())
// ✅ 目标时间区间昨天早上6点 至 今天早上6点
const startTime = `${yesYear}-${yesMonth}-${yesDay} 07:00:00`
const endTime = `${nowYear}-${nowMonth}-${nowDay} 07:00:00`
return {
list: [],
queryParams: {
pageNum: 1,
pageSize: 9999,
// status: 1,
byCreateTimeStart: startTime,
byCreateTimeEnd: endTime,
selectType: 'product',
enterCoilNo: '',
currentCoilNo: '',
warehouseId: '',
productName: '',
itemSpecification: '',
itemMaterial: '',
itemManufacturer: '',
},
loading: false,
warehouseIds: [
'1988150323162836993',
'1988150487185289217',
],
warehouseOptions: [
{ value: '1988150323162836993', label: '镀锌成品库' },
{ value: '1988150487185289217', label: '镀锌纵剪分条原料库' },
],
warehouseQueryMap: {
'1988150323162836993': {
selectType: 'product',
// createBy: 'suanzhakuguan',
warehouseId: '1988150323162836993'
},
'1988150487185289217': {
selectType: 'raw_material',
// createBy: 'suanzhakuguan',
warehouseId: '1988150487185289217'
},
},
}
},
computed: {
summary() {
// 总钢卷数量、总重、均重
const totalCount = this.list.length
const totalWeight = this.list.reduce((acc, cur) => acc + parseFloat(cur.netWeight), 0)
const avgWeight = totalCount > 0 ? (totalWeight / totalCount).toFixed(2) : 0
return {
totalCount,
totalWeight: totalWeight.toFixed(2),
avgWeight,
}
}
},
methods: {
getList() {
this.loading = true
Promise.all([
...this.warehouseIds.map(warehouseId => listCoilWithIds({
...this.queryParams,
...this.warehouseQueryMap[warehouseId],
pageSize: 9999,
pageNum: 1,
// dataType: 1,
}))
// // 镀锌成品库
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// selectType: 'product',
// // createBy: 'suanzhakuguan',
// warehouseId: '1988150323162836993'
// }),
// // 镀锌纵剪分条原料库
// listCoilWithIds({
// ...this.queryParams,
// pageSize: 9999,
// pageNum: 1,
// // dataType: 1,
// selectType: 'raw_material',
// // createBy: 'suanzhakuguan',
// warehouseId: '1988150487185289217'
// }),
]).then((resList) => {
console.log(resList)
const list = resList.flatMap(res => res.rows)
// 按照createTime 降序排序
this.list = list.sort(
(a, b) => new Date(b.createTime) - new Date(a.createTime)
)
this.loading = false
})
},
// 导出
exportData() {
this.download('wms/materialCoil/export', {
coilIds: this.list.map(item => item.coilId).join(',')
}, `materialCoil_${new Date().getTime()}.xlsx`)
},
},
mounted() {
this.getList()
}
}
</script>
<style scoped></style>

View File

@@ -45,6 +45,14 @@ module.exports = {
['^' + process.env.VUE_APP_BASE_API]: ''
}
},
// 直接代理Zinc1相关路径
'/zinc-api': {
target: `http://140.143.206.120:18082/prod-api`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
},
// 直接代理WebSocket相关路径
'/websocket': {
target: `http://localhost:8080`,

View File

@@ -0,0 +1,136 @@
package com.klp.controller;
import java.util.List;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.klp.common.annotation.RepeatSubmit;
import com.klp.common.annotation.Log;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import com.klp.domain.vo.WmsLeaveRequestVo;
import com.klp.domain.bo.WmsLeaveRequestBo;
import com.klp.service.IWmsLeaveRequestService;
import com.klp.common.core.page.TableDataInfo;
import java.util.List;
import java.util.Map;
/**
* 员工请假申请
*
* @author klp
* @date 2026-01-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wms/leaveRequest")
public class WmsLeaveRequestController extends BaseController {
private final IWmsLeaveRequestService iWmsLeaveRequestService;
/**
* 查询员工请假申请列表
*/
@GetMapping("/list")
public TableDataInfo<WmsLeaveRequestVo> list(WmsLeaveRequestBo bo, PageQuery pageQuery) {
return iWmsLeaveRequestService.queryPageList(bo, pageQuery);
}
/**
* 导出员工请假申请列表
*/
@Log(title = "员工请假申请", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(WmsLeaveRequestBo bo, HttpServletResponse response) {
List<WmsLeaveRequestVo> list = iWmsLeaveRequestService.queryList(bo);
ExcelUtil.exportExcel(list, "员工请假申请", WmsLeaveRequestVo.class, response);
}
/**
* 获取员工请假申请详细信息
*
* @param leaveId 主键
*/
@GetMapping("/{leaveId}")
public R<WmsLeaveRequestVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long leaveId) {
return R.ok(iWmsLeaveRequestService.queryById(leaveId));
}
/**
* 新增员工请假申请
*/
@Log(title = "员工请假申请", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody WmsLeaveRequestBo bo) {
return toAjax(iWmsLeaveRequestService.insertByBo(bo));
}
/**
* 修改员工请假申请
*/
@Log(title = "员工请假申请", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody WmsLeaveRequestBo bo) {
return toAjax(iWmsLeaveRequestService.updateByBo(bo));
}
/**
* 删除员工请假申请
*
* @param leaveIds 主键串
*/
@Log(title = "员工请假申请", businessType = BusinessType.DELETE)
@DeleteMapping("/{leaveIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] leaveIds) {
return toAjax(iWmsLeaveRequestService.deleteWithValidByIds(Arrays.asList(leaveIds), true));
}
/**
* 请假统计报表 - 按请假类型统计
*/
@GetMapping("/report/leaveType")
public R<List<Map<String, Object>>> getLeaveTypeReport(WmsLeaveRequestBo bo) {
return R.ok(iWmsLeaveRequestService.getLeaveTypeReport(bo));
}
/**
* 请假统计报表 - 按部门统计
*/
@GetMapping("/report/dept")
public R<List<Map<String, Object>>> getLeaveDeptReport(WmsLeaveRequestBo bo) {
return R.ok(iWmsLeaveRequestService.getLeaveDeptReport(bo));
}
/**
* 请假统计报表 - 按月份统计
*/
@GetMapping("/report/monthly")
public R<List<Map<String, Object>>> getLeaveMonthlyReport(WmsLeaveRequestBo bo) {
return R.ok(iWmsLeaveRequestService.getLeaveMonthlyReport(bo));
}
/**
* 根据请假人分组获取请假信息
*/
@GetMapping("/grouped")
public R<List<Map<String, Object>>> getLeaveListGroupedByApplicant(WmsLeaveRequestBo bo) {
return R.ok(iWmsLeaveRequestService.getLeaveListGroupedByApplicant(bo));
}
}

View File

@@ -266,9 +266,10 @@ public class WmsMaterialCoilController extends BaseController {
*/
@GetMapping("/checkCoilNoDuplicate")
public R<Map<String, Object>> checkCoilNoDuplicate(
@RequestParam(required = false) Long coilId,
@RequestParam(required = false) String enterCoilNo,
@RequestParam(required = false) String currentCoilNo) {
Map<String, Object> result = iWmsMaterialCoilService.checkCoilNoDuplicate(enterCoilNo, currentCoilNo);
Map<String, Object> result = iWmsMaterialCoilService.checkCoilNoDuplicate(coilId,enterCoilNo, currentCoilNo);
return R.ok(result);
}
@@ -288,5 +289,16 @@ public class WmsMaterialCoilController extends BaseController {
return R.ok(result);
}
/**
* 查询data_type=1的重复钢卷分组
* - 入场钢卷号重复分组
* - 当前钢卷号重复分组
*/
@GetMapping("/duplicateGroups")
public R<Map<String, Object>> getDuplicateCoilGroups() {
Map<String, Object> result = iWmsMaterialCoilService.getDuplicateCoilGroups();
return R.ok(result);
}
}

View File

@@ -0,0 +1,125 @@
package com.klp.controller;
import java.util.List;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import com.klp.common.annotation.RepeatSubmit;
import com.klp.common.annotation.Log;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import com.klp.domain.vo.WmsMealReportVo;
import com.klp.domain.bo.WmsMealReportBo;
import com.klp.service.IWmsMealReportService;
import com.klp.common.core.page.TableDataInfo;
import java.util.List;
import java.util.Map;
/**
* 部门报餐主
*
* @author klp
* @date 2026-01-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wms/mealReport")
public class WmsMealReportController extends BaseController {
private final IWmsMealReportService iWmsMealReportService;
/**
* 查询部门报餐主列表
*/
@GetMapping("/list")
public TableDataInfo<WmsMealReportVo> list(WmsMealReportBo bo, PageQuery pageQuery) {
return iWmsMealReportService.queryPageList(bo, pageQuery);
}
/**
* 导出部门报餐主列表
*/
@Log(title = "部门报餐主", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(WmsMealReportBo bo, HttpServletResponse response) {
List<WmsMealReportVo> list = iWmsMealReportService.queryList(bo);
ExcelUtil.exportExcel(list, "部门报餐主", WmsMealReportVo.class, response);
}
/**
* 获取部门报餐主详细信息
*
* @param reportId 主键
*/
@GetMapping("/{reportId}")
public R<WmsMealReportVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long reportId) {
return R.ok(iWmsMealReportService.queryById(reportId));
}
/**
* 新增部门报餐主
*/
@Log(title = "部门报餐主", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody WmsMealReportBo bo) {
return toAjax(iWmsMealReportService.insertByBo(bo));
}
/**
* 修改部门报餐主
*/
@Log(title = "部门报餐主", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody WmsMealReportBo bo) {
return toAjax(iWmsMealReportService.updateByBo(bo));
}
/**
* 删除部门报餐主
*
* @param reportIds 主键串
*/
@Log(title = "部门报餐主", businessType = BusinessType.DELETE)
@DeleteMapping("/{reportIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] reportIds) {
return toAjax(iWmsMealReportService.deleteWithValidByIds(Arrays.asList(reportIds), true));
}
/**
* 报餐统计报表 - 按餐别统计
*/
@GetMapping("/report/mealType")
public R<List<Map<String, Object>>> getMealTypeReport(WmsMealReportBo bo) {
return R.ok(iWmsMealReportService.getMealTypeReport(bo));
}
/**
* 报餐统计报表 - 按部门统计
*/
@GetMapping("/report/dept")
public R<List<Map<String, Object>>> getMealDeptReport(WmsMealReportBo bo) {
return R.ok(iWmsMealReportService.getMealDeptReport(bo));
}
/**
* 报餐统计报表 - 按日期统计
*/
@GetMapping("/report/date")
public R<List<Map<String, Object>>> getMealDateReport(WmsMealReportBo bo) {
return R.ok(iWmsMealReportService.getMealDateReport(bo));
}
}

View File

@@ -0,0 +1,80 @@
package com.klp.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 员工请假申请对象 wms_leave_request
*
* @author klp
* @date 2026-01-17
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("wms_leave_request")
public class WmsLeaveRequest extends BaseEntity {
private static final long serialVersionUID=1L;
/**
* 主键ID
*/
@TableId(value = "leave_id")
private Long leaveId;
/**
* 请假标题
*/
private String leaveTitle;
/**
* 请假类型(年假/事假/病假/调休/外出/出差等)
*/
private String leaveType;
/**
* 请假人姓名
*/
private String applicantName;
/**
* 请假人部门名称
*/
private String applicantDeptName;
/**
* 请假开始时间
*/
private Date startTime;
/**
* 请假结束时间
*/
private Date endTime;
/**
* 请假班次(早班/中班/晚班/夜班/全天等)
*/
private String leaveShift;
/**
* 请假天数
*/
private BigDecimal leaveDays;
/**
* 请假原因
*/
private String leaveReason;
/**
* 附件(病假证明等)
*/
private String attachmentUrls;
/**
* 逻辑删除标识0=正常1=已删
*/
@TableLogic
private Integer delFlag;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,71 @@
package com.klp.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 部门报餐主对象 wms_meal_report
*
* @author klp
* @date 2026-01-17
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("wms_meal_report")
public class WmsMealReport extends BaseEntity {
private static final long serialVersionUID=1L;
/**
* 主键ID
*/
@TableId(value = "report_id")
private Long reportId;
/**
* 用餐日期
*/
private Date reportDate;
/**
* 餐别1早餐 2午餐 3晚餐 4夜宵
*/
private Long mealType;
/**
* 部门名称
*/
private String deptName;
/**
* 用餐总人数
*/
private Long totalPeople;
/**
* 堂食人数
*/
private Long dineInPeople;
/**
* 打包人数
*/
private Long takeoutPeople;
/**
* 报餐人姓名
*/
private String reportUserName;
/**
* 状态1已提交 2已修改 3已作废
*/
private Long status;
/**
* 逻辑删除标识0=正常1=已删
*/
@TableLogic
private Integer delFlag;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,94 @@
package com.klp.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 员工请假申请业务对象 wms_leave_request
*
* @author klp
* @date 2026-01-17
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WmsLeaveRequestBo extends BaseEntity {
/**
* 主键ID
*/
private Long leaveId;
/**
* 请假标题
*/
private String leaveTitle;
/**
* 请假类型(年假/事假/病假/调休/外出/出差等)
*/
private String leaveType;
/**
* 请假人姓名
*/
private String applicantName;
/**
* 请假人部门名称
*/
private String applicantDeptName;
/**
* 请假开始时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date startTime;
/**
* 请假结束时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date endTime;
/**
* 导出ID列表逗号分隔
*/
private String leaveIds;
/**
* 请假班次(早班/中班/晚班/夜班/全天等)
*/
private String leaveShift;
/**
* 请假天数
*/
private BigDecimal leaveDays;
/**
* 请假原因
*/
private String leaveReason;
/**
* 附件(病假证明等)
*/
private String attachmentUrls;
/**
* 备注
*/
private String remark;
}

View File

@@ -56,6 +56,11 @@ public class WmsMaterialCoilBo extends BaseEntity {
*/
private Long warehouseId;
/**
* 所在库区IDs逗号分隔
*/
private String warehouseIds;
/**
* 下一库区ID
*/

View File

@@ -0,0 +1,76 @@
package com.klp.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.*;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 部门报餐主业务对象 wms_meal_report
*
* @author klp
* @date 2026-01-17
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WmsMealReportBo extends BaseEntity {
/**
* 主键ID
*/
private Long reportId;
/**
* 用餐日期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date reportDate;
/**
* 餐别1早餐 2午餐 3晚餐 4夜宵
*/
private Long mealType;
/**
* 部门名称
*/
private String deptName;
/**
* 用餐总人数
*/
private Long totalPeople;
/**
* 堂食人数
*/
private Long dineInPeople;
/**
* 打包人数
*/
private Long takeoutPeople;
/**
* 报餐人姓名
*/
private String reportUserName;
/**
* 状态1已提交 2已修改 3已作废
*/
private Long status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,97 @@
package com.klp.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.klp.common.annotation.ExcelDictFormat;
import com.klp.common.convert.ExcelDictConvert;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
/**
* 员工请假申请视图对象 wms_leave_request
*
* @author klp
* @date 2026-01-17
*/
@Data
@ExcelIgnoreUnannotated
public class WmsLeaveRequestVo extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long leaveId;
/**
* 请假标题
*/
@ExcelProperty(value = "请假标题")
private String leaveTitle;
/**
* 请假类型(年假/事假/病假/调休/外出/出差等)
*/
@ExcelProperty(value = "请假类型", converter = ExcelDictConvert.class)
private String leaveType;
/**
* 请假人姓名
*/
@ExcelProperty(value = "请假人姓名")
private String applicantName;
/**
* 请假人部门名称
*/
@ExcelProperty(value = "请假人部门名称")
private String applicantDeptName;
/**
* 请假开始时间
*/
@ExcelProperty(value = "请假开始时间")
private Date startTime;
/**
* 请假结束时间
*/
@ExcelProperty(value = "请假结束时间")
private Date endTime;
/**
* 请假班次(早班/中班/晚班/夜班/全天等)
*/
@ExcelProperty(value = "请假班次", converter = ExcelDictConvert.class)
private String leaveShift;
/**
* 请假天数
*/
@ExcelProperty(value = "请假天数")
private BigDecimal leaveDays;
/**
* 请假原因
*/
@ExcelProperty(value = "请假原因")
private String leaveReason;
/**
* 附件(病假证明等)
*/
private String attachmentUrls;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -166,4 +166,7 @@ public class WmsMaterialCoilExportVo {
* 更新时间(仅临时存储,不导出,用于发货时间为空时兜底)
*/
private Date updateTime;
// 数据类型
private Integer dataType;
}

View File

@@ -0,0 +1,90 @@
package com.klp.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.klp.common.annotation.ExcelDictFormat;
import com.klp.common.convert.ExcelDictConvert;
import com.klp.common.core.domain.BaseEntity;
import com.klp.common.utils.StringUtils;
import lombok.Data;
/**
* 部门报餐主视图对象 wms_meal_report
*
* @author klp
* @date 2026-01-17
*/
@Data
@ExcelIgnoreUnannotated
public class WmsMealReportVo extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long reportId;
/**
* 用餐日期
*/
@ExcelProperty(value = "用餐日期")
private Date reportDate;
/**
* 餐别1早餐 2午餐 3晚餐 4夜宵
*/
@ExcelProperty(value = "餐别1早餐 2午餐 3晚餐 4夜宵")
private Long mealType;
/**
* 部门名称
*/
@ExcelProperty(value = "部门名称")
private String deptName;
/**
* 用餐总人数
*/
@ExcelProperty(value = "用餐总人数")
private Long totalPeople;
/**
* 堂食人数
*/
@ExcelProperty(value = "堂食人数")
private Long dineInPeople;
/**
* 打包人数
*/
@ExcelProperty(value = "打包人数")
private Long takeoutPeople;
/**
* 报餐人姓名
*/
@ExcelProperty(value = "报餐人姓名")
private String reportUserName;
/**
* 状态1已提交 2已修改 3已作废
*/
@ExcelProperty(value = "状态1已提交 2已修改 3已作废")
private Long status;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
private String createByName;
private String updateByName;
}

View File

@@ -0,0 +1,15 @@
package com.klp.mapper;
import com.klp.domain.WmsLeaveRequest;
import com.klp.domain.vo.WmsLeaveRequestVo;
import com.klp.common.core.mapper.BaseMapperPlus;
/**
* 员工请假申请Mapper接口
*
* @author klp
* @date 2026-01-17
*/
public interface WmsLeaveRequestMapper extends BaseMapperPlus<WmsLeaveRequestMapper, WmsLeaveRequest, WmsLeaveRequestVo> {
}

View File

@@ -0,0 +1,15 @@
package com.klp.mapper;
import com.klp.domain.WmsMealReport;
import com.klp.domain.vo.WmsMealReportVo;
import com.klp.common.core.mapper.BaseMapperPlus;
/**
* 部门报餐主Mapper接口
*
* @author klp
* @date 2026-01-17
*/
public interface WmsMealReportMapper extends BaseMapperPlus<WmsMealReportMapper, WmsMealReport, WmsMealReportVo> {
}

View File

@@ -0,0 +1,70 @@
package com.klp.service;
import com.klp.domain.WmsLeaveRequest;
import com.klp.domain.vo.WmsLeaveRequestVo;
import com.klp.domain.bo.WmsLeaveRequestBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 员工请假申请Service接口
*
* @author klp
* @date 2026-01-17
*/
public interface IWmsLeaveRequestService {
/**
* 查询员工请假申请
*/
WmsLeaveRequestVo queryById(Long leaveId);
/**
* 查询员工请假申请列表
*/
TableDataInfo<WmsLeaveRequestVo> queryPageList(WmsLeaveRequestBo bo, PageQuery pageQuery);
/**
* 查询员工请假申请列表
*/
List<WmsLeaveRequestVo> queryList(WmsLeaveRequestBo bo);
/**
* 新增员工请假申请
*/
Boolean insertByBo(WmsLeaveRequestBo bo);
/**
* 修改员工请假申请
*/
Boolean updateByBo(WmsLeaveRequestBo bo);
/**
* 校验并批量删除员工请假申请信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 请假统计报表 - 按请假类型统计
*/
List<Map<String, Object>> getLeaveTypeReport(WmsLeaveRequestBo bo);
/**
* 请假统计报表 - 按部门统计
*/
List<Map<String, Object>> getLeaveDeptReport(WmsLeaveRequestBo bo);
/**
* 请假统计报表 - 按月份统计
*/
List<Map<String, Object>> getLeaveMonthlyReport(WmsLeaveRequestBo bo);
/**
* 根据请假人分组获取请假信息
*/
List<Map<String, Object>> getLeaveListGroupedByApplicant(WmsLeaveRequestBo bo);
}

View File

@@ -125,7 +125,7 @@ public interface IWmsMaterialCoilService {
* - enterCoilNoDuplicate: 入场钢卷号是否重复 (true/false)
* - currentCoilNoDuplicate: 当前钢卷号是否重复 (true/false)
*/
Map<String, Object> checkCoilNoDuplicate(String enterCoilNo, String currentCoilNo);
Map<String, Object> checkCoilNoDuplicate(Long coilId, String enterCoilNo, String currentCoilNo);
/**
* 根据入场钢卷号前缀查询最大的入场钢卷号
@@ -137,5 +137,16 @@ public interface IWmsMaterialCoilService {
* - prefix: 前缀值
*/
Map<String, Object> getMaxEnterCoilNoByPrefix(String enterCoilNoPrefix);
/**
* 查询data_type=1时的重复钢卷分组
* 将入场钢卷号重复的分为一组,将当前钢卷号重复的分为一组
* 返回结构:
* {
* enterGroups: [ { enterCoilNo: "xxx", coils: [WmsMaterialCoilVo...] }, ...],
* currentGroups: [ { currentCoilNo: "yyy", coils: [WmsMaterialCoilVo...] }, ...]
* }
*/
Map<String, Object> getDuplicateCoilGroups();
}

View File

@@ -0,0 +1,65 @@
package com.klp.service;
import com.klp.domain.WmsMealReport;
import com.klp.domain.vo.WmsMealReportVo;
import com.klp.domain.bo.WmsMealReportBo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 部门报餐主Service接口
*
* @author klp
* @date 2026-01-17
*/
public interface IWmsMealReportService {
/**
* 查询部门报餐主
*/
WmsMealReportVo queryById(Long reportId);
/**
* 查询部门报餐主列表
*/
TableDataInfo<WmsMealReportVo> queryPageList(WmsMealReportBo bo, PageQuery pageQuery);
/**
* 查询部门报餐主列表
*/
List<WmsMealReportVo> queryList(WmsMealReportBo bo);
/**
* 新增部门报餐主
*/
Boolean insertByBo(WmsMealReportBo bo);
/**
* 修改部门报餐主
*/
Boolean updateByBo(WmsMealReportBo bo);
/**
* 校验并批量删除部门报餐主信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 报餐统计报表 - 按餐别统计
*/
List<Map<String, Object>> getMealTypeReport(WmsMealReportBo bo);
/**
* 报餐统计报表 - 按部门统计
*/
List<Map<String, Object>> getMealDeptReport(WmsMealReportBo bo);
/**
* 报餐统计报表 - 按日期统计
*/
List<Map<String, Object>> getMealDateReport(WmsMealReportBo bo);
}

View File

@@ -0,0 +1,243 @@
package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.klp.domain.bo.WmsLeaveRequestBo;
import com.klp.domain.vo.WmsLeaveRequestVo;
import com.klp.domain.WmsLeaveRequest;
import com.klp.mapper.WmsLeaveRequestMapper;
import com.klp.service.IWmsLeaveRequestService;
import java.util.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
/**
* 员工请假申请Service业务层处理
*
* @author klp
* @date 2026-01-17
*/
@RequiredArgsConstructor
@Service
public class WmsLeaveRequestServiceImpl implements IWmsLeaveRequestService {
private final WmsLeaveRequestMapper baseMapper;
/**
* 查询员工请假申请
*/
@Override
public WmsLeaveRequestVo queryById(Long leaveId){
return baseMapper.selectVoById(leaveId);
}
/**
* 查询员工请假申请列表
*/
@Override
public TableDataInfo<WmsLeaveRequestVo> queryPageList(WmsLeaveRequestBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<WmsLeaveRequest> lqw = buildQueryWrapper(bo);
Page<WmsLeaveRequestVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询员工请假申请列表
*/
@Override
public List<WmsLeaveRequestVo> queryList(WmsLeaveRequestBo bo) {
LambdaQueryWrapper<WmsLeaveRequest> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<WmsLeaveRequest> buildQueryWrapper(WmsLeaveRequestBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<WmsLeaveRequest> lqw = Wrappers.lambdaQuery();
// 如果传入了ids则按ids筛选否则按其他条件筛选
if (StringUtils.isNotBlank(bo.getLeaveIds())) {
String[] idArray = bo.getLeaveIds().split(",");
List<Long> idList = new ArrayList<>();
for (String id : idArray) {
if (StringUtils.isNotBlank(id)) {
try {
idList.add(Long.parseLong(id.trim()));
} catch (NumberFormatException e) {
// 忽略无效的ID
}
}
}
if (!idList.isEmpty()) {
lqw.in(WmsLeaveRequest::getLeaveId, idList);
}
} else {
// 正常的条件筛选
lqw.eq(StringUtils.isNotBlank(bo.getLeaveTitle()), WmsLeaveRequest::getLeaveTitle, bo.getLeaveTitle());
lqw.eq(StringUtils.isNotBlank(bo.getLeaveType()), WmsLeaveRequest::getLeaveType, bo.getLeaveType());
lqw.like(StringUtils.isNotBlank(bo.getApplicantName()), WmsLeaveRequest::getApplicantName, bo.getApplicantName());
lqw.like(StringUtils.isNotBlank(bo.getApplicantDeptName()), WmsLeaveRequest::getApplicantDeptName, bo.getApplicantDeptName());
// 请假时间范围筛选:筛选出请假时间与查询时间范围有交集的记录
// 条件:(start_time <= endTime AND end_time >= startTime)
if (bo.getStartTime() != null && bo.getEndTime() != null) {
lqw.le(WmsLeaveRequest::getStartTime, bo.getEndTime())
.ge(WmsLeaveRequest::getEndTime, bo.getStartTime());
} else if (bo.getStartTime() != null) {
lqw.ge(WmsLeaveRequest::getEndTime, bo.getStartTime());
} else if (bo.getEndTime() != null) {
lqw.le(WmsLeaveRequest::getStartTime, bo.getEndTime());
}
lqw.eq(bo.getStartTime() != null, WmsLeaveRequest::getStartTime, bo.getStartTime());
lqw.eq(bo.getEndTime() != null, WmsLeaveRequest::getEndTime, bo.getEndTime());
lqw.eq(StringUtils.isNotBlank(bo.getLeaveShift()), WmsLeaveRequest::getLeaveShift, bo.getLeaveShift());
lqw.eq(bo.getLeaveDays() != null, WmsLeaveRequest::getLeaveDays, bo.getLeaveDays());
lqw.eq(StringUtils.isNotBlank(bo.getLeaveReason()), WmsLeaveRequest::getLeaveReason, bo.getLeaveReason());
lqw.eq(StringUtils.isNotBlank(bo.getAttachmentUrls()), WmsLeaveRequest::getAttachmentUrls, bo.getAttachmentUrls());
}
// 按请假开始时间倒序排列
lqw.orderByDesc(WmsLeaveRequest::getStartTime);
return lqw;
}
/**
* 新增员工请假申请
*/
@Override
public Boolean insertByBo(WmsLeaveRequestBo bo) {
WmsLeaveRequest add = BeanUtil.toBean(bo, WmsLeaveRequest.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setLeaveId(add.getLeaveId());
}
return flag;
}
/**
* 修改员工请假申请
*/
@Override
public Boolean updateByBo(WmsLeaveRequestBo bo) {
WmsLeaveRequest update = BeanUtil.toBean(bo, WmsLeaveRequest.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(WmsLeaveRequest entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除员工请假申请
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 请假统计报表 - 按请假类型统计
*/
@Override
public List<Map<String, Object>> getLeaveTypeReport(WmsLeaveRequestBo bo) {
QueryWrapper<WmsLeaveRequest> qw = new QueryWrapper<>();
qw.select("leave_type as type",
"COUNT(*) as count",
"SUM(leave_days) as total_days")
.eq(bo.getApplicantDeptName() != null, "applicant_dept_name", bo.getApplicantDeptName())
.ge(bo.getStartTime() != null, "start_time", bo.getStartTime())
.le(bo.getEndTime() != null, "end_time", bo.getEndTime())
.eq("del_flag", 0)
.groupBy("leave_type")
.orderByDesc("total_days");
return baseMapper.selectMaps(qw);
}
/**
* 请假统计报表 - 按部门统计
*/
@Override
public List<Map<String, Object>> getLeaveDeptReport(WmsLeaveRequestBo bo) {
QueryWrapper<WmsLeaveRequest> qw = new QueryWrapper<>();
qw.select("applicant_dept_name as dept_name",
"COUNT(*) as count",
"SUM(leave_days) as total_days")
.isNotNull("applicant_dept_name")
.eq(StringUtils.isNotBlank(bo.getLeaveType()), "leave_type", bo.getLeaveType())
.ge(bo.getStartTime() != null, "start_time", bo.getStartTime())
.le(bo.getEndTime() != null, "end_time", bo.getEndTime())
.eq("del_flag", 0)
.groupBy("applicant_dept_name")
.orderByDesc("total_days");
return baseMapper.selectMaps(qw);
}
/**
* 请假统计报表 - 按月份统计
*/
@Override
public List<Map<String, Object>> getLeaveMonthlyReport(WmsLeaveRequestBo bo) {
QueryWrapper<WmsLeaveRequest> qw = new QueryWrapper<>();
qw.select("DATE_FORMAT(start_time, '%Y-%m') as month",
"COUNT(*) as count",
"SUM(leave_days) as total_days")
.ge(bo.getStartTime() != null, "start_time", bo.getStartTime())
.le(bo.getEndTime() != null, "end_time", bo.getEndTime())
.eq(StringUtils.isNotBlank(bo.getLeaveType()), "leave_type", bo.getLeaveType())
.eq(bo.getApplicantDeptName() != null, "applicant_dept_name", bo.getApplicantDeptName())
.eq("del_flag", 0)
.groupBy("month")
.orderByAsc("month");
return baseMapper.selectMaps(qw);
}
/**
* 根据请假人分组获取请假信息
*/
@Override
public List<Map<String, Object>> getLeaveListGroupedByApplicant(WmsLeaveRequestBo bo) {
QueryWrapper<WmsLeaveRequest> qw = new QueryWrapper<>();
qw.select("applicant_name",
"applicant_dept_name",
"GROUP_CONCAT(CONCAT(leave_type, ':', DATE_FORMAT(start_time, '%Y-%m-%d %H:%i'), '~', DATE_FORMAT(end_time, '%Y-%m-%d %H:%i'), '(', leave_days, '天)') ORDER BY start_time SEPARATOR '; ') as leave_details",
"SUM(leave_days) as total_days",
"COUNT(*) as leave_count")
.eq("del_flag", 0);
// 时间范围筛选
if (bo.getStartTime() != null && bo.getEndTime() != null) {
qw.le("start_time", bo.getEndTime())
.ge("end_time", bo.getStartTime());
} else if (bo.getStartTime() != null) {
qw.ge("end_time", bo.getStartTime());
} else if (bo.getEndTime() != null) {
qw.le("start_time", bo.getEndTime());
}
// 其他筛选条件
qw.eq(StringUtils.isNotBlank(bo.getLeaveType()), "leave_type", bo.getLeaveType())
.like(StringUtils.isNotBlank(bo.getApplicantName()), "applicant_name", bo.getApplicantName())
.like(StringUtils.isNotBlank(bo.getApplicantDeptName()), "applicant_dept_name", bo.getApplicantDeptName())
.groupBy("applicant_name", "applicant_dept_name")
.orderByDesc("total_days");
return baseMapper.selectMaps(qw);
}
}

View File

@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.exception.ServiceException;
import com.klp.common.helper.LoginHelper;
import com.klp.common.utils.StringUtils;
import com.klp.common.utils.spring.SpringUtils;
@@ -341,12 +342,31 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
qw.like(StringUtils.isNotBlank(bo.getSupplierCoilNo()), "mc.supplier_coil_no", bo.getSupplierCoilNo());
qw.eq(bo.getDataType() != null, "mc.data_type", bo.getDataType());
qw.eq(bo.getMaterialType() != null, "mc.material_type", bo.getMaterialType());
qw.eq(bo.getWarehouseId() != null, "mc.warehouse_id", bo.getWarehouseId());
qw.eq(bo.getHasMergeSplit() != null, "mc.has_merge_split", bo.getHasMergeSplit());
qw.eq(bo.getStatus() != null, "mc.status", bo.getStatus());
qw.eq(StringUtils.isNotBlank(bo.getItemType()), "mc.item_type", bo.getItemType());
qw.eq(StringUtils.isNotBlank(bo.getCreateBy()), "mc.create_by", bo.getCreateBy());
qw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), "mc.update_by", bo.getUpdateBy());
// 统一处理 warehouseId 与 warehouseIds
List<Long> warehouseIdList = new ArrayList<>();
if (bo.getWarehouseId() != null) {
warehouseIdList.add(bo.getWarehouseId());
}
if (StringUtils.isNotBlank(bo.getWarehouseIds())) {
String[] warehouseIdArray = bo.getWarehouseIds().split(",");
for (String warehouseIdStr : warehouseIdArray) {
if (StringUtils.isNotBlank(warehouseIdStr)) {
try {
warehouseIdList.add(Long.parseLong(warehouseIdStr.trim()));
} catch (NumberFormatException ignore) {
}
}
}
}
if (!warehouseIdList.isEmpty()) {
qw.in("mc.warehouse_id", warehouseIdList.stream().distinct().collect(Collectors.toList()));
}
// 新增长度
qw.eq(bo.getLength() != null, "mc.length", bo.getLength());
// 如果actualWarehouseId不为空则根据实际库区ID进行查询 如果为-1,则查询无库区的数据
@@ -412,9 +432,9 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
}
// 使用 EXISTS 针对 selectType 的细粒度筛选(使用参数占位符防注入)
if (hasSelectType) {
qw.eq("mc.item_type", bo.getSelectType());
if (hasAnyItemFilter) {
if (hasSelectType && hasAnyItemFilter) {
// 执行筛选逻辑(和上面完全一样)
qw.eq("mc.item_type", bo.getSelectType());
StringBuilder existsSql = new StringBuilder();
List<Object> existsArgs = new ArrayList<>();
if ("product".equals(bo.getSelectType())) {
@@ -454,7 +474,6 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
qw.apply(existsSql.toString(), existsArgs.toArray());
}
}
}
// 显式 itemId 条件:与 EXISTS 共存时,语义为交集
if (CollectionUtils.isNotEmpty(explicitItemIds)) {
@@ -540,6 +559,72 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
return qw;
}
@Override
public Map<String, Object> getDuplicateCoilGroups() {
LambdaQueryWrapper<WmsMaterialCoil> lqw = Wrappers.lambdaQuery();
lqw.eq(WmsMaterialCoil::getDataType, 1);
lqw.eq(WmsMaterialCoil::getDelFlag, 0);
List<WmsMaterialCoil> all = baseMapper.selectList(lqw);
Map<String, List<WmsMaterialCoil>> enterGrouped = all.stream()
.filter(e -> StringUtils.isNotBlank(e.getEnterCoilNo()))
.collect(Collectors.groupingBy(WmsMaterialCoil::getEnterCoilNo));
Map<String, List<WmsMaterialCoil>> currentGrouped = all.stream()
.filter(e -> StringUtils.isNotBlank(e.getCurrentCoilNo()))
.collect(Collectors.groupingBy(WmsMaterialCoil::getCurrentCoilNo));
List<Map<String, Object>> enterGroups = enterGrouped.entrySet().stream()
.filter(en -> en.getValue() != null && en.getValue().size() > 1)
.map(en -> {
List<WmsMaterialCoilVo> vos = en.getValue().stream().map(this::toVoBasic).collect(Collectors.toList());
Map<String, Object> m = new HashMap<>();
m.put("enterCoilNo", en.getKey());
m.put("coils", vos);
return m;
})
.collect(Collectors.toList());
List<Map<String, Object>> currentGroups = currentGrouped.entrySet().stream()
.filter(en -> en.getValue() != null && en.getValue().size() > 1)
.map(en -> {
List<WmsMaterialCoilVo> vos = en.getValue().stream().map(this::toVoBasic).collect(Collectors.toList());
Map<String, Object> m = new HashMap<>();
m.put("currentCoilNo", en.getKey());
m.put("coils", vos);
return m;
})
.collect(Collectors.toList());
// 可选:批量填充关联对象信息
List<WmsMaterialCoilVo> allVos = new ArrayList<>();
for (Map<String, Object> g : enterGroups) {
Object list = g.get("coils");
if (list instanceof List) {
allVos.addAll((List<WmsMaterialCoilVo>) list);
}
}
for (Map<String, Object> g : currentGroups) {
Object list = g.get("coils");
if (list instanceof List) {
allVos.addAll((List<WmsMaterialCoilVo>) list);
}
}
if (!allVos.isEmpty()) {
fillRelatedObjectsBatch(allVos);
}
Map<String, Object> result = new HashMap<>();
result.put("enterGroups", enterGroups);
result.put("currentGroups", currentGroups);
return result;
}
private WmsMaterialCoilVo toVoBasic(WmsMaterialCoil e) {
WmsMaterialCoilVo vo = new WmsMaterialCoilVo();
BeanUtils.copyProperties(e, vo);
return vo;
}
/**
* 构建 OR 连接的 LIKE 子句,使用 MyBatis-Plus apply 的 {index} 占位符并将参数加入 args。
* 例如column = "p.product_name", values = "A,B" -> 返回 "(p.product_name LIKE {0} OR p.product_name LIKE {1})"
@@ -744,6 +829,10 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
// 2. 查找或创建stock
findOrCreateStock(bo);
// 处理实际库区:-1 表示空置库,统一转 NULL
if (bo.getActualWarehouseId() != null && bo.getActualWarehouseId() == -1) {
bo.setActualWarehouseId(null);
}
// 3. 插入钢卷数据
WmsMaterialCoil add = BeanUtil.toBean(bo, WmsMaterialCoil.class);
if(bo.getDataType() != null && bo.getDataType() == 10){
@@ -948,6 +1037,11 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
throw new RuntimeException("原钢卷不存在");
}
// oldCoil 如果是历史卷也就是date_type=0 的时候就是历史钢卷
if (oldCoil.getDataType() == 0) {
throw new RuntimeException("原钢卷已被更新");
}
// 若修改实际库位,先进行校验
if (bo.getActualWarehouseId() != null) {
Long ignoreOccupiedId = Objects.equals(bo.getActualWarehouseId(), oldCoil.getActualWarehouseId())
@@ -2116,6 +2210,11 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
vo.setCreateTime(finalDate);
}
// 非1的情况保持原有create_time不变
// 如果是dataType=0的历史数据将实际库区设置为null
if (vo.getDataType() != null && vo.getDataType() == 0) {
vo.setActualWarehouseName(null);
}
});
return wmsMaterialCoilExportVos;
}
@@ -2131,6 +2230,10 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
WmsMaterialCoilVo wmsMaterialCoilVo = queryById(coilId);
Long oldActualWarehouseId = wmsMaterialCoilVo != null ? wmsMaterialCoilVo.getActualWarehouseId() : null;
// 如果当前钢卷为历史数据应该抛异常
if (wmsMaterialCoilVo != null && wmsMaterialCoilVo.getDataType() == 0) {
throw new RuntimeException("当前数据为历史数据,请勿发货!");
}
// 1. 更新钢卷为已发货并记录发货时间同时清空实际库区占用改用Wrapper实现
int rows = 0;
//获取当前调用接口的这个人的username
@@ -2503,11 +2606,25 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
/**
* 检查钢卷号是否重复
* 根据入场钢卷号和当前钢卷号查询数据库,判断哪个钢卷号重复
* 新增逻辑:修改历史记录时不检查重复
*/
@Override
public Map<String, Object> checkCoilNoDuplicate(String enterCoilNo, String currentCoilNo) {
public Map<String, Object> checkCoilNoDuplicate(Long coilId, String enterCoilNo, String currentCoilNo) {
Map<String, Object> result = new HashMap<>();
// 新增核心逻辑:先判断是否操作的是历史记录
// 1. 如果coilId不为空修改操作先查询该钢卷的dataType
if (coilId != null) {
WmsMaterialCoil coil = baseMapper.selectById(coilId);
// 2. 如果查询到钢卷且dataType!=1说明是历史记录直接返回无重复
if (coil != null && coil.getDataType() != 1) {
result.put("duplicateType", "none");
result.put("enterCoilNoDuplicate", false);
result.put("currentCoilNoDuplicate", false);
return result; // 直接返回,不执行后续检查
}
}
boolean enterCoilNoDuplicate = false;
boolean currentCoilNoDuplicate = false;
@@ -2515,7 +2632,12 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
if (StringUtils.isNotBlank(enterCoilNo)) {
LambdaQueryWrapper<WmsMaterialCoil> enterWrapper = Wrappers.lambdaQuery();
enterWrapper.eq(WmsMaterialCoil::getEnterCoilNo, enterCoilNo)
.eq(WmsMaterialCoil::getDelFlag, 0);
.eq(WmsMaterialCoil::getDelFlag, 0)
.eq(WmsMaterialCoil::getDataType, 1); // 过滤历史数据
// 如果是修改操作,排除自身
if (coilId != null) {
enterWrapper.ne(WmsMaterialCoil::getCoilId, coilId);
}
long enterCount = baseMapper.selectCount(enterWrapper);
enterCoilNoDuplicate = enterCount > 0;
}
@@ -2524,7 +2646,13 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
if (StringUtils.isNotBlank(currentCoilNo)) {
LambdaQueryWrapper<WmsMaterialCoil> currentWrapper = Wrappers.lambdaQuery();
currentWrapper.eq(WmsMaterialCoil::getCurrentCoilNo, currentCoilNo)
.eq(WmsMaterialCoil::getDelFlag, 0);
.eq(WmsMaterialCoil::getDelFlag, 0)
.eq(WmsMaterialCoil::getDataType, 1);
// 如果是修改操作,排除自身
if (coilId != null) {
currentWrapper.ne(WmsMaterialCoil::getCoilId, coilId);
}
long currentCount = baseMapper.selectCount(currentWrapper);
currentCoilNoDuplicate = currentCount > 0;
}

View File

@@ -0,0 +1,232 @@
package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import com.klp.domain.vo.WmsMaterialCoilVo;
import com.klp.system.service.impl.SysUserServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.klp.domain.bo.WmsMealReportBo;
import com.klp.domain.vo.WmsMealReportVo;
import com.klp.domain.WmsMealReport;
import com.klp.mapper.WmsMealReportMapper;
import com.klp.service.IWmsMealReportService;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
/**
* 部门报餐主Service业务层处理
*
* @author klp
* @date 2026-01-17
*/
@RequiredArgsConstructor
@Service
public class WmsMealReportServiceImpl implements IWmsMealReportService {
private final WmsMealReportMapper baseMapper;
private final SysUserServiceImpl userService;
/**
* 查询部门报餐主
*/
@Override
public WmsMealReportVo queryById(Long reportId){
return baseMapper.selectVoById(reportId);
}
/**
* 查询部门报餐主列表
*/
@Override
public TableDataInfo<WmsMealReportVo> queryPageList(WmsMealReportBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<WmsMealReport> lqw = buildQueryWrapper(bo);
Page<WmsMealReportVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
List<WmsMealReportVo> records = result.getRecords();
if (records == null || records.isEmpty()) {
return TableDataInfo.build(result);
}
Set<String> userNames = records.stream()
.flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getUpdateBy()))
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
Map<String, String> nickMap = Collections.emptyMap();
if (!userNames.isEmpty()) {
nickMap = userService.selectNickNameMapByUserNames(new ArrayList<>(userNames));
}
// 单次遍历:填充创建/更新/发货人昵称,并构建物料/产品对象
for (WmsMealReportVo vo : records) {
if (!nickMap.isEmpty()) {
if (StringUtils.isNotBlank(vo.getCreateBy())) {
vo.setCreateByName(nickMap.getOrDefault(vo.getCreateBy(), vo.getCreateBy()));
}
if (StringUtils.isNotBlank(vo.getUpdateBy())) {
vo.setUpdateByName(nickMap.getOrDefault(vo.getUpdateBy(), vo.getUpdateBy()));
}
}
}
return TableDataInfo.build(result);
}
/**
* 查询部门报餐主列表
*/
@Override
public List<WmsMealReportVo> queryList(WmsMealReportBo bo) {
LambdaQueryWrapper<WmsMealReport> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<WmsMealReport> buildQueryWrapper(WmsMealReportBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<WmsMealReport> lqw = Wrappers.lambdaQuery();
// 修改日期查询逻辑查询当天的所有记录从当天00:00:00到23:59:59
if (bo.getReportDate() != null) {
LocalDateTime startDate = bo.getReportDate().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
.with(LocalTime.MIN); // 当天的开始时间 00:00:00
LocalDateTime endDate = bo.getReportDate().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
.with(LocalTime.MAX); // 当天的结束时间 23:59:59
lqw.between(WmsMealReport::getReportDate, Date.from(startDate.atZone(ZoneId.systemDefault()).toInstant()),
Date.from(endDate.atZone(ZoneId.systemDefault()).toInstant()));
}
lqw.eq(bo.getMealType() != null, WmsMealReport::getMealType, bo.getMealType());
lqw.like(StringUtils.isNotBlank(bo.getDeptName()), WmsMealReport::getDeptName, bo.getDeptName());
lqw.eq(bo.getTotalPeople() != null, WmsMealReport::getTotalPeople, bo.getTotalPeople());
lqw.eq(bo.getDineInPeople() != null, WmsMealReport::getDineInPeople, bo.getDineInPeople());
lqw.eq(bo.getTakeoutPeople() != null, WmsMealReport::getTakeoutPeople, bo.getTakeoutPeople());
lqw.like(StringUtils.isNotBlank(bo.getReportUserName()), WmsMealReport::getReportUserName, bo.getReportUserName());
lqw.eq(bo.getStatus() != null, WmsMealReport::getStatus, bo.getStatus());
return lqw;
}
/**
* 新增部门报餐主
*/
@Override
public Boolean insertByBo(WmsMealReportBo bo) {
WmsMealReport add = BeanUtil.toBean(bo, WmsMealReport.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setReportId(add.getReportId());
}
return flag;
}
/**
* 修改部门报餐主
*/
@Override
public Boolean updateByBo(WmsMealReportBo bo) {
WmsMealReport update = BeanUtil.toBean(bo, WmsMealReport.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(WmsMealReport entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除部门报餐主
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 报餐统计报表 - 按餐别统计
*/
@Override
public List<Map<String, Object>> getMealTypeReport(WmsMealReportBo bo) {
QueryWrapper<WmsMealReport> qw = new QueryWrapper<>();
qw.select("CASE meal_type " +
"WHEN 1 THEN '早餐' " +
"WHEN 2 THEN '午餐' " +
"WHEN 3 THEN '晚餐' " +
"WHEN 4 THEN '夜宵' " +
"ELSE '未知' END as meal_name",
"SUM(total_people) as total_people",
"SUM(dine_in_people) as total_dine_in",
"SUM(takeout_people) as total_takeout",
"COUNT(*) as report_count")
.eq(StringUtils.isNotBlank(bo.getDeptName()), "dept_name", bo.getDeptName())
.ge(bo.getReportDate() != null, "report_date", bo.getReportDate())
.le(bo.getReportDate() != null, "report_date", bo.getReportDate())
.eq("del_flag", 0)
.eq(bo.getStatus() != null, "status", bo.getStatus())
.groupBy("meal_type")
.orderByAsc("meal_type");
return baseMapper.selectMaps(qw);
}
/**
* 报餐统计报表 - 按部门统计
*/
@Override
public List<Map<String, Object>> getMealDeptReport(WmsMealReportBo bo) {
QueryWrapper<WmsMealReport> qw = new QueryWrapper<>();
qw.select("dept_name",
"SUM(total_people) as total_people",
"SUM(dine_in_people) as total_dine_in",
"SUM(takeout_people) as total_takeout",
"COUNT(*) as report_count",
"AVG(total_people) as avg_people")
.eq(bo.getMealType() != null, "meal_type", bo.getMealType())
.ge(bo.getReportDate() != null, "report_date", bo.getReportDate())
.le(bo.getReportDate() != null, "report_date", bo.getReportDate())
.eq("del_flag", 0)
.eq(bo.getStatus() != null, "status", bo.getStatus())
.groupBy("dept_name")
.orderByDesc("total_people");
return baseMapper.selectMaps(qw);
}
/**
* 报餐统计报表 - 按日期统计
*/
@Override
public List<Map<String, Object>> getMealDateReport(WmsMealReportBo bo) {
QueryWrapper<WmsMealReport> qw = new QueryWrapper<>();
qw.select("report_date",
"SUM(total_people) as total_people",
"SUM(dine_in_people) as total_dine_in",
"SUM(takeout_people) as total_takeout",
"COUNT(*) as report_count")
.eq(StringUtils.isNotBlank(bo.getDeptName()), "dept_name", bo.getDeptName())
.eq(bo.getMealType() != null, "meal_type", bo.getMealType())
.ge(bo.getReportDate() != null, "report_date", bo.getReportDate())
.le(bo.getReportDate() != null, "report_date", bo.getReportDate())
.eq("del_flag", 0)
.eq(bo.getStatus() != null, "status", bo.getStatus())
.groupBy("report_date")
.orderByAsc("report_date");
return baseMapper.selectMaps(qw);
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.mapper.WmsLeaveRequestMapper">
<resultMap type="com.klp.domain.WmsLeaveRequest" id="WmsLeaveRequestResult">
<result property="leaveId" column="leave_id"/>
<result property="leaveTitle" column="leave_title"/>
<result property="leaveType" column="leave_type"/>
<result property="applicantName" column="applicant_name"/>
<result property="applicantDeptName" column="applicant_dept_name"/>
<result property="startTime" column="start_time"/>
<result property="endTime" column="end_time"/>
<result property="leaveShift" column="leave_shift"/>
<result property="leaveDays" column="leave_days"/>
<result property="leaveReason" column="leave_reason"/>
<result property="attachmentUrls" column="attachment_urls"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="remark" column="remark"/>
</resultMap>
</mapper>

View File

@@ -350,6 +350,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
END AS itemTypeDesc,
-- 物品ID
mc.item_id AS itemId,
-- 数据类型
mc.data_type AS dataType,
-- 逻辑库区
w.warehouse_name AS warehouseName,
-- 实际库区

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.mapper.WmsMealReportMapper">
<resultMap type="com.klp.domain.WmsMealReport" id="WmsMealReportResult">
<result property="reportId" column="report_id"/>
<result property="reportDate" column="report_date"/>
<result property="mealType" column="meal_type"/>
<result property="deptName" column="dept_name"/>
<result property="totalPeople" column="total_people"/>
<result property="dineInPeople" column="dine_in_people"/>
<result property="takeoutPeople" column="takeout_people"/>
<result property="reportUserName" column="report_user_name"/>
<result property="status" column="status"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="remark" column="remark"/>
</resultMap>
</mapper>

12
pom.xml
View File

@@ -384,11 +384,11 @@
<artifactId>klp-erp</artifactId>
<version>${klp-flowable-plus.version}</version>
</dependency>
<dependency>
<groupId>com.klp</groupId>
<artifactId>klp-hrm</artifactId>
<version>${klp-flowable-plus.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.klp</groupId>-->
<!-- <artifactId>klp-hrm</artifactId>-->
<!-- <version>${klp-flowable-plus.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.klp</groupId>
<artifactId>klp-crm</artifactId>
@@ -416,7 +416,7 @@
<module>klp-ems</module>
<module>klp-pocket</module>
<module>klp-erp</module>
<module>klp-hrm</module>
<!-- <module>klp-hrm</module>-->
<module>klp-crm</module>
</modules>
<packaging>pom</packaging>