<template>
|
<view class="page">
|
<!-- 刀具选择区 -->
|
<view class="top-section-grid">
|
<view class="form-cell">
|
<label class="form-label">选择刀具编号:</label>
|
<select v-model="selectedToolNo" class="form-select">
|
<option v-for="tool in toolList" :key="tool.no" :value="tool.no">{{ tool.no }} | {{ tool.name }}</option>
|
</select>
|
<button class="btn-blue" @click="showToolDialog = true">刀具目录</button>
|
</view>
|
<view class="form-cell">
|
<label class="form-label">设置使用上限:</label>
|
<input class="input" type="number" v-model="useLimitInput" placeholder="每次换刀后手填" :disabled="!selectedToolNo" />
|
<button class="btn-blue" @click="setUseLimit" :disabled="!selectedToolNo || !useLimitInput">保存上限</button>
|
</view>
|
<view class="form-cell">
|
<label class="form-label">刀具名称:</label>
|
<input class="input" v-model="toolName" placeholder="刀具带出" disabled />
|
<label class="form-label" style="margin-left: 16px;">规格型号:</label>
|
<input class="input" v-model="toolModel" placeholder="刀具带出" disabled />
|
</view>
|
</view>
|
|
<!-- 操作按钮 -->
|
<view class="button-row">
|
<button class="save-btn" @click="handleUpTool">上刀提交</button>
|
<button class="save-btn" @click="handleDownTool">下刀提交</button>
|
<button class="cancel-btn" @click="cancel">取消</button>
|
</view>
|
|
<!-- 刀具目录弹窗 -->
|
<view v-if="showToolDialog" class="dialog-overlay">
|
<view class="dialog">
|
<view class="form-group">
|
<input v-model="searchKey" placeholder="输入刀具编码、名称模糊搜索" class="input" />
|
<button class="btn-blue" @click="searchTool">搜索</button>
|
</view>
|
<view class="tool-list">
|
<button v-for="tool in filteredTools" :key="tool.no" class="tool-btn" @click="selectTool(tool)">
|
{{ tool.no }} | {{ tool.name }}
|
</button>
|
</view>
|
<view class="dialog-actions">
|
<div style="display: flex; align-items: center;">
|
<button class="btn-blue" @click="prevPage" :disabled="pageIndex === 1">上一页</button>
|
<span style="margin: 0 12px;">第{{ pageIndex }}页 / 共{{ totalPages }}页</span>
|
<button class="btn-blue" @click="nextPage" :disabled="pageIndex === totalPages">下一页</button>
|
</div>
|
<div>
|
<button class="btn-blue" @click="confirmTool">确定</button>
|
<button class="btn-disabled" @click="showToolDialog = false">取消</button>
|
</div>
|
</view>
|
</view>
|
</view>
|
|
<!-- 刀具使用记录表格 -->
|
<view class="table-section">
|
<table>
|
<thead>
|
<tr>
|
<th>刀具编号</th>
|
<th>刀具名称</th>
|
<th>上刀时间</th>
|
<th>上刀计数</th>
|
<th>下刀时间</th>
|
<th>下刀计数</th>
|
<th>使用次数</th>
|
<th>使用上限</th>
|
<th>寿命比%</th>
|
<th>寿命比预警值</th>
|
<th>预警状态</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="item in toolRecords" :key="item.id">
|
<td>{{ item.no }}</td>
|
<td>{{ item.name }}</td>
|
<td>{{ item.upTime }}</td>
|
<td>{{ item.upCount }}</td>
|
<td>{{ item.downTime }}</td>
|
<td>{{ item.downCount }}</td>
|
<td>{{ item.useCount }}</td>
|
<td>{{ item.useLimit }}</td>
|
<td>{{ item.lifePercent }}</td>
|
<td>{{ item.lifeWarn }}</td>
|
<td :class="item.warnStatus === '警告' ? 'warn' : ''">{{ item.warnStatus }}</td>
|
</tr>
|
</tbody>
|
</table>
|
</view>
|
|
<!-- 说明 -->
|
<view class="tool-desc">
|
<p style="color:red;">当前工单中,换了几次刀,就会产生几条数据。上刀时间、下刀时间在表中能看到。</p>
|
<p style="color:red;">上刀时间和对应时间用生产计数器匹配,查出当时的生产数(累计计数)。</p>
|
<p style="color:red;">寿命比预警值在刀具上,默认统一。</p>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
export default {
|
data() {
|
return {
|
machineNo: '',//机台编码
|
workOrderNo: '',//工单号
|
pageIndex: 1,
|
pageSize: 18,
|
total: 0,
|
toolList: [],
|
selectedToolNo: '',
|
toolName: '',
|
toolModel: '',
|
showToolDialog: false,
|
searchKey: '',
|
filteredTools: [],
|
useLimitInput: '',
|
toolRecords: []
|
};
|
},
|
computed: {
|
totalPages() {
|
return Math.ceil(this.total / this.pageSize) || 1;
|
}
|
},
|
methods: {
|
async fetchTools(searchKey) {
|
try {
|
const res = await this.$post({
|
url: '/MesCutterLedger/QueryTools',
|
data: JSON.stringify({
|
searchKey,
|
pageIndex: this.pageIndex,
|
pageSize: this.pageSize
|
}),
|
headers: { 'Content-Type': 'application/json' }
|
});
|
|
if (res.status === 0) {
|
// 兼容不同返回结构:直接数组 / { tbBillList, total } / { data: [...] }
|
const payload = Array.isArray(res.data) ? res.data
|
: (res.data && res.data.tbBillList) ? res.data.tbBillList
|
: (res.data && res.data.data) ? res.data.data
|
: [];
|
|
const getField = (obj, ...keys) => {
|
for (const k of keys) if (obj?.[k] !== undefined && obj?.[k] !== null) return obj[k];
|
return null;
|
};
|
|
const mapped = (payload || []).map(t => ({
|
no: getField(t, 'cutterId', 'CUTTER_ID', 'cutteR_ID', 'daA001', 'no'),
|
name: getField(t, 'cutterName', 'CUTTER_NAME', 'cutteR_NAME', 'name'),
|
model: getField(t, 'cutterModel', 'CUTTER_MODEL', 'cutteR_MODEL', 'model')
|
}));
|
|
// 填充弹窗列表和下拉列表(模板中下拉使用 toolList)
|
this.filteredTools = mapped;
|
this.toolList = mapped.slice();
|
|
// 处理 total:当后端 totalCount 为 0 时,回退到嵌套 total 或映射数组长度
|
const totalFromRes = Number(res.totalCount);
|
if (Number.isFinite(totalFromRes) && totalFromRes > 0) {
|
this.total = totalFromRes;
|
} else {
|
this.total = Number(res.data?.total ?? res.data?.totalCount ?? res.total ?? mapped.length) || 0;
|
}
|
} else {
|
this.$showMessage(res.message || '查询失败');
|
}
|
} catch (err) {
|
console.error('fetchTools 错误:', err);
|
this.$showMessage('查询刀具失败,请检查网络或接口');
|
}
|
},
|
//翻页
|
async prevPage() {
|
if (this.pageIndex > 1) {
|
this.pageIndex--;
|
await this.fetchTools(this.searchKey);
|
}
|
},
|
async nextPage() {
|
if (this.pageIndex < this.totalPages) {
|
this.pageIndex++;
|
await this.fetchTools(this.searchKey);
|
}
|
},
|
async searchTool() {
|
this.pageIndex = 1; // 搜索时重置到第一页
|
await this.fetchTools(this.searchKey);
|
},
|
selectTool(tool) {
|
this.selectedToolNo = tool.no;
|
this.toolName = tool.name;
|
this.toolModel = tool.model;
|
},
|
confirmTool() {
|
this.showToolDialog = false;
|
},
|
async handleUpTool() {
|
if (!this.workOrderNo) {
|
this.$showMessage('工单号不能为空');
|
return;
|
}
|
if (!this.machineNo) {
|
this.$showMessage('机台号不能为空');
|
return;
|
}
|
if (!this.selectedToolNo) {
|
this.$showMessage('刀具编号不能为空');
|
return;
|
}
|
if (!this.useLimitInput) {
|
this.$showMessage('使用上限不能为空');
|
return;
|
}
|
const useLimit = Number(this.useLimitInput);
|
if (isNaN(useLimit) || useLimit <= 0) {
|
this.$showMessage('请输入有效的使用上限');
|
return;
|
}
|
const payload = {
|
workOrderNo: this.workOrderNo, // 工单号
|
machineNo: this.machineNo, // 机台编号
|
toolNo: this.selectedToolNo, // 刀具编号
|
type: '上刀', // 上刀
|
useLimit: this.useLimitInput ? Number(this.useLimitInput) : null // 使用上限
|
};
|
const res = await this.$post({
|
url: '/MesCutterLedger/SubmitToolAction',
|
data: JSON.stringify(payload),
|
headers: { 'Content-Type': 'application/json' }
|
});
|
if (res.status === 0) {
|
this.$showMessage('上刀提交成功');
|
// 成功后刷新列表
|
await this.fetchFormData();
|
} else {
|
this.$showMessage(res.message || '上刀提交失败');
|
}
|
},
|
async handleDownTool() {
|
if (!this.workOrderNo) {
|
this.$showMessage('工单号不能为空');
|
return;
|
}
|
if (!this.machineNo) {
|
this.$showMessage('机台号不能为空');
|
return;
|
}
|
if (!this.selectedToolNo) {
|
this.$showMessage('刀具编号不能为空');
|
return;
|
}
|
if (!this.useLimitInput) {
|
this.$showMessage('使用上限不能为空');
|
return;
|
}
|
const useLimit = Number(this.useLimitInput);
|
if (isNaN(useLimit) || useLimit <= 0) {
|
this.$showMessage('请输入有效的使用上限');
|
return;
|
}
|
const payload = {
|
workOrderNo: this.workOrderNo,
|
machineNo: this.machineNo,
|
toolNo: this.selectedToolNo,
|
type: '下刀', // 下刀
|
useLimit: this.useLimitInput ? Number(this.useLimitInput) : null
|
};
|
const res = await this.$post({
|
url: '/MesCutterLedger/SubmitToolAction',
|
data: JSON.stringify(payload),
|
headers: { 'Content-Type': 'application/json' }
|
});
|
if (res.status === 0) {
|
this.$showMessage('下刀提交成功');
|
// 成功后刷新列表
|
await this.fetchFormData();
|
} else {
|
this.$showMessage(res.message || '下刀提交失败');
|
}
|
},
|
cancel() {
|
this.selectedToolNo = '';
|
this.toolName = '';
|
this.toolModel = '';
|
},
|
async fetchFormData() {
|
// 重写:增强兼容性、统一字段映射、格式化时间和百分比,计算预警状态
|
if (!this.workOrderNo || !this.machineNo) {
|
console.warn('工单号或机台号为空,跳过获取表单数据');
|
return;
|
}
|
|
const payload = {
|
workOrderNo: this.workOrderNo.trim(),
|
machineNo: this.machineNo.trim()
|
};
|
|
try {
|
console.log('请求参数:', payload);
|
|
const res = await this.$post({
|
url: '/MesCutterLedger/GetFormData',
|
data: JSON.stringify(payload),
|
headers: { 'Content-Type': 'application/json' }
|
});
|
|
if (res.status !== 0) {
|
this.$showMessage(res.message || '获取表单数据失败');
|
return;
|
}
|
|
// 兼容多种返回结构,取到数组
|
const list = Array.isArray(res.data) ? res.data
|
: (res.data && res.data.tbBillList) ? res.data.tbBillList
|
: (res.data && res.data.data) ? res.data.data
|
: [];
|
|
const getField = (obj, ...keys) => {
|
for (const k of keys) if (obj?.[k] !== undefined && obj?.[k] !== null) return obj[k];
|
return null;
|
};
|
|
const parseNumber = v => {
|
if (v === null || v === undefined || v === '') return null;
|
const s = String(v).replace(/[,%%]/g, '').trim();
|
const n = parseFloat(s);
|
return Number.isFinite(n) ? n : null;
|
};
|
|
const formatPercent = n => (n === null || n === undefined || isNaN(n)) ? '' : `${Number(n).toFixed(2)}%`;
|
|
const mapped = (list || []).map(t => {
|
const upTimeRaw = getField(t, 'uP_TIME', 'UP_TIME', 'uPTime', 'UPTIME', 'UpTime');
|
const downTimeRaw = getField(t, 'dowN_TIME', 'DOWN_TIME', 'downTime', 'DOWNTIME');
|
const lifePercentRaw = getField(t, 'lifE_PERCENT', 'LIFE_PERCENT', 'lifePercent', 'LIFEPERCENT');
|
const lifeWarnRaw = getField(t, 'lifE_WARN', 'LIFE_WARN', 'lifeWarn', 'LIFEWARN');
|
|
const lifePercentNum = parseNumber(lifePercentRaw);
|
const lifeWarnNum = parseNumber(lifeWarnRaw);
|
|
// 预警规则:当寿命比 >= 预警值时标记 警告(根据图片示例:90.01% vs 90% 为 警告)
|
let warnStatus = getField(t, 'status', 'STATUS') || '';
|
if (lifeWarnNum !== null && lifePercentNum !== null) {
|
warnStatus = (lifePercentNum >= lifeWarnNum) ? '警告' : '正常';
|
} else {
|
// 如果后端直接提供状态字段,则保留,否则默认空
|
warnStatus = warnStatus || '';
|
}
|
|
return {
|
id: getField(t, 'id', 'ID') || `${getField(t, 'cutteR_ID') || getField(t, 'CUTTER_ID') || ''}-${upTimeRaw || ''}`,
|
no: getField(t, 'cutteR_ID', 'CUTTER_ID', 'cutterId', 'no') || '',
|
name: getField(t, 'cutteR_NAME', 'CUTTER_NAME', 'cutterName', 'name') || '',
|
upTime: this.formatDateTime(upTimeRaw),
|
upCount: getField(t, 'uP_COUNT', 'UP_COUNT', 'upCount') ?? '',
|
downTime: this.formatDateTime(downTimeRaw),
|
downCount: getField(t, 'dowN_COUNT', 'DOWN_COUNT', 'downCount') ?? '',
|
useCount: getField(t, 'usE_COUNT', 'USE_COUNT', 'useCount') ?? '',
|
useLimit: getField(t, 'usE_LIMIT', 'USE_LIMIT', 'useLimit') ?? '',
|
lifePercent: formatPercent(lifePercentNum),
|
lifeWarn: lifeWarnNum !== null ? `${Number(lifeWarnNum).toFixed(0)}%` : (lifeWarnRaw ? String(lifeWarnRaw) : ''),
|
warnStatus
|
};
|
});
|
|
this.toolRecords = mapped;
|
|
// 更新 total:优先使用后端 totalCount,否则使用返回数组长度
|
const totalFromRes = Number(res.totalCount);
|
if (Number.isFinite(totalFromRes) && totalFromRes > 0) {
|
this.total = totalFromRes;
|
} else {
|
this.total = Number(res.data?.total ?? res.data?.totalCount ?? this.toolRecords.length) || 0;
|
}
|
} catch (error) {
|
console.error('获取表单数据错误:', error);
|
this.$showMessage('获取数据失败,请检查网络连接');
|
}
|
},
|
formatDateTime(dateTimeStr) {
|
if (!dateTimeStr) return '';
|
// 支持多种后端时间格式:ISO / 时间戳 / 自定义字符串
|
// 优先尝试解析为 Date,失败则返回原始字符串的可读片段
|
try {
|
// 如果是时间戳(秒或毫秒)
|
const s = String(dateTimeStr).trim();
|
if (/^\d{10}$/.test(s)) {
|
const d = new Date(Number(s) * 1000);
|
return `${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
|
}
|
if (/^\d{13}$/.test(s)) {
|
const d = new Date(Number(s));
|
return `${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
|
}
|
const date = new Date(dateTimeStr);
|
if (!isNaN(date.getTime())) {
|
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
|
}
|
// 回退:截取到日期和时间部分(常见格式)
|
const match = String(dateTimeStr).match(/(\d{1,4}[-\/]\d{1,2}[-\/]\d{1,2}).*?(\d{1,2}:\d{2})/);
|
if (match) return `${match[1].replace(/-/g, '/').replace(/^\d{4}\//, (m) => m)} ${match[2]}`;
|
return String(dateTimeStr);
|
} catch {
|
return String(dateTimeStr);
|
}
|
}
|
//// 添加辅助方法
|
//formatDateTime(dateTimeStr) {
|
// if (!dateTimeStr) return '';
|
// // 根据后端返回的时间格式进行调整
|
// const date = new Date(dateTimeStr);
|
// return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
|
//}
|
},
|
mounted() {
|
this.fetchTools('');
|
this.machineNo = uni.getStorageSync('machineNo') || '';
|
this.workOrderNo = uni.getStorageSync('daa001') || '';
|
|
// 添加调试信息
|
console.log('机台号:', this.machineNo);
|
console.log('工单号:', this.workOrderNo);
|
|
if (this.machineNo && this.workOrderNo) {
|
this.fetchFormData();
|
} else {
|
console.warn('机台号或工单号为空,无法获取表单数据');
|
}
|
}
|
};
|
</script>
|
|
<style scoped>
|
.top-section-grid {
|
display: flex;
|
justify-content: center;
|
align-items: flex-end;
|
gap: 32px;
|
margin-bottom: 2vh;
|
}
|
|
.form-cell {
|
display: flex;
|
align-items: center;
|
}
|
|
.form-label {
|
width: 90px;
|
font-weight: bold;
|
}
|
|
.input {
|
padding: 1vh;
|
font-size: 1.1vw;
|
border: 1px solid #ccc;
|
width: 10vw;
|
margin-right: 8px;
|
}
|
|
.form-select {
|
width: 12vw;
|
padding: 1vh;
|
font-size: 1.1vw;
|
margin-right: 8px;
|
}
|
|
.btn-blue {
|
background-color: #00A2E9;
|
color: white;
|
border: none;
|
padding: 8px 18px;
|
margin-left: 8px;
|
border-radius: 5px;
|
cursor: pointer;
|
}
|
|
.button-row {
|
display: flex;
|
justify-content: center;
|
gap: 32px;
|
margin: 2vh 0;
|
}
|
|
.save-btn, .cancel-btn {
|
width: 28%;
|
padding: 1.5vh;
|
background-color: #00A2E9;
|
color: white;
|
font-size: 1.2vw;
|
border: none;
|
text-align: center;
|
border-radius: 5px;
|
}
|
|
.cancel-btn {
|
background-color: #ccc;
|
color: #333;
|
}
|
|
.dialog-overlay {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(0,0,0,0.3);
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
z-index: 1000;
|
}
|
|
.dialog {
|
background: #fff;
|
padding: 2vh 2vw;
|
border-radius: 8px;
|
width: 60vw;
|
}
|
|
.tool-list {
|
display: flex;
|
flex-wrap: wrap;
|
margin: 1vh 0;
|
max-height: 40vh;
|
overflow-y: auto;
|
}
|
|
.tool-btn {
|
margin: 5px 10px 5px 0;
|
padding: 8px 16px;
|
background: #f5f5f5;
|
border: 1px solid #ccc;
|
border-radius: 4px;
|
cursor: pointer;
|
background: #e0e0e0;
|
color: #888;
|
}
|
|
.dialog-actions {
|
display: flex;
|
justify-content: space-between;
|
margin-top: 2vh;
|
}
|
|
.table-section {
|
margin: 2vh 0;
|
overflow-x: auto;
|
}
|
|
table {
|
width: 100%;
|
border-collapse: collapse;
|
}
|
|
th, td {
|
border: 1px solid #ccc;
|
padding: 8px 4px;
|
text-align: center;
|
font-size: 1vw;
|
}
|
|
.warn {
|
color: red;
|
font-weight: bold;
|
}
|
|
.bottom-section {
|
display: flex;
|
justify-content: space-around;
|
margin-top: 2vh;
|
}
|
|
.tool-desc {
|
margin-top: 2vh;
|
}
|
</style>
|