<template>
|
<div>
|
<el-dialog v-model="isShow"
|
title="工艺路线项目"
|
width="800px"
|
@close="closeModal">
|
<div class="operate-button">
|
<el-button type="primary"
|
@click="isShowProductSelectDialog = true"
|
class="mb5"
|
style="margin-bottom: 10px;">
|
选择产品
|
</el-button>
|
<el-switch v-model="isTable"
|
inline-prompt
|
active-text="表格"
|
inactive-text="列表"
|
@change="handleViewChange" />
|
</div>
|
<el-table v-if="isTable"
|
ref="multipleTable"
|
v-loading="tableLoading"
|
border
|
:data="routeItems"
|
:header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
|
row-key="id"
|
tooltip-effect="dark"
|
class="lims-table"
|
style="cursor: move;">
|
<el-table-column align="center"
|
label="序号"
|
width="60">
|
<template #default="scope">
|
{{ scope.$index + 1 }}
|
</template>
|
</el-table-column>
|
<el-table-column v-for="(item, index) in tableColumn"
|
:key="index"
|
:label="item.label"
|
:width="item.width"
|
show-overflow-tooltip>
|
<template #default="scope"
|
v-if="item.dataType === 'action'">
|
<el-button v-for="(op, opIndex) in item.operation"
|
:key="opIndex"
|
:type="op.type"
|
:link="op.link"
|
size="small"
|
@click.stop="op.clickFun(scope.row)">
|
{{ op.name }}
|
</el-button>
|
</template>
|
<template #default="scope"
|
v-else>
|
<template v-if="item.prop === 'processId'">
|
<el-select v-model="scope.row[item.prop]"
|
style="width: 100%;"
|
@mousedown.stop>
|
<el-option v-for="process in processOptions"
|
:key="process.id"
|
:label="process.name"
|
:value="process.id" />
|
</el-select>
|
</template>
|
<template v-else>
|
{{ scope.row[item.prop] || '-' }}
|
</template>
|
</template>
|
</el-table-column>
|
</el-table>
|
<!-- 使用普通div替代el-steps -->
|
<div v-else
|
ref="stepsContainer"
|
class="mb5 custom-steps"
|
style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;">
|
<div v-for="(item, index) in routeItems"
|
:key="item.id"
|
class="custom-step draggable-step"
|
:data-id="item.id"
|
style="cursor: move; flex: 0 0 auto; min-width: 220px;">
|
<div class="step-content">
|
<div class="step-number">{{ index + 1 }}</div>
|
<el-card :header="item.productName"
|
class="step-card"
|
style="cursor: move;">
|
<div class="step-card-content">
|
<p>{{ item.model }}</p>
|
<p>{{ item.unit }}</p>
|
<el-select v-model="item.processId"
|
style="width: 100%;"
|
@mousedown.stop>
|
<el-option v-for="process in processOptions"
|
:key="process.id"
|
:label="process.name"
|
:value="process.id" />
|
</el-select>
|
</div>
|
<template #footer>
|
<div class="step-card-footer">
|
<el-button type="danger"
|
link
|
size="small"
|
@click.stop="removeItemByID(item.id)">删除</el-button>
|
</div>
|
</template>
|
</el-card>
|
</div>
|
</div>
|
</div>
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button type="primary"
|
@click="handleSubmit">确认</el-button>
|
<el-button @click="closeModal">取消</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
<ProductSelectDialog v-model="isShowProductSelectDialog"
|
@confirm="handelSelectProducts" />
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
computed,
|
getCurrentInstance,
|
onMounted,
|
onUnmounted,
|
nextTick,
|
} from "vue";
|
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
|
import {
|
findProductProcessRouteItemList,
|
addOrUpdateProductProcessRouteItem,
|
deleteRouteItem,
|
} from "@/api/productionManagement/productProcessRoute.js";
|
import { processList } from "@/api/productionManagement/productionProcess.js";
|
import Sortable from "sortablejs";
|
|
const props = defineProps({
|
visible: {
|
type: Boolean,
|
required: true,
|
default: false,
|
},
|
record: {
|
type: Object,
|
required: true,
|
default: () => ({}),
|
},
|
});
|
|
const emit = defineEmits(["update:visible", "completed"]);
|
|
const processOptions = ref([]);
|
const tableLoading = ref(false);
|
const isShowProductSelectDialog = ref(false);
|
const routeItems = ref([]);
|
let tableSortable = null;
|
let stepsSortable = null;
|
const multipleTable = ref(null);
|
const stepsContainer = ref(null);
|
const isTable = ref(true);
|
|
const isShow = computed({
|
get() {
|
return props.visible;
|
},
|
set(val) {
|
emit("update:visible", val);
|
},
|
});
|
|
const tableColumn = ref([
|
{ label: "产品名称", prop: "productName", width: 180 },
|
{ label: "规格名称", prop: "model", width: 150 },
|
{ label: "单位", prop: "unit", width: 80 },
|
{ label: "工序名称", prop: "processId", width: 180 },
|
{
|
dataType: "action",
|
label: "操作",
|
align: "center",
|
fixed: "right",
|
width: 100,
|
operation: [
|
{
|
name: "删除",
|
type: "danger",
|
link: true,
|
clickFun: row => {
|
console.log(row.id, "删除");
|
|
const dragSortx = routeItems.value.findIndex(
|
item => item.dragSort === row.dragSort
|
);
|
const idx = routeItems.value.findIndex(item => item.id === row.id);
|
console.log(idx, "idx");
|
if (row.id) {
|
deleteRouteItemByIds({ id: row.id }, idx);
|
} else {
|
removeItem(dragSortx);
|
}
|
},
|
},
|
],
|
},
|
]);
|
|
const removeItem = index => {
|
console.log("软删除", index);
|
|
routeItems.value.splice(index, 1);
|
updateDragSort();
|
nextTick(() => initSortable());
|
};
|
|
const removeItemByID = id => {
|
const idx = routeItems.value.findIndex(item => item.id === id);
|
if (idx > -1) {
|
routeItems.value.splice(idx, 1);
|
updateDragSort();
|
nextTick(() => initSortable());
|
}
|
};
|
|
const deleteRouteItemByIds = (ids, index) => {
|
deleteRouteItem(ids).then(res => {
|
routeItems.value.splice(index, 1);
|
updateDragSort();
|
nextTick(() => initSortable());
|
});
|
};
|
|
const closeModal = () => {
|
isShow.value = false;
|
};
|
|
const updateDragSort = () => {
|
routeItems.value.forEach((item, index) => {
|
item.dragSort = index + 1;
|
});
|
routeItems.value = [...routeItems.value];
|
console.log("更新后的数组:", routeItems.value);
|
};
|
|
const handelSelectProducts = products => {
|
destroySortable();
|
|
// 计算新的dragSort值起始点
|
const maxDragSort =
|
routeItems.value.length > 0
|
? Math.max(...routeItems.value.map(item => item.dragSort || 0))
|
: 0;
|
|
const newData = products.map(({ id, ...product }, index) => ({
|
...product,
|
productModelId: id,
|
routeId: props.record.id,
|
// id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
processId: undefined,
|
dragSort: maxDragSort + index + 1,
|
}));
|
|
console.log("选择产品前数组:", routeItems.value);
|
routeItems.value.push(...newData);
|
updateDragSort();
|
console.log("选择产品后数组:", routeItems.value);
|
|
// 延迟初始化,确保DOM完全渲染
|
nextTick(() => {
|
// 强制重新渲染组件
|
if (proxy?.$forceUpdate) {
|
proxy.$forceUpdate();
|
}
|
|
const temp = [...routeItems.value];
|
routeItems.value = [];
|
nextTick(() => {
|
routeItems.value = temp;
|
initSortable();
|
});
|
});
|
};
|
|
const findProcessRouteItems = () => {
|
tableLoading.value = true;
|
findProductProcessRouteItemList({ orderId: props.record.id })
|
.then(res => {
|
tableLoading.value = false;
|
routeItems.value = res.data.map(item => ({
|
...item,
|
processId: item.processId === 0 ? undefined : item.processId,
|
}));
|
// 延迟初始化,确保DOM完全渲染
|
nextTick(() => {
|
setTimeout(() => initSortable(), 100);
|
});
|
})
|
.catch(err => {
|
tableLoading.value = false;
|
console.error("获取列表失败:", err);
|
});
|
};
|
|
const findProcessList = () => {
|
processList({})
|
.then(res => {
|
processOptions.value = res.data;
|
})
|
.catch(err => {
|
console.error("获取工序失败:", err);
|
});
|
};
|
|
const { proxy } = getCurrentInstance() || {};
|
|
const handleSubmit = () => {
|
const hasEmptyProcess = routeItems.value.some(item => !item.processId);
|
if (hasEmptyProcess) {
|
proxy?.$modal?.msgError("请为所有项目选择工序");
|
return;
|
}
|
|
addOrUpdateProductProcessRouteItem({
|
routeId: props.record.id,
|
processRouteItem: routeItems.value,
|
})
|
.then(res => {
|
isShow.value = false;
|
emit("completed");
|
proxy?.$modal?.msgSuccess("提交成功");
|
})
|
.catch(err => {
|
proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
|
});
|
};
|
|
const destroySortable = () => {
|
if (tableSortable) {
|
tableSortable.destroy();
|
tableSortable = null;
|
}
|
if (stepsSortable) {
|
stepsSortable.destroy();
|
stepsSortable = null;
|
}
|
};
|
|
const initSortable = () => {
|
destroySortable();
|
|
if (isTable.value) {
|
if (!multipleTable.value) return;
|
const tbody =
|
multipleTable.value.$el.querySelector(".el-table__body tbody") ||
|
multipleTable.value.$el.querySelector(
|
".el-table__body-wrapper > table > tbody"
|
);
|
if (!tbody) return;
|
|
tableSortable = new Sortable(tbody, {
|
animation: 150,
|
ghostClass: "sortable-ghost",
|
handle: ".el-table__row",
|
filter: ".el-button, .el-select",
|
onEnd: evt => {
|
if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex])
|
return;
|
|
// 使用数组 splice 方法重新排序,与表格模式保持一致
|
const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
|
routeItems.value.splice(evt.newIndex, 0, moveItem);
|
updateDragSort();
|
console.log("排序后数组:", routeItems.value);
|
},
|
});
|
} else {
|
if (!stepsContainer.value) return;
|
|
// 修改:直接使用stepsContainer.value作为拖拽容器
|
const stepsList = stepsContainer.value;
|
if (!stepsList) {
|
console.warn("未找到步骤条拖拽容器");
|
return;
|
}
|
|
// 修改:简化拖拽配置
|
stepsSortable = new Sortable(stepsList, {
|
animation: 150,
|
ghostClass: "sortable-ghost",
|
draggable: ".draggable-step", // 可拖拽元素
|
handle: ".draggable-step, .step-card", // 拖拽手柄
|
filter: ".el-button, .el-select, .el-input", // 过滤按钮/选择器
|
forceFallback: true,
|
fallbackClass: "sortable-fallback",
|
preventOnFilter: true,
|
scroll: true,
|
scrollSensitivity: 30,
|
scrollSpeed: 10,
|
bubbleScroll: true,
|
onEnd: evt => {
|
if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex])
|
return;
|
|
// 使用数组 splice 方法重新排序
|
const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
|
routeItems.value.splice(evt.newIndex, 0, moveItem);
|
updateDragSort();
|
},
|
});
|
|
// 调试:打印容器和实例,确认绑定成功
|
console.log("步骤条拖拽容器:", stepsList);
|
console.log("Sortable实例:", stepsSortable);
|
}
|
};
|
|
const handleViewChange = () => {
|
destroySortable();
|
// 延迟初始化,确保视图切换后DOM完全渲染
|
nextTick(() => {
|
setTimeout(() => initSortable(), 100);
|
});
|
};
|
|
onMounted(() => {
|
findProcessRouteItems();
|
findProcessList();
|
});
|
|
onUnmounted(() => {
|
destroySortable();
|
});
|
|
defineExpose({
|
closeModal,
|
handleSubmit,
|
isShow,
|
});
|
</script>
|
|
<style scoped>
|
:deep(.sortable-ghost) {
|
opacity: 0.6;
|
background-color: #f5f7fa !important;
|
}
|
|
:deep(.el-table__row) {
|
transition: background-color 0.2s;
|
}
|
|
:deep(.el-table__row:hover) {
|
background-color: #f9fafc !important;
|
}
|
|
:deep(.el-card__footer) {
|
padding: 0 !important;
|
}
|
|
.operate-button {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
/* 修改:自定义步骤条容器样式 */
|
.custom-steps {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: flex-start;
|
gap: 20px;
|
min-height: 100px;
|
}
|
|
/* 修改:自定义步骤项样式 */
|
.custom-step {
|
cursor: move !important;
|
padding: 8px;
|
position: relative;
|
transition: all 0.2s ease;
|
flex: 0 0 auto;
|
min-width: 220px;
|
touch-action: none;
|
}
|
|
/* 拖拽悬浮样式,提示可拖拽 */
|
.custom-step:hover {
|
background-color: rgba(64, 158, 255, 0.05);
|
transform: translateY(-2px);
|
}
|
|
.sortable-ghost {
|
opacity: 0.4;
|
background-color: #f5f7fa !important;
|
border: 2px dashed #409eff;
|
margin: 10px;
|
transform: scale(1.02);
|
}
|
|
.sortable-fallback {
|
opacity: 0.9;
|
background-color: #f5f7fa;
|
border: 1px solid #409eff;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
transform: rotate(2deg);
|
margin: 10px;
|
}
|
|
.step-card {
|
cursor: move !important;
|
transition: box-shadow 0.2s ease;
|
user-select: none;
|
-webkit-user-select: none;
|
pointer-events: auto;
|
margin: 10px;
|
height: 240px;
|
}
|
|
.step-card:hover {
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
}
|
|
.step-content {
|
width: 220px;
|
user-select: none;
|
}
|
|
.step-card-content {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.step-card-footer {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
padding: 10px;
|
}
|
|
/* 自定义序号样式优化 */
|
.step-number {
|
font-weight: bold;
|
text-align: center;
|
width: 36px;
|
height: 36px;
|
line-height: 36px;
|
margin: 0 auto 10px;
|
background: #409eff;
|
color: #fff;
|
border-radius: 50%;
|
font-size: 14px;
|
}
|
</style>
|