进度填报大屏界面基本功能

This commit is contained in:
Teo
2025-05-30 19:51:35 +08:00
parent 0a2d8b06f8
commit 6758437892
22 changed files with 2028 additions and 128 deletions

View File

@ -1,8 +1,16 @@
<template>
<div class="daily_paper">
<el-dialog v-model="isShowDialog" @close="onCancel" width="1000px" :close-on-click-modal="false" :destroy-on-close="true">
<el-dialog
v-model="isShowDialog"
@close="onCancel"
width="1000px"
:close-on-click-modal="false"
:destroy-on-close="true"
:lock-scroll="false"
:append-to-body="false"
>
<template #header>
<div v-drag="['.daily_paper .el-dialog', '.daily_paper .el-dialog__header']" style="font-size: 18px">{{ infoDetail.name }} 日报填写</div>
<div style="font-size: 18px">{{ infoDetail.name }} 日报填写</div>
</template>
<div class="box">
<div class="box-left">
@ -197,6 +205,32 @@ const clickOpen = (row: any) => {
defineExpose({ openDialog });
const emit = defineEmits(['getProgressList']);
let scrollTop = 0;
watch(
() => isShowDialog.value,
(visible) => {
if (visible) {
scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.width = '100%';
document.body.style.paddingRight = `${scrollbarWidth}px`; // 👈 补偿滚动条宽度
} else {
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
document.body.style.paddingRight = ''; // 👈 恢复
nextTick(() => {
window.scrollTo(0, scrollTop);
});
}
}
);
</script>
<style lang="scss" scoped>

View File

@ -1,6 +1,14 @@
<template>
<div class="daily-paper-count">
<el-dialog v-model="isShowDialog" @close="onCancel" width="65vw" :close-on-click-modal="false" :destroy-on-close="true">
<el-dialog
v-model="isShowDialog"
@close="onCancel"
width="70vw"
:close-on-click-modal="false"
:destroy-on-close="true"
:lock-scroll="false"
:append-to-body="false"
>
<template #header>
<div v-drag="['.daily-paper-count .el-dialog', '.daily-paper-count .el-dialog__header']" style="font-size: 18px">
{{ infoDetail.name }} 日报填写
@ -24,23 +32,23 @@
<div style="margin-left: 45px" m="4">
<el-table
:border="true"
:data="propsRow.detail"
:data="propsRow.detailList"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
highlight-current-row
>
<el-table-column label="序号" type="index" width="60px" />
<el-table-column label="计划日期" prop="date" />
<el-table-column label="数量" prop="planNum" width="60" />
<el-table-column label="完成量" prop="finishedNum" width="60" />
<el-table-column label="AI填报" prop="autoFill" width="60" />
<el-table-column label="操作" class-name="small-padding" width="200px" fixed="right">
<el-table-column label="数量" prop="planNumber" width="60" />
<el-table-column label="完成量" prop="finishedNumber" width="70" />
<el-table-column label="AI填报" prop="aiFill" width="70" />
<el-table-column label="操作" class-name="small-padding" width="170px" fixed="right">
<template #default="{ row: scopeRow, $index }">
<el-button type="primary" link @click="handleDayAdd(scopeRow, propsRow)">
<el-icon><ele-Plus /></el-icon>日报
<el-icon><Plus /></el-icon>日报
</el-button>
<el-button type="success" link @click="handleView(scopeRow, propsRow)">
<el-icon><ele-View /></el-icon>查看
<el-icon><View /></el-icon>查看
</el-button>
</template>
</el-table-column>
@ -49,8 +57,8 @@
</template>
</el-table-column>
<el-table-column label="序号" type="index" :index="indexMethod" width="60px" />
<el-table-column label="计划数量" prop="planNum" min-width="100px" />
<el-table-column label="完成数量" prop="finishedNum" min-width="100px" />
<el-table-column label="计划数量" prop="planNumber" min-width="100px" />
<el-table-column label="完成数量" prop="finishedNumber" min-width="100px" />
<el-table-column label="延期量" min-width="100px">
<template #default="{ row: scopeRow }">
<el-tag :type="filterW(scopeRow) ? 'danger' : 'success'">
@ -58,10 +66,10 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="AI填报" prop="autoFill" min-width="100px" />
<el-table-column label="AI填报" prop="aiFill" min-width="100px" />
<el-table-column label="开始时间" min-width="100px">
<template #default="{ row: scopeRow }">
<span>{{ scopeRow.startAt.split(' ')[0] }}</span>
<span>{{ scopeRow.startDate.split(' ')[0] }}</span>
</template>
</el-table-column>
</el-table>
@ -73,13 +81,14 @@
@pagination="getWorkList"
layout="total, sizes, prev, pager, next"
:isSmall="5"
class="float-left mt-4.5!"
/>
</div>
<div class="box_right" v-if="showDayWork">
<div class="time_submit">
<span>{{ formDetail.submitTime }}</span>
<el-button type="primary" :disabled="!checkedList.length || flag" @click="onUploadDaily" size="large">
<el-icon><ele-Upload /></el-icon>提交日报
<el-button type="primary" :disabled="!checkedList.length || flag" @click="onUploadDaily" size="small">
<el-icon><Upload /></el-icon>提交日报
</el-button>
</div>
<el-table
@ -97,14 +106,14 @@
<el-table-column label="状态" align="center" min-width="100px">
<template #default="{ row: scopeRow }">
<el-tag :type="typeList[scopeRow.status]">
{{ filterStatus(scopeRow.status) }}
{{ filterStatus(Number(scopeRow.status)) }}
</el-tag>
</template>
</el-table-column>
</el-table-column>
</el-table>
<pagination
v-show="detailTotal > 0"
v-show="1"
:total="detailTotal"
v-model:page="detailQueryParams.pageNum"
v-model:limit="detailQueryParams.pageSize"
@ -116,8 +125,8 @@
<div class="box_right" v-else>
<div class="time_submit">
<span>{{ formDetail.submitTime }}</span>
<el-button type="danger" :disabled="single" @click="handleRemove(null)" size="large">
<el-icon><ele-SemiSelect /></el-icon>批量移除
<el-button type="danger" :disabled="single" @click="handleRemove(null)" size="small">
<el-icon><Minus /></el-icon>批量移除
</el-button>
</div>
<el-table
@ -134,14 +143,14 @@
<el-table-column label="编号" align="center" prop="name" min-width="100px" />
<el-table-column label="填报方式" align="center" prop="status" min-width="100px">
<template #default="{ row: scopeRow }">
<span v-if="scopeRow.status === 2">手动填报</span>
<span v-if="scopeRow.status === 3">AI识别</span>
<span v-if="scopeRow.finishType === '1'">手动填报</span>
<span v-if="scopeRow.finishType === '2'">AI识别</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="100px">
<template #default="{ row: scopeRow }">
<el-button type="danger" link @click="handleRemove(scopeRow)">
<el-icon><ele-SemiSelect /></el-icon>移除
<el-icon><Minus /></el-icon>移除
</el-button>
</template>
</el-table-column>
@ -175,7 +184,7 @@ import { workScheduleListQuery } from '@/api/progress/plan/types';
// 响应式状态
const state = reactive<{
expandRowKeys: number[];
expandRowKeys: string[];
loading: boolean;
isShowDialog: boolean;
queryParams: workScheduleListQuery;
@ -186,7 +195,7 @@ const state = reactive<{
workId: string;
id: string;
submitTime: string;
finishedNum: number;
finishedNumber: number;
};
detialList: any[];
detailTotal: number;
@ -225,12 +234,12 @@ const state = reactive<{
workId: '',
id: '',
submitTime: '选择日期',
finishedNum: 0
finishedNumber: 0
},
detialList: [],
detailTotal: 0,
detailQueryParams: {
pageSize: 20,
pageSize: 10,
pageNum: 1
},
loading1: false,
@ -242,7 +251,7 @@ const state = reactive<{
detialWordList: [],
detailTotalWork: 0,
detailQueryParamsWork: {
pageSize: 20,
pageSize: 10,
pageNum: 1
},
single: true,
@ -318,15 +327,13 @@ const resetForm = (bool: boolean) => {
const getPvModuleList = () => {
loading1.value = true;
pvModuleList({
workId: formDetail.value.workId,
...detailQueryParams.value,
type: infoDetail.value.work_type,
status: 0
id: formDetail.value.id,
...detailQueryParams.value
}).then((res: any) => {
loading1.value = false;
if (res.code === 0) {
detialList.value = res.data.list;
detailTotal.value = res.data.total;
if (res.code === 200) {
detialList.value = res.rows;
detailTotal.value = res.total;
}
});
};
@ -335,13 +342,13 @@ const getPvModuleList = () => {
const getWorkList = (bool = false) => {
loading.value = true;
workScheduleList(queryParams.value).then((res: any) => {
if (res.code === 0) {
state.tableData = res.data.list.map((item: any, i: number) => {
if (res.code === 200) {
state.tableData = res.rows.map((item: any, i: number) => {
item.index = i + 1;
item.autoFill = item.detail?.reduce((sum: number, child: any) => sum + child.autoFill, 0) || 0;
item.aiFill = item.detailList?.reduce((sum: number, child: any) => sum + child.aiFill, 0) || 0;
return item;
});
state.total = res.data.total;
state.total = res.total;
}
loading.value = false;
});
@ -366,7 +373,7 @@ const indexMethod = (index: number): number => {
// 日报添加
const handleDayAdd = (row: any, obj: any) => {
resetForm(true);
formDetail.value.id = obj.id;
formDetail.value.id = row.id;
formDetail.value.submitTime = row.date;
state.updateRow = row;
getPvModuleList();
@ -376,8 +383,9 @@ const tableKey = (row: any) => row.id;
// 展开行处理
const clickOpen = (row: any) => {
const index = state.expandRowKeys.indexOf(row.id);
index === -1 ? state.expandRowKeys.push(row.id) : state.expandRowKeys.splice(index, 1);
const rowId = String(row.id);
const index = state.expandRowKeys.indexOf(rowId);
index === -1 ? state.expandRowKeys.push(rowId) : state.expandRowKeys.splice(index, 1);
state.expandRowKeys = [...new Set(state.expandRowKeys)];
};
@ -394,22 +402,21 @@ const onUploadDaily = () => {
}
const obj = {
ids: checkedList.value,
workID: formDetail.value.workId,
doneTime: formDetail.value.submitTime,
planID: formDetail.value.id
finishedDetailIdList: checkedList.value,
id: formDetail.value.id
};
state.flag = true;
addDaily(obj).then((res: any) => {
if (res.code === 0) {
if (res.code === 200) {
ElMessage.success('添加成功');
if (state.updateRow) {
state.updateRow.finishedNum += checkedList.value.length;
state.updateRow.finishedNumber += checkedList.value.length;
}
checkedList.value = [];
multipleTableRef.value?.clearSelection();
getPvModuleList();
getWorkList();
} else {
ElMessage.error(res.message);
}
@ -420,10 +427,11 @@ const onUploadDaily = () => {
// 查看日报
const handleView = (row: any, obj: any) => {
resetForm(false);
getDailyBookList(row.date);
state.updateRow = row;
formDetail.value.id = obj.id;
formDetail.value.id = row.id;
formDetail.value.submitTime = row.date;
getDailyBookList(row.date);
showDayWork.value = false;
};
@ -431,12 +439,12 @@ const handleView = (row: any, obj: any) => {
const getDailyBookList = (doneTime: string) => {
detialWordList.value = [];
getDailyBook({
workId: formDetail.value.workId,
type: infoDetail.value.work_type,
doneTime
id: formDetail.value.id,
...detailQueryParams.value
}).then((res: any) => {
if (res.code === 0) {
detialWordList.value = res.data.list || [];
if (res.code === 200) {
detialWordList.value = res.rows || [];
detailTotalWork.value = res.total;
} else {
ElMessage.error(res.message);
}
@ -453,10 +461,8 @@ const handleSelectionChangeWork = (selection: any[]) => {
const handleRemove = (row?: any) => {
const planID = row ? [row.id] : state.checkList;
const obj = {
planID,
id: formDetail.value.id,
workID: formDetail.value.workId,
time: formDetail.value.submitTime
detailIdList: planID,
id: formDetail.value.id
};
ElMessageBox.confirm('确认移除该条数据?', '温馨提示', {
@ -466,12 +472,13 @@ const handleRemove = (row?: any) => {
})
.then(() => {
deleteDaily(obj).then((res: any) => {
if (res.code === 0) {
if (res.code === 200) {
ElMessage.success('移除成功');
if (state.updateRow) {
state.updateRow.finishedNum -= planID.length;
state.updateRow.finishedNumber -= planID.length;
}
getDailyBookList(formDetail.value.submitTime);
getWorkList();
} else {
ElMessage.error(res.message);
}
@ -482,14 +489,14 @@ const handleRemove = (row?: any) => {
// 延期计算
const filterW = (row: any): number => {
const { finishedNum, planNum, endAt } = row;
const { finishedNumber, planNumber, endAt } = row;
if (!endAt) return 0;
const endTime = new Date(endAt).getTime();
const now = new Date().getTime();
if (endTime <= now && planNum > finishedNum) {
return planNum - finishedNum;
if (endTime <= now && planNumber > finishedNumber) {
return planNumber - finishedNumber;
}
return 0;
};
@ -500,6 +507,32 @@ defineExpose({
closeDialog
});
const emit = defineEmits(['getProgressList']);
let scrollTop = 0;
watch(
() => isShowDialog.value,
(visible) => {
if (visible) {
scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.width = '100%';
document.body.style.paddingRight = `${scrollbarWidth}px`; // 👈 补偿滚动条宽度
} else {
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
document.body.style.paddingRight = ''; // 👈 恢复
nextTick(() => {
window.scrollTo(0, scrollTop);
});
}
}
);
</script>
<style lang="scss" scoped>

View File

@ -253,6 +253,7 @@ const onSubmit = () => {
workScheduleAddPlan(payload).then((res: any) => {
if (res.code === 200) {
ElMessage.success('添加成功');
emit('getProgressList');
closeDialog();
} else {
ElMessage.error(res.message);
@ -292,6 +293,7 @@ const fetchLastTime = (row: typeof infoDetail) => {
// Export function if needed externally
defineExpose({ openDialog });
const emit = defineEmits(['getProgressList']);
</script>
<style scoped lang="scss">

View File

@ -47,10 +47,27 @@
border
>
<el-table-column label="" width="50" type="expand">
<template #header>
<el-icon
class="cursor-pointer text-4! transform-rotate-z--90 transition-all-300"
:class="!isExpandAll ? 'transform-rotate-z--90' : 'transform-rotate-z-90'"
@click="handleToggleExpandAll"
><Expand
/></el-icon>
</template>
<template #default="scope">
<el-card class="pl-25" shadow="hover">
<el-table :data="scope.row.children" border>
<el-table-column label="名称" align="center" prop="name" width="150" />
<el-table-column label="名称" align="center" prop="name" width="170">
<template #default="{ row }">
<el-tooltip :content="row.remark" placement="top" effect="dark" v-if="row.remark">
<span class="flex items-center justify-center"
><i class="iconfont icon-wenhao mr-0.5 text-3.5! text-#999"></i>{{ row.name }}</span
>
</el-tooltip>
<span v-else>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<dict-tag :options="progress_status" :value="row.status" />
@ -85,7 +102,7 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template #default="scope">
<el-button
<!-- <el-button
type="warning"
icon="Download"
link
@ -94,12 +111,13 @@
v-hasPermi="['progress:progressCategory:add']"
>
导入数据
</el-button>
</el-button> -->
<el-button
type="warning"
icon="Download"
link
size="small"
v-if="scope.row.name === '光伏板'"
@click="openDialog(scope.row, 'importTableStatus', '上传表格')"
v-hasPermi="['progress:progressCategory:add']"
>
@ -254,9 +272,13 @@ const { queryParams, form, rules } = toRefs(data);
const getList = async () => {
if (!queryParams.value.matrixId) {
const res = await getProjectSquare(currentProject.value.id);
if (!matrixValue.value) matrixValue.value = res.rows[0].id;
matrixOptions.value = res.rows;
queryParams.value.matrixId = res.rows[0].id;
if (res.rows.length === 0) {
proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵');
} else {
if (!matrixValue.value) matrixValue.value = res.rows[0].id;
matrixOptions.value = res.rows;
queryParams.value.matrixId = res.rows[0].id;
}
}
loading.value = true;
const res = await listProgressCategory(queryParams.value);
@ -288,6 +310,13 @@ const reset = () => {
progressCategoryFormRef.value?.resetFields();
};
//切换项目重置方阵
const resetMatrix = () => {
matrixValue.value = undefined;
queryParams.value.matrixId = undefined;
matrixOptions.value = [];
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
@ -384,6 +413,7 @@ const listeningProject = watch(
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
resetMatrix();
getList();
}
);

View File

@ -0,0 +1,282 @@
<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="父类别id" prop="pid">
<el-input v-model="queryParams.pid" placeholder="请输入父类别id" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="类别名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入类别名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目id" prop="projectId">
<el-input v-model="queryParams.projectId" placeholder="请输入项目id" 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="['progress:progressCategoryTemplate:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
ref="progressCategoryTemplateTableRef"
v-loading="loading"
:data="progressCategoryTemplateList"
row-key="id"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column label="类别名称" align="center" prop="name" />
<el-table-column label="计量方式" align="center" prop="unitType" />
<el-table-column label="工作类型" align="center" prop="workType" />
<el-table-column label="项目id" align="center" prop="projectId" />
<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="['progress:progressCategoryTemplate:edit']" />
</el-tooltip>
<el-tooltip content="新增" placement="top">
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['progress:progressCategoryTemplate:add']" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['progress:progressCategoryTemplate:remove']" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加或修改进度类别模版对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="progressCategoryTemplateFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="父类别id" prop="pid">
<el-tree-select
v-model="form.pid"
:data="progressCategoryTemplateOptions"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
placeholder="请选择父类别id"
check-strictly
/>
</el-form-item>
<el-form-item label="类别名称" prop="name">
<el-input v-model="form.name" placeholder="请输入类别名称" />
</el-form-item>
<el-form-item label="项目id" prop="projectId">
<el-input v-model="form.projectId" placeholder="请输入项目id" />
</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="ProgressCategoryTemplate" lang="ts">
import { listProgressCategoryTemplate, getProgressCategoryTemplate, delProgressCategoryTemplate, addProgressCategoryTemplate, updateProgressCategoryTemplate } from "@/api/progress/progressCategoryTemplate";
import { ProgressCategoryTemplateVO, ProgressCategoryTemplateQuery, ProgressCategoryTemplateForm } from '@/api/progress/progressCategoryTemplate/types';
type ProgressCategoryTemplateOption = {
id: number;
name: string;
children?: ProgressCategoryTemplateOption[];
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;;
const progressCategoryTemplateList = ref<ProgressCategoryTemplateVO[]>([]);
const progressCategoryTemplateOptions = ref<ProgressCategoryTemplateOption[]>([]);
const buttonLoading = ref(false);
const showSearch = ref(true);
const isExpandAll = ref(true);
const loading = ref(false);
const queryFormRef = ref<ElFormInstance>();
const progressCategoryTemplateFormRef = ref<ElFormInstance>();
const progressCategoryTemplateTableRef = ref<ElTableInstance>()
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: ProgressCategoryTemplateForm = {
id: undefined,
pid: undefined,
name: undefined,
unitType: undefined,
workType: undefined,
projectId: undefined,
remark: undefined,
}
const data = reactive<PageData<ProgressCategoryTemplateForm, ProgressCategoryTemplateQuery>>({
form: {...initFormData},
queryParams: {
pid: undefined,
name: undefined,
unitType: undefined,
workType: undefined,
projectId: undefined,
params: {
}
},
rules: {
id: [
{ required: true, message: "主键id不能为空", trigger: "blur" }
],
pid: [
{ required: true, message: "父类别id不能为空", trigger: "blur" }
],
name: [
{ required: true, message: "类别名称不能为空", trigger: "blur" }
],
unitType: [
{ required: true, message: "计量方式不能为空", trigger: "change" }
],
projectId: [
{ required: true, message: "项目id不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询进度类别模版列表 */
const getList = async () => {
loading.value = true;
const res = await listProgressCategoryTemplate(queryParams.value);
const data = proxy?.handleTree<ProgressCategoryTemplateVO>(res.data, "id", "pid");
if (data) {
progressCategoryTemplateList.value = data;
loading.value = false;
}
}
/** 查询进度类别模版下拉树结构 */
const getTreeselect = async () => {
const res = await listProgressCategoryTemplate();
progressCategoryTemplateOptions.value = [];
const data: ProgressCategoryTemplateOption = { id: 0, name: '顶级节点', children: [] };
data.children = proxy?.handleTree<ProgressCategoryTemplateOption>(res.data, "id", "pid");
progressCategoryTemplateOptions.value.push(data);
}
// 取消按钮
const cancel = () => {
reset();
dialog.visible = false;
}
// 表单重置
const reset = () => {
form.value = {...initFormData}
progressCategoryTemplateFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 新增按钮操作 */
const handleAdd = (row?: ProgressCategoryTemplateVO) => {
reset();
getTreeselect();
if (row != null && row.id) {
form.value.pid = row.id;
} else {
form.value.pid = 0;
}
dialog.visible = true;
dialog.title = "添加进度类别模版";
}
/** 展开/折叠操作 */
const handleToggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
toggleExpandAll(progressCategoryTemplateList.value, isExpandAll.value)
}
/** 展开/折叠操作 */
const toggleExpandAll = (data: ProgressCategoryTemplateVO[], status: boolean) => {
data.forEach((item) => {
progressCategoryTemplateTableRef.value?.toggleRowExpansion(item, status)
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
})
}
/** 修改按钮操作 */
const handleUpdate = async (row: ProgressCategoryTemplateVO) => {
reset();
await getTreeselect();
if (row != null) {
form.value.pid = row.pid;
}
const res = await getProgressCategoryTemplate(row.id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改进度类别模版";
}
/** 提交按钮 */
const submitForm = () => {
progressCategoryTemplateFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateProgressCategoryTemplate(form.value).finally(() => buttonLoading.value = false);
} else {
await addProgressCategoryTemplate(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row: ProgressCategoryTemplateVO) => {
await proxy?.$modal.confirm('是否确认删除进度类别模版编号为"' + row.id + '"的数据项?');
loading.value = true;
await delProgressCategoryTemplate(row.id).finally(() => loading.value = false);
await getList();
proxy?.$modal.msgSuccess("删除成功");
}
onMounted(() => {
getList();
});
</script>

View File

@ -0,0 +1,479 @@
<template>
<div class="header flex justify-end">
<el-form :model="queryParams" ref="form" label-width="80px" inline class="flex items-center">
<el-form-item label="请选择项目:" prop="pid" label-width="100">
<el-select v-model="selectedProjectId" placeholder="请选择" @change="handleSelect" clearable>
<el-option v-for="item in ProjectList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="请选择方阵:" prop="pid" label-width="100">
<el-select v-model="matrixValue" placeholder="请选择" @change="handleChange" clearable>
<el-option v-for="item in matrixOptions" :key="item.id" :label="item.matrixName" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
</div>
<div class="ol-map" id="olMap"></div>
<div class="aside">
<el-tree
style="max-width: 600px"
:data="progressCategoryList"
ref="treeRef"
show-checkbox
@check-change="handleCheckChange"
:props="treeProps"
:load="loadNode"
node-key="id"
lazy
@node-collapse="closeNode"
@node-expand="openNode"
/>
</div>
<div class="submit">
<el-button type="primary" size="default" @click="submit">提交</el-button>
</div>
</template>
<script lang="ts" setup>
import Map from 'ol/Map'; // OpenLayers的主要类用于创建和管理地图
import View from 'ol/View'; // OpenLayers的视图类定义地图的视图属性
import { Tile as TileLayer } from 'ol/layer'; // OpenLayers的瓦片图层类
import { XYZ } from 'ol/source'; // OpenLayers的瓦片数据源包括XYZ格式和OpenStreetMap专用的数据源
import { defaults as defaultControls, defaults, FullScreen, MousePosition, ScaleLine } from 'ol/control';
import { fromLonLat } from 'ol/proj';
import { useUserStoreHook } from '@/store/modules/user';
import { getProjectSquare, listProgressCategory, addDaily, workScheduleListPosition } from '@/api/progress/plan';
import { ProgressCategoryVO, progressPlanDetailForm } from '@/api/progress/plan/types';
import { Circle, Fill, Stroke, Style, Text } from 'ol/style';
import Feature from 'ol/Feature';
import { Point, Polygon } from 'ol/geom';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import Node from 'element-plus/es/components/tree/src/model/node.mjs';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const ProjectList = computed(() => userStore.projects);
const selectedProjectId = ref(userStore.selectedProject?.id || '');
const treeRef = ref();
const queryParams = ref({
pid: undefined,
name: undefined,
unitType: undefined,
projectId: currentProject.value.id,
matrixId: undefined,
params: {}
});
const submitForm = ref<progressPlanDetailForm>({
id: '',
finishedDetailIdList: [] as string[]
});
const loading = ref(false);
const matrixOptions = ref([]);
const matrixValue = ref<number | undefined>(matrixOptions.value.length > 0 ? matrixOptions.value[0].id : undefined);
const progressCategoryList = ref<ProgressCategoryVO[]>([]);
const treeProps = {
children: 'children',
label: 'name',
isLeaf: 'leaf',
hasChildren: 'hasChildren' // 重要
};
//切换项目
const handleSelect = (projectId: string) => {
const selectedProject = ProjectList.value.find((p) => p.id === projectId);
if (selectedProject) {
userStore.setSelectedProject(selectedProject);
resetMatrix();
getList();
}
};
/** 进度类别树选中事件 */
const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean) => {
const node: Node | undefined = treeRef.value?.getNode(data.id);
if (node && node.level === 3) {
console.log('第三级节点被选中:', data, '选中状态:', checked);
}
if (!node || node.level !== 3 || !checked) return;
const parent = node.parent;
if (!parent) return;
// 遍历兄弟节点,取消选中除当前节点之外的其他第三级节点
parent.childNodes.forEach((sibling: Node) => {
if (sibling !== node) {
treeRef.value.setChecked(sibling.data.id, false, false);
}
});
submitForm.value.id = data.id; // 设置提交表单的id
};
/** 关闭节点事件 */
const closeNode = (node: any) => {
// 清除子节点
if (node.pid) {
node.threeChildren.forEach((child: any) => {
const feature = featureMap[child.id];
if (feature && sharedSource.hasFeature(feature)) {
sharedSource.removeFeature(feature);
}
});
}
};
/** 打开节点事件 */
const openNode = (node: any) => {
// 清除子节点
if (!node.pid) return;
addPointToMap(node.threeChildren); // 添加点到地图
};
//懒加载子节点
const loadNode = async (node: any, resolve: (data: any[]) => void) => {
if (node.level !== 2) {
// 只对二级节点加载子节点
resolve(node.data.children || []);
return;
}
const secondLevelNodeId = node.data.id;
const res = await workScheduleListPosition(secondLevelNodeId); // 替换成你的 API
const children = res.data.detailList || [];
if (children.length === 0) {
proxy?.$modal.msgWarning(`节点 "${node.data.name}" 为空`);
resolve([]);
}
// 标记子节点为叶子节点
const threeLeafList = children.map((detail) => {
return {
...detail,
name: detail.date, // 设置为叶子节点
leaf: true // 标记为叶子节点
};
});
progressCategoryList.value.forEach((item, i) => {
let indexNum = item.children.findIndex((item) => item.id === secondLevelNodeId);
if (indexNum !== -1) {
item.children[indexNum].threeChildren = res.data.facilityList; // 将子节点添加到当前节点的threeChildren属性中
}
});
resolve(threeLeafList);
};
/** 提交按钮点击事件 */
const submit = () => {
console.log('sunbmitForm', submitForm.value);
addDaily(submitForm.value)
.then(() => {
proxy?.$modal.msgSuccess('提交成功');
resetTreeAndMap();
})
.catch((error) => {
proxy?.$modal.msgError(`提交失败: ${error.message}`);
});
};
//重置树形结构选中以及图层高亮
const resetTreeAndMap = () => {
// 重置树形结构选中状态
treeRef.value?.setCheckedKeys([]);
// 清除地图上的所有高亮
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
sharedSource.getFeatures().forEach((feature) => {
if (feature.get('highlighted')) {
feature.setStyle(defaultStyle(feature.get('name'), scale)); // 恢复默认样式
feature.set('highlighted', false); // 重置高亮状态
}
});
// 清空已完成列表
submitForm.value.finishedDetailIdList = [];
};
/** 方阵选择器改变事件 */
const handleChange = (value: number) => {
queryParams.value.matrixId = value;
getList();
};
//切换项目重置方阵
const resetMatrix = () => {
matrixValue.value = undefined;
queryParams.value.matrixId = undefined;
matrixOptions.value = [];
};
/** 查询进度类别列表 */
const getList = async () => {
if (!queryParams.value.matrixId) {
const res = await getProjectSquare(currentProject.value.id);
if (res.rows.length === 0) {
proxy?.$modal.msgWarning('当前项目下没有方阵,请先创建方阵');
} else {
if (!matrixValue.value) matrixValue.value = res.rows[0].id;
matrixOptions.value = res.rows;
queryParams.value.matrixId = res.rows[0].id;
}
}
loading.value = true;
queryParams.value.projectId = currentProject.value.id;
const res = await listProgressCategory(queryParams.value);
const data = proxy?.handleTree<ProgressCategoryVO>(res.data, 'id', 'pid');
if (data) {
progressCategoryList.value = data;
loading.value = false;
}
};
let map: any = null;
const layerData = reactive<any>({});
const centerPosition = ref(fromLonLat([107.12932403398425, 23.805564054229908]));
const initOLMap = () => {
// 创造地图实例
map = new Map({
// 设置地图容器的ID
target: 'olMap',
// 定义地图的图层列表,用于显示特定的地理信息。
layers: [
// 高德地图
// TileLayer表示一个瓦片图层它由一系列瓦片通常是图片组成用于在地图上显示地理数据。
new TileLayer({
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源它允许你通过URL模板来获取瓦片
source: new XYZ({
url: 'https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
})
})
],
// 设置地图的视图参数
// View表示地图的视图它定义了地图的中心点、缩放级别、旋转角度等参数。
view: new View({
// fromLonLat是一个函数用于将经纬度坐标转换为地图的坐标系统。
center: centerPosition.value, //地图中心点
zoom: 15, // 缩放级别
minZoom: 0, // 最小缩放级别
// maxZoom: 18, // 最大缩放级别
constrainResolution: true // 因为存在非整数的缩放级别所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别这个必须要设置当缩放在非整数级别时地图会糊
// projection: 'EPSG:4326' // 投影坐标系默认是3857
}),
//加载控件到地图容器中
controls: defaultControls({
zoom: false,
rotate: false,
attribution: false
})
});
map.on('click', (e: any) => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
map.forEachFeatureAtPixel(e.pixel, (feature: Feature) => {
if (feature.get('status') === '2') return; // 如果是完成状态,直接返回
const isHighlighted = feature.get('highlighted') === true;
const geomType = feature.getGeometry().getType();
if (isHighlighted) {
feature.setStyle(defaultStyle(feature.get('name'), scale)); // 清除高亮样式
feature.set('highlighted', false);
submitForm.value.finishedDetailIdList = submitForm.value.finishedDetailIdList.filter((id) => id !== feature.get('id')); // 从已完成列表中移除
return;
}
if (geomType === 'Polygon') {
feature.setStyle(highlightStyle(feature.get('name'), scale));
feature.set('highlighted', true);
submitForm.value.finishedDetailIdList.push(feature.get('id')); // 添加到已完成列表
}
});
});
map.getView().on('change:resolution', () => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
sharedSource.getFeatures().forEach((feature) => {
const style = feature.getStyle();
if (style instanceof Style && style.getText()) {
style.getText().setScale(scale);
feature.setStyle(style); // 重新应用样式
}
});
});
};
const highlightStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: 'orange',
width: 4
}),
fill: new Fill({
color: 'rgba(255,165,0,0.3)' // 半透明橙色
}),
text: new Text({
font: '14px Microsoft YaHei',
text: name,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
scale,
fill: new Fill({ color: 'orange' })
})
});
};
const defaultStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: '#003366',
width: 2
}),
text: new Text({
font: '12px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#003366 ' })
}),
fill: new Fill({ color: 'skyblue' })
});
};
const successStyle = (name, scale) => {
return new Style({
stroke: new Stroke({
color: '#2E7D32 ',
width: 2
}),
text: new Text({
font: '14px Microsoft YaHei',
text: name,
scale,
placement: 'line', // 👈 关键属性
offsetX: 50, // 向右偏移 10 像素
offsetY: 20, // 向下偏移 5 像素
fill: new Fill({ color: '#FFFFFF ' })
}),
fill: new Fill({ color: '#7bdd63 ' })
});
};
/**
* 创建图层
* @param {*} pointObj 坐标数组
* @param {*} type 类型
* @param {*} id 唯一id
* @param {*} name 名称
* */
// 共享 source 和图层(全局一次性创建)
const sharedSource = new VectorSource();
const sharedLayer = new VectorLayer({
source: sharedSource,
renderMode: 'image' // 提高渲染性能
} as any);
// id => Feature 映射表
const featureMap: Record<string, Feature> = {};
const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: string, status?: string) => {
let geometry;
if (type === 'Point') {
geometry = new Point(fromLonLat(pointObj));
} else if (type === 'Polygon') {
const coords = pointObj.map((arr: any) => fromLonLat(arr));
geometry = new Polygon([coords]);
}
const feature = new Feature({ geometry });
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
const pointStyle = new Style({
image: new Circle({
radius: 2,
fill: new Fill({ color: 'red' })
}),
text: new Text({
font: '12px Microsoft YaHei',
text: name,
scale,
fill: new Fill({ color: '#7bdd63' })
})
});
const polygonStyle = status == '2' ? successStyle(name, scale) : defaultStyle(name || '', scale);
feature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
feature.set('name', name || ''); // 设置名称
feature.set('status', status || ''); // 设置完成状态 2为完成 其他为未完成
feature.set('id', id || '');
// 缓存 feature用于后续判断
featureMap[id] = feature;
};
// 添加点到地图
const addPointToMap = (features: Array<any>) => {
features.forEach((item: any) => {
const fid = item.id;
// 没创建过就先创建
if (!featureMap[fid]) {
creatPoint(item.positions, item.type, fid, item.name, item.status);
}
// 添加到共享 source 中(避免重复添加)
const feature = featureMap[fid];
if (!sharedSource.hasFeature(feature)) {
sharedSource.addFeature(feature);
}
});
};
onMounted(() => {
// 地图初始化
initOLMap();
map.addLayer(sharedLayer);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
getList();
});
</script>
<style lang="scss" scoped>
.ol-map {
height: 100vh;
width: 100%;
position: absolute;
z-index: 1;
}
.header {
height: 90px;
width: 100%;
position: absolute;
z-index: 2;
}
.aside {
position: absolute;
top: 10%;
left: 30px;
width: 300px;
height: 70vh;
background-color: rgba(255, 255, 255, 0.8);
z-index: 3;
overflow: auto;
}
.submit {
position: absolute;
bottom: 70px;
right: 70px;
z-index: 3;
}
</style>