<template>
|
<view class="work-order-page">
|
<!-- 页面头部 -->
|
<PageHeader title="生产报工" />
|
<!-- 搜索区域 -->
|
<view class="search-section">
|
<view class="search-bar">
|
<view class="search-input">
|
<up-input class="search-text"
|
placeholder="请输入工单编号"
|
v-model="searchForm.workOrderNo"
|
@change="handleQuery"
|
clearable />
|
</view>
|
<view class="filter-button"
|
@click="handleQuery">
|
<u-icon name="search"
|
size="24"
|
color="#999"></u-icon>
|
</view>
|
</view>
|
</view>
|
<!-- 工单列表 -->
|
<view v-if="tableData.length > 0"
|
class="work-order-list">
|
<view v-for="(item, index) in tableData"
|
:key="index"
|
class="work-order-item">
|
<view class="item-header">
|
<view class="item-title">
|
<text class="work-order-no">{{ item.workOrderNo || '无编号' }}</text>
|
<up-tag :type="getWorkOrderTypeTag(item.workOrderType)"
|
size="small"
|
class="type-tag">
|
{{ item.workOrderType || '未知' }}
|
</up-tag>
|
</view>
|
<up-tag :type="getCompletionStatusTag(item.completionStatus)"
|
size="small">
|
{{ getCompletionStatusText(item.completionStatus) }}
|
</up-tag>
|
</view>
|
<view class="item-content">
|
<view class="content-row">
|
<text class="label">产品名称:</text>
|
<text class="value">{{ item.productName || '-' }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">规格:</text>
|
<text class="value">{{ item.model || '-' }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">单位:</text>
|
<text class="value">{{ item.unit || '-' }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">工序名称:</text>
|
<text class="value">{{ item.processName || '-' }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">需求数量:</text>
|
<text class="value">{{ item.planQuantity || 0 }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">完成数量:</text>
|
<text class="value">{{ item.completeQuantity || 0 }}</text>
|
</view>
|
<view class="content-row">
|
<text class="label">完成进度:</text>
|
<view class="progress-container">
|
<!-- <up-progress :percentage="toProgressPercentage(item.completionStatus)"
|
:color="progressColor(toProgressPercentage(item.completionStatus))"
|
:active-color="progressColor(toProgressPercentage(item.completionStatus))"
|
:height="6"
|
:show-percentage="false"
|
class="progress-bar" /> -->
|
<text class="progress-text">{{ toProgressPercentage(item.completionStatus) }}%</text>
|
</view>
|
</view>
|
<view class="content-row">
|
<text class="label">计划时间:</text>
|
<text class="value">{{ formatDate(item.planStartTime) }} 至 {{ formatDate(item.planEndTime) }}</text>
|
</view>
|
<view v-if="item.actualStartTime"
|
class="content-row">
|
<text class="label">实际时间:</text>
|
<text class="value">{{ formatDate(item.actualStartTime) }} 至 {{ formatDate(item.actualEndTime) }}</text>
|
</view>
|
</view>
|
<view class="item-footer">
|
<u-button size="small"
|
@click="handleEdit(item)">
|
编辑
|
</u-button>
|
<u-button size="small"
|
@click="viewFileList(item)">
|
附件
|
</u-button>
|
<u-button type="primary"
|
size="small"
|
:disabled="item.planQuantity <= 0"
|
@click="showReportDialog(item)">
|
报工
|
</u-button>
|
</view>
|
</view>
|
</view>
|
<!-- 空状态 -->
|
<view v-else
|
class="no-data">
|
<up-empty mode="data"
|
text="暂无工单数据" />
|
</view>
|
<!-- 分页组件 -->
|
<!-- 编辑时间弹窗 -->
|
<up-popup v-model:show="editDialogVisible"
|
mode="center"
|
round>
|
<view class="dialog-content">
|
<view class="dialog-header">
|
<text class="dialog-title">编辑时间</text>
|
<up-icon name="close"
|
size="20"
|
color="#999"
|
@click="editDialogVisible = false" />
|
</view>
|
<view class="dialog-body">
|
<view class="form-item">
|
<text class="form-label">计划开始时间</text>
|
<view class="fake-input-wrapper"
|
@click="showDatePicker('planStartTime',editrow.planStartTime)">
|
<text class="fake-input-text"
|
:class="{ 'placeholder': !editrow.planStartTime }">
|
{{ editrow.planStartTime || '请选择计划开始时间' }}
|
</text>
|
<up-icon name="calendar"
|
size="20"
|
color="#999" />
|
</view>
|
</view>
|
<view class="form-item">
|
<text class="form-label">计划结束时间</text>
|
<view class="fake-input-wrapper"
|
@click="showDatePicker('planEndTime',editrow.planEndTime)">
|
<text class="fake-input-text"
|
:class="{ 'placeholder': !editrow.planEndTime }">
|
{{ editrow.planEndTime || '请选择计划结束时间' }}
|
</text>
|
<up-icon name="calendar"
|
size="20"
|
color="#999" />
|
</view>
|
</view>
|
<view class="form-item">
|
<text class="form-label">实际开始时间</text>
|
<view class="fake-input-wrapper"
|
@click="showDatePicker('actualStartTime',editrow.actualStartTime)">
|
<text class="fake-input-text"
|
:class="{ 'placeholder': !editrow.actualStartTime }">
|
{{ editrow.actualStartTime || '请选择实际开始时间' }}
|
</text>
|
<up-icon name="calendar"
|
size="20"
|
color="#999" />
|
</view>
|
</view>
|
<view class="form-item">
|
<text class="form-label">实际结束时间</text>
|
<view class="fake-input-wrapper"
|
@click="showDatePicker('actualEndTime',editrow.actualEndTime)">
|
<text class="fake-input-text"
|
:class="{ 'placeholder': !editrow.actualEndTime }">
|
{{ editrow.actualEndTime || '请选择实际结束时间' }}
|
</text>
|
<up-icon name="calendar"
|
size="20"
|
color="#999" />
|
</view>
|
</view>
|
</view>
|
<view class="dialog-footer">
|
<u-button type="default"
|
class="footer-btn"
|
@click="editDialogVisible = false">
|
取消
|
</u-button>
|
<u-button type="primary"
|
class="footer-btn"
|
@click="handleUpdate">
|
确定
|
</u-button>
|
</view>
|
</view>
|
</up-popup>
|
<!-- 报工弹窗 -->
|
<up-popup v-model:show="reportDialogVisible"
|
mode="center"
|
round
|
style="width: 500px">
|
<view class="dialog-content">
|
<view class="dialog-header">
|
<text class="dialog-title">报工</text>
|
<up-icon name="close"
|
size="20"
|
color="#999"
|
@click="reportDialogVisible = false" />
|
</view>
|
<view class="dialog-body">
|
<view class="form-item">
|
<text class="form-label">待生产数量</text>
|
<up-input v-model="reportForm.planQuantity"
|
disabled
|
readonly />
|
</view>
|
<view class="form-item">
|
<text class="form-label required">本次生产数量</text>
|
<up-input v-model.number="reportForm.quantity"
|
type="number"
|
placeholder="请输入本次生产数量" />
|
</view>
|
<view class="form-item">
|
<text class="form-label">报废数量</text>
|
<up-input v-model.number="reportForm.scrapQty"
|
type="number"
|
placeholder="请输入报废数量" />
|
</view>
|
<view class="form-item">
|
<text class="form-label">班组信息</text>
|
<view class="fake-input-wrapper"
|
@click="showUserSheet = true">
|
<text class="fake-input-text"
|
:class="{ 'placeholder': !reportForm.userName }">
|
{{ reportForm.userName || '请选择班组信息' }}
|
</text>
|
<up-icon name="arrow-right"
|
size="20"
|
color="#999" />
|
</view>
|
</view>
|
</view>
|
<view class="dialog-footer">
|
<u-button type="default"
|
class="footer-btn"
|
@click="reportDialogVisible = false">
|
取消
|
</u-button>
|
<u-button type="primary"
|
class="footer-btn"
|
@click="handleReport">
|
确定
|
</u-button>
|
</view>
|
</view>
|
</up-popup>
|
<!-- 日期选择器 -->
|
<!-- <up-popup v-model:show="showDate"
|
mode="date"
|
:start-year="2020"
|
:end-year="2030"
|
@close="showDate = false"
|
@confirm="confirmDate" /> -->
|
<u-datetime-picker :show="showDate"
|
v-model="value1"
|
@close="showDate = false"
|
@confirm="confirmDate"
|
@cancel="showDate = false"
|
mode="date"
|
format="YYYY-MM-DD"></u-datetime-picker>
|
<!-- 班组选择 -->
|
<up-action-sheet :show="showUserSheet"
|
:actions="userSheetOptions"
|
@select="selectUser"
|
@close="showUserSheet = false"
|
title="选择班组信息" />
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, computed } from "vue";
|
import { onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import dayjs from "dayjs";
|
import {
|
productWorkOrderPage,
|
updateProductWorkOrder,
|
addProductMain,
|
} from "@/api/productionManagement/workOrder.js";
|
import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
|
|
// 显示提示信息
|
const showToast = message => {
|
uni.showToast({
|
title: message,
|
icon: "none",
|
});
|
};
|
|
// 搜索表单
|
const searchForm = ref({
|
workOrderNo: "",
|
});
|
|
// 工单列表数据
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
const value1 = ref(new Date());
|
// 分页数据
|
const page = reactive({
|
current: -1,
|
size: -1,
|
total: 0,
|
});
|
|
// 编辑弹窗
|
const editDialogVisible = ref(false);
|
const editrow = ref(null);
|
|
// 报工弹窗
|
const reportDialogVisible = ref(false);
|
const reportForm = reactive({
|
planQuantity: 0,
|
quantity: 0,
|
userName: "",
|
workOrderId: "",
|
reportWork: "",
|
productProcessRouteItemId: "",
|
userId: "",
|
productMainId: null,
|
scrapQty: 0,
|
});
|
const currentReportRowData = ref(null);
|
|
// 日期选择器
|
const showDate = ref(false);
|
const currentDateField = ref("");
|
|
// 班组选择
|
const showUserSheet = ref(false);
|
const userOptions = ref([]);
|
const userSheetOptions = computed(() => {
|
return userOptions.value.map(user => ({
|
name: user.nickName,
|
value: user.userId,
|
}));
|
});
|
|
// 格式化日期
|
const formatDate = dateStr => {
|
if (!dateStr) return "-";
|
return dayjs(dateStr).format("YYYY-MM-DD");
|
};
|
|
// 进度百分比转换
|
const toProgressPercentage = val => {
|
const n = Number(val);
|
if (!Number.isFinite(n)) return 0;
|
if (n <= 0) return 0;
|
if (n >= 100) return 100;
|
return Math.round(n);
|
};
|
|
// 进度条颜色
|
const progressColor = percentage => {
|
const p = toProgressPercentage(percentage);
|
if (p < 30) return "#f56c6c";
|
if (p < 50) return "#e6a23c";
|
if (p < 80) return "#409eff";
|
return "#67c23a";
|
};
|
|
// 获取工单类型标签
|
const getWorkOrderTypeTag = type => {
|
switch (type) {
|
case "生产工单":
|
return "success";
|
case "维修工单":
|
return "warning";
|
case "检验工单":
|
return "info";
|
default:
|
return "info";
|
}
|
};
|
|
// 获取完成状态标签
|
const getCompletionStatusTag = status => {
|
const percentage = toProgressPercentage(status);
|
if (percentage >= 100) return "success";
|
if (percentage >= 50) return "warning";
|
return "error";
|
};
|
|
// 获取完成状态文本
|
const getCompletionStatusText = status => {
|
const percentage = toProgressPercentage(status);
|
if (percentage >= 100) return "已完成";
|
return `${percentage}%`;
|
};
|
|
// 查询列表
|
const handleQuery = () => {
|
page.current = -1;
|
getList();
|
};
|
|
const getList = () => {
|
tableLoading.value = true;
|
const params = { ...searchForm.value, ...page };
|
productWorkOrderPage(params)
|
.then(res => {
|
tableLoading.value = false;
|
tableData.value = res.data.records || [];
|
// tableData.value = [
|
// {
|
// id: "WO20260304001",
|
// workOrderNo: "WO20260304001",
|
// workOrderType: "生产工单",
|
// productName: "不锈钢板材",
|
// model: "304-2B",
|
// unit: "kg",
|
// processName: "切割工序",
|
// planQuantity: 1000,
|
// completeQuantity: 650,
|
// completionStatus: 65,
|
// planStartTime: "2026-03-01",
|
// planEndTime: "2026-03-10",
|
// actualStartTime: "2026-03-02",
|
// actualEndTime: null,
|
// remark: "紧急订单,请优先处理",
|
// },
|
// ];
|
page.total = res.data.total || 0;
|
})
|
.catch(() => {
|
tableLoading.value = false;
|
showToast("获取工单列表失败");
|
});
|
};
|
|
// 编辑工单
|
const handleEdit = row => {
|
editrow.value = JSON.parse(JSON.stringify(row));
|
editDialogVisible.value = true;
|
};
|
|
// 更新工单
|
const handleUpdate = () => {
|
updateProductWorkOrder(editrow.value)
|
.then(res => {
|
showToast("修改成功");
|
editDialogVisible.value = false;
|
getList();
|
})
|
.catch(() => {
|
showToast("修改失败");
|
});
|
};
|
|
// 显示日期选择器
|
const showDatePicker = (field, defaultValue) => {
|
currentDateField.value = field;
|
// 设置默认值
|
if (defaultValue) {
|
value1.value = dayjs(defaultValue);
|
} else {
|
value1.value = dayjs();
|
}
|
showDate.value = true;
|
};
|
|
// 确认日期选择
|
const confirmDate = e => {
|
if (currentDateField.value && editrow.value) {
|
// 确保日期格式为 YYYY-MM-DD
|
const formattedDate = dayjs(e.value).format("YYYY-MM-DD");
|
editrow.value[currentDateField.value] = formattedDate;
|
}
|
showDate.value = false;
|
};
|
|
// 显示报工弹窗
|
const showReportDialog = row => {
|
currentReportRowData.value = row;
|
reportForm.planQuantity = row.planQuantity || 0;
|
reportForm.quantity = row.quantity || 0;
|
reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
|
reportForm.workOrderId = row.id;
|
reportForm.reportWork = row.reportWork;
|
reportForm.productMainId = row.productMainId;
|
reportForm.scrapQty = row.scrapQty || 0;
|
|
// 获取当前登录用户信息,设置为默认选中
|
getUserProfile()
|
.then(res => {
|
if (res.code === 200) {
|
reportForm.userId = res.data.userId;
|
reportForm.userName = res.data.nickName;
|
}
|
})
|
.catch(err => {
|
console.error("获取用户信息失败", err);
|
});
|
|
reportDialogVisible.value = true;
|
};
|
|
// 处理报工
|
const handleReport = () => {
|
if (reportForm.planQuantity <= 0) {
|
showToast("待生产数量为0,无法报工");
|
return;
|
}
|
if (!reportForm.quantity || reportForm.quantity <= 0) {
|
showToast("请输入有效的本次生产数量");
|
return;
|
}
|
if (reportForm.quantity > reportForm.planQuantity) {
|
showToast("本次生产数量不能超过待生产数量");
|
return;
|
}
|
|
addProductMain(reportForm)
|
.then(res => {
|
if (res.code === 200) {
|
showToast("报工成功");
|
reportDialogVisible.value = false;
|
getList();
|
} else {
|
showToast(res.msg || "报工失败");
|
}
|
})
|
.catch(() => {
|
showToast("报工失败");
|
});
|
};
|
|
const viewFileList = item => {
|
uni.setStorageSync("workOrderFileId", item.id);
|
uni.navigateTo({
|
url: "/pages/productionManagement/workOrder/fileList",
|
});
|
};
|
|
// 获取用户列表
|
const getUserList = () => {
|
userListNoPageByTenantId()
|
.then(res => {
|
if (res.code === 200) {
|
userOptions.value = res.data || [];
|
}
|
})
|
.catch(err => {
|
console.error("获取用户列表失败", err);
|
});
|
};
|
|
// 选择用户
|
const selectUser = e => {
|
reportForm.userId = e.value;
|
const selectedUser = userOptions.value.find(user => user.userId === e.value);
|
if (selectedUser) {
|
reportForm.userName = selectedUser.nickName;
|
}
|
showUserSheet.value = false;
|
};
|
|
onMounted(() => {
|
getList();
|
getUserList();
|
});
|
|
onShow(() => {
|
getList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "../../../styles/sales-common.scss";
|
|
.work-order-page {
|
min-height: 100vh;
|
background-color: #f5f5f5;
|
}
|
|
// 搜索区域
|
.search-container {
|
padding: 16px;
|
background-color: #ffffff;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
}
|
|
.search-box {
|
display: flex;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.search-box :deep(.up-input) {
|
flex: 1;
|
}
|
|
// 工单列表
|
.work-order-list {
|
padding: 16px;
|
}
|
|
.work-order-item {
|
background: #ffffff;
|
border-radius: 12px;
|
margin-bottom: 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
overflow: hidden;
|
}
|
|
.item-header {
|
padding: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.item-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.work-order-no {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.type-tag {
|
margin-left: 8px;
|
}
|
|
.item-content {
|
padding: 16px;
|
}
|
|
.content-row {
|
display: flex;
|
margin-bottom: 12px;
|
align-items: flex-start;
|
}
|
|
.content-row:last-child {
|
margin-bottom: 0;
|
}
|
|
.label {
|
width: 90px;
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.value {
|
flex: 1;
|
font-size: 14px;
|
color: #303133;
|
}
|
|
.progress-container {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.progress-bar {
|
flex: 1;
|
}
|
|
.progress-text {
|
font-size: 12px;
|
color: #606266;
|
min-width: 40px;
|
}
|
|
.item-footer {
|
padding: 16px;
|
border-top: 1px solid #f0f0f0;
|
display: flex;
|
gap: 12px;
|
justify-content: flex-end;
|
}
|
|
// 空状态
|
.no-data {
|
padding: 60px 20px;
|
text-align: center;
|
}
|
|
// 分页组件
|
.pagination {
|
padding: 20px;
|
background: #fff;
|
margin-top: 10px;
|
display: flex;
|
justify-content: center;
|
}
|
|
// 弹窗样式
|
.dialog-content {
|
padding: 24px;
|
background: #ffffff;
|
border-radius: 12px;
|
width: 90vw;
|
}
|
|
.dialog-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 24px;
|
padding-bottom: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.dialog-title {
|
font-size: 18px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.dialog-body {
|
margin-bottom: 24px;
|
}
|
|
.form-item {
|
margin-bottom: 20px;
|
}
|
|
.form-label {
|
display: block;
|
font-size: 14px;
|
color: #606266;
|
margin-bottom: 8px;
|
}
|
|
.form-label.required::before {
|
content: "*";
|
color: #f56c6c;
|
margin-right: 4px;
|
}
|
|
.dialog-body :deep(.up-input) {
|
width: 100%;
|
}
|
|
.fake-input-wrapper {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 44px;
|
padding: 0 12px;
|
// background-color: #f5f7fa;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
}
|
|
.fake-input-text {
|
font-size: 14px;
|
color: #303133;
|
}
|
|
.fake-input-text.placeholder {
|
color: #c0c4cc;
|
}
|
|
.dialog-footer {
|
display: flex;
|
gap: 16px;
|
padding-top: 16px;
|
border-top: 1px solid #f0f0f0;
|
}
|
|
.footer-btn {
|
flex: 1;
|
height: 44px;
|
}
|
</style>
|