Files
td_official/src/views/patch/index.vue
2025-08-29 14:47:58 +08:00

669 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="p-2">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<!-- 任务列表卡片 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['patch:patch:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['patch:patch:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 任务列表表格 -->
<el-table v-loading="loading" :data="masterList" @selection-change="handleSelectionChange" border>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="执行人名称" align="center" prop="slaveName" />
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="任务描述" align="center" prop="describe" />
<el-table-column label="计划完成时间" align="center" prop="pcd" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.pcd, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="实际完成时间" align="center" prop="act" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.act, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- 完成进度列点击打开进度详情 -->
<el-table-column prop="completionProgress" label="完成进度" align="center">
<template #default="scope">
<el-button type="text" @click="showProgressDetail(scope.row.id)" class="text-primary">
{{ scope.row.completionProgress || '未设置' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="taskStatus">
<template #default="scope">
<span
:class="[Number(scope.row.taskStatus) === 0 ? 'text-warning' : Number(scope.row.taskStatus) === 1 ? 'text-success' : 'text-danger']"
>
{{
Number(scope.row.taskStatus) === 0 ? '未完成' : Number(scope.row.taskStatus) === 1 ? '已完成' : `未知状态(${scope.row.taskStatus})`
}}
</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 #default="scope">
<!-- <el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['patch:patch:edit']"></el-button>
</el-tooltip> -->
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['patch:patch:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 进度详情弹窗 -->
<el-dialog :title="`进度详情(当前总进度:${currentTotalProgress}%`" v-model="progressDialogVisible" width="1000px" destroy-on-close>
<div class="mb-4">
<!-- v-hasPermi="['patch:progress:add']" -->
<el-button type="primary" plain icon="Plus" size="small" @click="handleProgressAdd()"> 新增进度 </el-button>
</div>
<el-table v-loading="progressLoading" :data="progressList" border empty-text="暂无进度数据" style="width: 100%">
<el-table-column prop="slaveName" label="执行人姓名" align="center" width="150" />
<el-table-column prop="progress" label="进度" align="center" width="120" />
<el-table-column prop="remark" label="备注" align="center" />
<el-table-column prop="updateTime" label="更新时间" align="center" width="200">
<template #default="scope">
<span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template #default="scope">
<el-tooltip content="修改进度" placement="top">
<el-button
link
type="primary"
icon="Edit"
size="small"
@click="handleProgressUpdate(scope.row)"
v-hasPermi="['patch:progress:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除进度" placement="top">
<el-button
link
type="primary"
icon="Delete"
size="small"
@click="handleProgressDelete(scope.row)"
v-hasPermi="['patch:progress:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 新增/修改进度弹窗 -->
<el-dialog :title="progressDialog.title" v-model="progressDialog.visible" width="500px" append-to-body destroy-on-close>
<el-form ref="progressFormRef" :model="progressForm" :rules="progressRules" label-width="120px">
<el-input v-model="progressForm.ordersId" placeholder="关联的任务ID" readonly style="color: #666; background: #f5f7fa" type="hidden" />
<el-input v-model="progressForm.projectId" type="hidden" />
<el-input v-model="progressForm.slaveId" placeholder="请输入执行人ID" v-if="false" />
<el-form-item label="进度" prop="progress">
<el-input v-model="progressForm.progress" placeholder="请输入进度0-100之间的数字" type="number" @input="handleProgressInput" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="progressForm.remark" type="textarea" placeholder="请输入备注" rows="3" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="progressBtnLoading" type="primary" @click="submitProgressForm"> </el-button>
<el-button @click="cancelProgressForm"> </el-button>
</div>
</template>
</el-dialog>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 新增/修改任务弹窗 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body destroy-on-close>
<el-form ref="masterFormRef" :model="form" :rules="rules" label-width="100px">
<el-input v-model="form.projectId" placeholder="请输入项目ID" :readonly="!!currentProjectId" :disabled="!!currentProjectId" v-if="false" />
<template #help>
<span v-if="currentProjectId" class="text-success">已自动关联当前选中项目</span>
</template>
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="form.taskName" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="执行人姓名" prop="userId">
<el-select v-model="form.userId" placeholder="请选择执行人姓名" clearable style="width: 100%">
<el-option v-for="item in slaveOptions" :key="item.id" :label="item.nickName" :value="item.userId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="计划完成时间" prop="pcd">
<el-date-picker clearable v-model="form.pcd" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择计划完成时间" />
</el-form-item>
<el-form-item label="实际完成时间" prop="act">
<el-date-picker clearable v-model="form.act" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择实际完成时间" />
</el-form-item>
<el-form-item label="任务描述" prop="describe">
<el-input v-model="form.describe" type="textarea" placeholder="请输入任务描述" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Master" lang="ts">
// 1. 导入接口
import {
listMaster,
getMaster,
delMaster,
addMaster,
updateMaster,
getProgressDetail,
addProgress,
editProgress,
deleteProgress,
getUserName
} from '@/api/patch';
import { MasterVO, MasterQuery, MasterForm } from '@/api/patch/types';
// 2. 导入Vue核心依赖
import { getCurrentInstance, ComponentInternalInstance, onMounted, ref, reactive, toRefs, nextTick, computed, watch } from 'vue';
import type { ElFormInstance } from 'element-plus';
import { useUserStoreHook } from '@/store/modules/user';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目ID
const currentProject = computed(() => userStore.selectedProject);
const currentProjectId = computed(() => currentProject.value?.id);
// 1. 调整类型定义,匹配接口返回的字段
const slaveOptions = ref([]);
const userMap = new Map();
async function initSlaveOptions() {
try {
const response = await getUserName();
console.log(response);
slaveOptions.value = response.data;
slaveOptions.value.forEach((item) => {
userMap.set(item.userId, item.nickName);
});
} catch (error) {
slaveOptions.value = []; // 错误时重置为空数组
}
}
initSlaveOptions();
// 3. 类型声明
interface DialogOption {
visible: boolean;
title: string;
}
interface PageData<F, Q> {
form: F;
queryParams: Q;
rules: Record<string, any[]>;
}
interface ProgressDetail {
id?: string | number;
ordersId: string | number;
slaveId: string | number;
slaveName: string;
progress: string;
remark?: string;
updateTime?: string;
projectId?: string | number;
}
interface ProgressForm extends Omit<ProgressDetail, 'updateTime'> {}
// 4. 获取组件实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 5. 环境变量获取
const isDev = import.meta.env.MODE === 'development';
// 6. 状态管理
const progressDialogVisible = ref(false);
const progressList = ref<ProgressDetail[]>([]);
const progressLoading = ref(false);
const currentMasterId = ref<string | number>('');
const currentTotalProgress = ref<number>(0); // 当前任务的所有执行人进度总和
const progressDialog = reactive<DialogOption>({ visible: false, title: '' });
const progressFormRef = ref<ElFormInstance>();
const progressForm = reactive<ProgressForm>({
projectId: currentProjectId.value,
id: undefined,
ordersId: '',
slaveId: '',
slaveName: '',
progress: '',
remark: ''
});
const progressBtnLoading = ref(false);
const progressRules = reactive<Record<string, any[]>>({
ordersId: [{ required: true, message: '关联任务ID不能为空', trigger: 'blur' }],
slaveId: [{ required: true, message: '请输入执行人ID', trigger: 'blur' }],
slaveName: [{ required: true, message: '请输入执行人姓名', trigger: 'blur' }],
progress: [{ required: true, message: '请输入进度', trigger: 'blur' }]
});
const masterList = ref<MasterVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const masterFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({ visible: false, title: '' });
const initFormData = {
id: undefined,
projectId: currentProjectId.value,
taskName: undefined,
describe: undefined,
pcd: undefined,
act: undefined,
completionProgress: undefined,
taskStatus: undefined,
remark: undefined,
userId: '',
userName: ''
};
const data = reactive<PageData<MasterForm, MasterQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProjectId.value, // 默认查询当前项目的任务
taskName: undefined,
describe: undefined,
pcd: undefined,
act: undefined,
completionProgress: undefined,
taskStatus: undefined,
params: {}
},
rules: {
projectId: [{ required: true, message: '请输入项目ID', trigger: 'blur' }],
userId: [{ required: true, message: '请选择执行人', trigger: 'blur' }],
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
completionProgress: [{ required: true, message: '请输入完成进度', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 监听当前项目ID变化自动更新相关数据
watch(currentProjectId, (newVal) => {
// 更新查询参数中的项目ID
queryParams.value.projectId = newVal;
// 更新表单中的项目ID如果是新增状态
if (!form.value.id) {
form.value.projectId = newVal;
}
// 更新进度表单中的项目ID
progressForm.projectId = newVal;
// 重新加载任务列表
getList();
});
// 7. 核心方法
const getList = async () => {
loading.value = true;
try {
const res = await listMaster(queryParams.value);
masterList.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取任务列表失败', error);
proxy?.$modal.msgError('获取任务列表失败');
masterList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
const showProgressDetail = async (masterId: string | number) => {
if (!masterId || (typeof masterId === 'string' && masterId.trim() === '')) {
proxy?.$modal.msgError('无效的任务ID无法加载进度详情');
return;
}
currentMasterId.value = masterId;
progressDialogVisible.value = true;
progressLoading.value = true;
progressList.value = [];
currentTotalProgress.value = 0; // 重置总和
try {
const res = await getProgressDetail(masterId);
progressList.value = res.data || [];
// 计算所有执行人的进度总和
currentTotalProgress.value = progressList.value.reduce((total, item) => {
const progressNum = Number(item.progress) || 0;
return total + progressNum;
}, 0);
} catch (error) {
console.error('获取进度详情失败', error);
proxy?.$modal.msgError('获取进度详情失败');
} finally {
progressLoading.value = false;
}
};
const resetProgressForm = () => {
progressForm.id = undefined;
progressForm.slaveId = '';
progressForm.slaveName = '';
progressForm.progress = '';
progressForm.remark = '';
progressForm.projectId = currentProjectId.value; // 重置时关联当前项目
progressFormRef.value?.resetFields();
nextTick(() => {
progressForm.ordersId = currentMasterId.value;
});
};
const handleProgressAdd = () => {
if (!currentMasterId.value) {
proxy?.$modal.msgError('请先从任务列表打开「进度详情」,再新增进度');
return;
}
resetProgressForm();
progressDialog.visible = true;
progressDialog.title = '新增进度';
};
const handleProgressUpdate = (row: ProgressDetail) => {
progressForm.id = row.id;
progressForm.ordersId = row.ordersId;
progressForm.slaveId = row.slaveId;
progressForm.slaveName = row.slaveName;
progressForm.progress = row.progress;
progressForm.remark = row.remark || '';
progressForm.projectId = row.projectId || currentProjectId.value; // 保持项目ID一致
progressDialog.visible = true;
progressDialog.title = '修改进度';
};
const submitProgressForm = () => {
progressFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
if (!progressForm.ordersId) {
proxy?.$modal.msgError('关联任务ID丢失请关闭弹窗重新打开进度详情');
progressBtnLoading.value = false;
return;
}
// 确保进度总和不超过100
const newProgress = Number(progressForm.progress) || 0;
let expectedTotal = 0;
if (progressForm.id) {
// 编辑场景:总和 = 当前总和 - 旧进度 + 新进度
const oldProgressItem = progressList.value.find((item) => item.id === progressForm.id);
const oldProgress = oldProgressItem ? Number(oldProgressItem.progress) || 0 : 0;
expectedTotal = currentTotalProgress.value - oldProgress + newProgress;
} else {
// 新增场景:总和 = 当前总和 + 新进度
expectedTotal = currentTotalProgress.value + newProgress;
}
if (expectedTotal > 100) {
proxy?.$modal.msgError(`进度总和不能超过100%,当前预计总和为${expectedTotal}%`);
progressBtnLoading.value = false;
return;
}
progressBtnLoading.value = true;
try {
if (progressForm.id) {
await editProgress(progressForm);
proxy?.$modal.msgSuccess('修改进度成功');
} else {
await addProgress(progressForm);
proxy?.$modal.msgSuccess('新增进度成功');
}
progressDialog.visible = false;
await showProgressDetail(currentMasterId.value); // 刷新进度列表
getList(); // 同步刷新任务列表
} catch (error) {
console.error(`${progressForm.id ? '修改' : '新增'}进度失败`, error);
proxy?.$modal.msgError(`${progressForm.id ? '修改' : '新增'}进度失败`);
} finally {
progressBtnLoading.value = false;
}
});
};
const cancelProgressForm = () => {
progressDialog.visible = false;
getList();
};
const handleProgressDelete = async (row: ProgressDetail) => {
if (!row.id) return;
try {
await proxy?.$modal.confirm(`是否确认删除该进度记录?`);
await deleteProgress(row.id);
proxy?.$modal.msgSuccess('删除进度成功');
await showProgressDetail(currentMasterId.value);
getList();
} catch (error) {
console.error('删除进度失败', error);
if (error !== 'cancel') {
proxy?.$modal.msgError('删除进度失败');
}
}
};
const cancel = () => {
form.value = { ...initFormData, projectId: currentProjectId.value };
masterFormRef.value?.resetFields();
dialog.visible = false;
getList();
};
const reset = () => {
form.value = { ...initFormData, projectId: currentProjectId.value };
masterFormRef.value?.resetFields();
};
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
const resetQuery = () => {
queryFormRef.value?.resetFields();
// 重置查询时保留当前项目ID
queryParams.value.projectId = currentProjectId.value;
handleQuery();
};
const handleSelectionChange = (selection: MasterVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = selection.length === 0;
};
const handleAdd = () => {
reset();
// 新增任务时自动填充当前项目ID
form.value.projectId = currentProjectId.value;
dialog.visible = true;
dialog.title = '添加派单';
};
const handleUpdate = async (row?: MasterVO) => {
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning('请选择需要修改的任务');
return;
}
try {
const res = await getMaster(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改派单';
} catch (error) {
console.error('获取任务详情失败', error);
proxy?.$modal.msgError('获取任务详情失败');
}
};
const submitForm = () => {
masterFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
form.value.userName = userMap.get(form.value.userId);
try {
// 确保提交时使用当前项目ID
if (!form.value.id && currentProjectId.value) {
form.value.projectId = currentProjectId.value;
}
if (form.value.id) await updateMaster(form.value);
else await addMaster(form.value);
proxy?.$modal.msgSuccess(`${form.value.id ? '修改' : '新增'}成功`);
dialog.visible = false;
await getList();
} catch (error) {
console.error(`${form.value.id ? '修改' : '新增'}任务失败`, error);
proxy?.$modal.msgError(`${form.value.id ? '修改' : '新增'}任务失败`);
} finally {
buttonLoading.value = false;
}
});
};
const handleDelete = async (row?: MasterVO) => {
const _ids = row?.id || ids.value;
if (!_ids || (_ids instanceof Array && _ids.length === 0)) {
proxy?.$modal.msgWarning('请选择需要删除的任务');
return;
}
try {
await proxy?.$modal.confirm(`是否确认删除选中的${row ? '任务' : '任务们'}`);
await delMaster(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除任务失败', error);
if (error !== 'cancel') proxy?.$modal.msgError('删除任务失败');
} finally {
loading.value = false;
}
};
const handleProgressInput = () => {
let value = progressForm.progress;
// 清除非数字字符(保留小数点)
if (typeof value === 'string') {
value = value.replace(/[^\d.]/g, '');
// 确保只有一个小数点
const dotIndex = value.indexOf('.');
if (dotIndex !== -1) {
value = value.slice(0, dotIndex + 1) + value.slice(dotIndex + 1).replace(/\./g, '');
}
}
// 转换为数字处理
let num = Number(value);
// 限制范围在0-100
if (num < 0) num = 0;
if (num > 100) num = 100;
// 处理空值情况
if (value === '' || isNaN(num)) {
progressForm.progress = '';
} else {
progressForm.progress = num.toString();
}
};
const handleExport = () => {
proxy?.download('patch/patch/export', { ...queryParams.value }, `任务列表_${new Date().getTime()}.xlsx`);
};
onMounted(() => {
getList();
});
// 监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style scoped lang="scss">
.mb-4 {
margin-bottom: 16px;
}
.el-table .small-padding .cell {
padding: 0 5px;
}
.el-table .fixed-width {
width: 120px !important;
}
.el-table-column .el-button--text + .el-button--text {
margin-left: 10px;
}
</style>