<template>
|
<el-dialog
|
:title="title"
|
v-model="visible"
|
width="1000px"
|
append-to-body
|
@close="handleClose"
|
>
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="0">
|
<!-- 顶部基础信息 -->
|
<div class="base-info-row">
|
<div class="info-item">
|
<span class="item-label required">名称</span>
|
<el-input
|
v-model="form.name"
|
placeholder="请输入名称"
|
maxlength="10"
|
show-word-limit
|
style="width: 220px"
|
/>
|
</div>
|
<div class="info-item">
|
<span class="item-label">备注</span>
|
<el-input
|
v-model="form.description"
|
placeholder="请输入备注"
|
maxlength="20"
|
show-word-limit
|
style="width: 220px"
|
/>
|
</div>
|
<div class="info-item">
|
<span class="item-label">附件</span>
|
<el-upload
|
:action="uploadUrl"
|
:headers="uploadHeaders"
|
:on-success="handleUploadSuccess"
|
:on-remove="handleRemove"
|
:file-list="form.attachmentList"
|
name="files"
|
multiple
|
>
|
<el-button type="primary">上传附件</el-button>
|
</el-upload>
|
</div>
|
</div>
|
|
<!-- 步骤配置表格 -->
|
<p class="top-tip">请按照顺序配置项目阶段,拖拽<b>步骤</b>排序即可</p>
|
<div class="step-table-container">
|
<el-table
|
:data="form.savePlanNodeList"
|
border
|
style="width: 100%"
|
row-key="id"
|
class="drag-table"
|
>
|
<el-table-column label="步骤" width="80" align="center" class-name="drag-handle">
|
<template #default="scope">
|
<div class="step-index" style="cursor: move;">
|
{{ scope.$index + 1 }}
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="阶段名称" min-width="150">
|
<template #header>
|
<span class="required-star">*</span> 阶段名称
|
</template>
|
<template #default="scope">
|
<el-form-item
|
:prop="'savePlanNodeList.' + scope.$index + '.name'"
|
:rules="[{ required: true, message: '请输入阶段名称', trigger: 'blur' }]"
|
>
|
<el-input v-model="scope.row.name" placeholder="请输入" />
|
</el-form-item>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="负责人" width="180">
|
<template #header>
|
<span class="required-star">*</span> 负责人
|
</template>
|
<template #default="scope">
|
<el-form-item
|
:prop="'savePlanNodeList.' + scope.$index + '.leaderId'"
|
:rules="[{ required: true, message: '请选择负责人', trigger: 'change' }]"
|
>
|
<el-select
|
v-model="scope.row.leaderId"
|
placeholder="测试"
|
@change="(val) => handleLeaderChange(val, scope.row)"
|
>
|
<el-option
|
v-for="item in userOptions"
|
:key="item.userId"
|
:label="item.nickName"
|
:value="item.userId"
|
/>
|
</el-select>
|
</el-form-item>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="预计工期 (天)" width="150">
|
<template #header>
|
预计工期 (天)
|
<el-tooltip content="完成该阶段预计需要的天数" placement="top">
|
<el-icon class="info-icon"><QuestionFilled /></el-icon>
|
</el-tooltip>
|
</template>
|
<template #default="scope">
|
<el-input-number
|
v-model="scope.row.estimatedDuration"
|
:min="0"
|
controls-position="right"
|
style="width: 100%"
|
/>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="工时单价" width="120">
|
<template #default="scope">
|
<el-input v-model="scope.row.hourlyRate" placeholder="请输入" />
|
</template>
|
</el-table-column>
|
|
<el-table-column label="作业内容" min-width="150">
|
<template #default="scope">
|
<el-input v-model="scope.row.workContent" placeholder="请输入" />
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="add-row-btn" @click="addStep">
|
<el-icon><Plus /></el-icon> 新增一行
|
</div>
|
</div>
|
</el-form>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="visible = false">取消</el-button>
|
<el-button type="primary" @click="submitForm">提交</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, watch, onMounted, nextTick } from 'vue';
|
import { Plus, QuestionFilled } from '@element-plus/icons-vue';
|
import { userListNoPageByTenantId } from '@/api/system/user';
|
import { ElMessage } from 'element-plus';
|
import { getToken } from '@/utils/auth';
|
import Sortable from 'sortablejs';
|
|
const props = defineProps({
|
modelValue: Boolean,
|
title: String,
|
data: Object
|
});
|
|
const emit = defineEmits(['update:modelValue', 'submit']);
|
|
const visible = ref(false);
|
const formRef = ref(null);
|
const userOptions = ref([]);
|
const uploadHeaders = { Authorization: "Bearer " + getToken() };
|
// 上传地址
|
const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
|
let sortable = null;
|
|
const form = ref({
|
id: undefined,
|
name: '',
|
description: '',
|
attachmentIds: [],
|
attachmentList: [],
|
savePlanNodeList: []
|
});
|
|
const rules = {
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
};
|
|
// 监听弹窗显示/隐藏
|
watch(() => props.modelValue, (val) => {
|
visible.value = val;
|
if (val) {
|
if (props.data) {
|
// 编辑模式 - 回显数据
|
form.value = {
|
id: props.data.id,
|
name: props.data.name,
|
description: props.data.description,
|
attachmentIds: [],
|
attachmentList: props.data.attachmentList || [],
|
savePlanNodeList: []
|
};
|
|
// 回显附件ID
|
if (form.value.attachmentList && form.value.attachmentList.length > 0) {
|
form.value.attachmentIds = form.value.attachmentList.map(item => item.id);
|
}
|
|
// 回显步骤节点
|
if (props.data.planNodeList && props.data.planNodeList.length > 0) {
|
form.value.savePlanNodeList = props.data.planNodeList.map(node => ({
|
id: node.id,
|
projectManagementPlanId: node.projectManagementPlanId,
|
sort: node.sort,
|
name: node.name,
|
leaderId: node.leaderId,
|
leaderName: node.leaderName,
|
estimatedDuration: node.estimatedDuration,
|
hourlyRate: node.hourlyRate,
|
workContent: node.workContent
|
}));
|
} else {
|
form.value.savePlanNodeList = [createDefaultNode()];
|
}
|
} else {
|
// 新增模式
|
resetForm();
|
}
|
// 初始化拖拽
|
nextTick(() => {
|
initSortable();
|
});
|
}
|
});
|
|
watch(visible, (val) => {
|
emit('update:modelValue', val);
|
});
|
|
/** 初始化拖拽 */
|
function initSortable() {
|
const el = document.querySelector('.drag-table .el-table__body-wrapper tbody');
|
if (!el) return;
|
|
if (sortable) {
|
sortable.destroy();
|
}
|
|
sortable = Sortable.create(el, {
|
handle: '.drag-handle',
|
animation: 150,
|
onEnd: ({ newIndex, oldIndex }) => {
|
const targetRow = form.value.savePlanNodeList.splice(oldIndex, 1)[0];
|
form.value.savePlanNodeList.splice(newIndex, 0, targetRow);
|
}
|
});
|
}
|
|
/** 创建默认节点对象 */
|
function createDefaultNode() {
|
return {
|
name: '',
|
leaderId: null,
|
leaderName: null,
|
estimatedDuration: null,
|
hourlyRate: null,
|
workContent: null
|
};
|
}
|
|
/** 重置表单 */
|
function resetForm() {
|
form.value = {
|
id: undefined,
|
name: '',
|
description: '',
|
attachmentIds: [],
|
attachmentList: [],
|
savePlanNodeList: [createDefaultNode()]
|
};
|
if (formRef.value) {
|
formRef.value.resetFields();
|
}
|
}
|
|
/** 获取用户列表 */
|
async function getUserList() {
|
try {
|
const res = await userListNoPageByTenantId();
|
if (res.code === 200) {
|
userOptions.value = res.data || [];
|
}
|
} catch (error) {
|
console.error('获取用户列表失败:', error);
|
}
|
}
|
|
/** 处理负责人变化 */
|
function handleLeaderChange(val, row) {
|
const user = userOptions.value.find(u => u.userId === val);
|
if (user) {
|
row.leaderName = user.nickName;
|
}
|
}
|
|
/** 处理文件上传成功 */
|
function handleUploadSuccess(response, file, fileList) {
|
if (response.code === 200) {
|
ElMessage.success('上传成功');
|
// 假设后端返回的数据结构中包含文件ID和URL等信息
|
// 这里需要根据实际接口返回结构进行调整
|
// 通常 response.data 包含文件信息
|
const newFile = response.data;
|
if (newFile && newFile.id) {
|
form.value.attachmentIds.push(newFile.id);
|
form.value.attachmentList.push({
|
name: file.name,
|
url: newFile.url,
|
id: newFile.id
|
});
|
}
|
} else {
|
ElMessage.error(response.msg || '上传失败');
|
}
|
}
|
|
/** 处理文件移除 */
|
function handleRemove(file) {
|
const index = form.value.attachmentList.findIndex(item => item.name === file.name);
|
if (index !== -1) {
|
const fileId = form.value.attachmentList[index].id;
|
form.value.attachmentList.splice(index, 1);
|
const idIndex = form.value.attachmentIds.indexOf(fileId);
|
if (idIndex !== -1) {
|
form.value.attachmentIds.splice(idIndex, 1);
|
}
|
}
|
}
|
|
/** 添加步骤 */
|
function addStep() {
|
form.value.savePlanNodeList.push(createDefaultNode());
|
}
|
|
/** 移除步骤 */
|
function removeStep(index) {
|
if (form.value.savePlanNodeList.length <= 1) {
|
ElMessage.warning('至少保留一个步骤');
|
return;
|
}
|
form.value.savePlanNodeList.splice(index, 1);
|
}
|
|
/** 移动步骤 */
|
function moveStep(index, direction) {
|
const targetIndex = index + direction;
|
if (targetIndex < 0 || targetIndex >= form.value.savePlanNodeList.length) return;
|
|
const list = form.value.savePlanNodeList;
|
const temp = list[index];
|
list[index] = list[targetIndex];
|
list[targetIndex] = temp;
|
}
|
|
/** 提交表单 */
|
async function submitForm() {
|
if (!formRef.value) return;
|
|
try {
|
const valid = await formRef.value.validate();
|
if (valid) {
|
// 提交前自动填充 sort 字段,按当前数组顺序排序
|
form.value.savePlanNodeList.forEach((node, index) => {
|
node.sort = index;
|
});
|
emit('submit', form.value);
|
}
|
} catch (error) {
|
console.error('表单验证失败:', error);
|
}
|
}
|
|
/** 关闭弹窗 */
|
function handleClose() {
|
resetForm();
|
}
|
|
onMounted(() => {
|
getUserList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.base-info-row {
|
display: flex;
|
gap: 40px;
|
margin-bottom: 25px;
|
padding: 0 10px;
|
|
.info-item {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
|
.item-label {
|
font-size: 14px;
|
color: #606266;
|
white-space: nowrap;
|
|
&.required::before {
|
content: '*';
|
color: #f56c6c;
|
margin-right: 4px;
|
}
|
}
|
}
|
}
|
|
.step-table-container {
|
padding: 0 10px;
|
|
:deep(.el-form-item) {
|
margin-bottom: 0;
|
}
|
|
.required-star {
|
color: #f56c6c;
|
margin-right: 4px;
|
}
|
|
.info-icon {
|
font-size: 14px;
|
color: #909399;
|
margin-left: 4px;
|
cursor: pointer;
|
}
|
|
.add-row-btn {
|
margin-top: 15px;
|
height: 40px;
|
border: 1px dashed #dcdfe6;
|
border-radius: 4px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
color: #409eff;
|
cursor: pointer;
|
font-size: 14px;
|
transition: all 0.3s;
|
|
&:hover {
|
border-color: #409eff;
|
background-color: #f0f7ff;
|
}
|
}
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: flex-end;
|
gap: 15px;
|
padding-top: 10px;
|
}
|
.top-tip {
|
|
font-size: 14px;
|
color: #606266;
|
margin:0 0 10px 10px;
|
}
|
</style>
|