Files
td_official/src/views/progress/progressPaper/index.vue

570 lines
18 KiB
Vue
Raw Normal View History

2025-05-30 19:51:35 +08:00
<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" style="margin-bottom: 0">
2025-05-30 19:51:35 +08:00
<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" style="margin-bottom: 0">
2025-05-30 19:51:35 +08:00
<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"
@check-change="handleCheckChange"
:props="treeProps"
:load="loadNode"
node-key="id"
lazy
:render-content="renderContent"
check-strictly
2025-06-04 19:33:17 +08:00
:expand-on-click-node="false"
2025-05-30 19:51:35 +08:00
/>
</div>
<div class="submit">
<el-button type="primary" size="default" @click="submit" :loading="loading">提交</el-button>
2025-05-30 19:51:35 +08:00
</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 { defaults as defaultInteractions } from 'ol/interaction';
2025-05-30 19:51:35 +08:00
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';
import { ElCheckbox } from 'element-plus';
2025-05-30 19:51:35 +08:00
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',
2025-06-04 19:33:17 +08:00
hasChildren: 'hasChildren', // 重要
disabled: 'disabled'
2025-05-30 19:51:35 +08:00
};
//切换项目
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);
2025-06-04 19:33:17 +08:00
if (node.level === 2) {
//二级节点 展开显示图层 收起删除
const children = node.childNodes.map((child) => child.data);
children.forEach((child) => {
child.disabled = !checked;
});
// 让 el-tree 刷新显示
treeRef.value.updateKeyChildren(data.id, children);
if (checked) {
addPointToMap(data.threeChildren); // 添加点到地图
clearLevel3Checked(node.level); // 清除三级节点的选中状态
treeRef.value.setChecked(data.id, true, false);
} else {
data.threeChildren.forEach((child: any) => {
const feature = featureMap[child.id];
if (feature && sharedSource.hasFeature(feature)) {
sharedSource.removeFeature(feature);
}
});
}
return;
}
if (!checked) return (submitForm.value.id = ''); // 只处理第三级节点的选中事件
2025-05-30 19:51:35 +08:00
const parent = node.parent;
if (!parent) return;
//消除所有节点的选中状态
2025-06-04 19:33:17 +08:00
clearLevel3Checked(node.level); // 清除三级节点的选中状态
// 设置当前点击项为选中
treeRef.value.setChecked(data.id, true, false);
2025-05-30 19:51:35 +08:00
submitForm.value.id = data.id; // 设置提交表单的id
};
2025-06-04 19:33:17 +08:00
//清除某一级节点所有选中状态
const clearLevel3Checked = (level) => {
const rootNodes = treeRef.value.store.root.childNodes;
const clearChecked = (nodes: any[]) => {
nodes.forEach((node) => {
if (node.level === level) {
treeRef.value.setChecked(node.key, false, false);
} else if (node.childNodes?.length) {
clearChecked(node.childNodes);
2025-05-30 19:51:35 +08:00
}
});
2025-06-04 19:33:17 +08:00
};
2025-05-30 19:51:35 +08:00
2025-06-04 19:33:17 +08:00
clearChecked(rootNodes);
2025-05-30 19:51:35 +08:00
};
2025-06-04 19:33:17 +08:00
/** 关闭节点事件 */
// 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); // 添加点到地图
// };
2025-05-30 19:51:35 +08:00
//懒加载子节点
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, // 设置为叶子节点
2025-06-04 19:33:17 +08:00
leaf: true, // 标记为叶子节点
disabled: true
2025-05-30 19:51:35 +08:00
};
});
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属性中
2025-06-04 19:33:17 +08:00
// item.children[indexNum].detailChildren = children; // 将子节点添加到当前节点的threeChildren属性中
2025-05-30 19:51:35 +08:00
}
});
resolve(threeLeafList);
};
/** 提交按钮点击事件 */
const submit = () => {
console.log('sunbmitForm', submitForm.value);
const { finishedDetailIdList, id } = submitForm.value;
if (!id || finishedDetailIdList.length === 0) return proxy?.$modal.msgWarning('请选择图层以及日期');
loading.value = true;
2025-05-30 19:51:35 +08:00
addDaily(submitForm.value)
.then(() => {
proxy?.$modal.msgSuccess('提交成功');
const scale = Math.max(map.getView().getZoom() / 10, 1); // 获取当前缩放比例
sharedSource.getFeatures().forEach((feature) => {
if (feature.get('highlighted')) {
feature.setStyle(successStyle(feature.get('name'), scale)); // 转为成功样式
feature.set('highlighted', false); // 重置高亮状态
feature.set('status', '2'); // 设置为完成状态
}
});
2025-05-30 19:51:35 +08:00
resetTreeAndMap();
})
.catch((error) => {
proxy?.$modal.msgError(`提交失败: ${error.message}`);
});
};
//重置树形结构选中以及图层高亮
const resetTreeAndMap = () => {
// 重置树形结构选中状态
2025-06-04 19:33:17 +08:00
clearLevel3Checked(3);
//取消加载状态
loading.value = false;
2025-05-30 19:51:35 +08:00
// 清除地图上的所有高亮
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();
};
//限定部分节点能选择
2025-06-04 19:33:17 +08:00
const renderContent = (context, { node, data }) => {
if (node.level === 3) {
return h('span', { class: 'custom-tree-node' }, [
h(ElCheckbox, {
modelValue: node.checked,
'onUpdate:modelValue': (val) => node.setChecked(val),
2025-06-04 19:33:17 +08:00
style: 'margin-right: 8px;',
disabled: data.disabled
}),
h('span', node.label)
]);
}
if (node.level === 2) {
return h('span', { class: 'custom-tree-node' }, [
h(ElCheckbox, {
modelValue: node.checked,
'onUpdate:modelValue': (val) => node.setChecked(val),
style: 'margin-right: 8px;',
disabled: !data.threeChildren || data.threeChildren.length == 0
}),
h('span', node.label)
]);
} else {
return h('span', node.label);
}
};
2025-05-30 19:51:35 +08:00
//切换项目重置方阵
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
}),
interactions: defaultInteractions({
doubleClickZoom: false // 禁用双击缩放
2025-05-30 19:51:35 +08:00
})
});
map.on('click', (e: any) => {
const zoom = map.getView().getZoom();
const scale = Math.max(zoom / 10, 1); // 缩放比例,根据需要调整公式
map.forEachFeatureAtPixel(e.pixel, (feature: Feature) => {
const geomType = feature.getGeometry().getType();
if (feature.get('status') === '2' || geomType != 'Polygon') return; // 如果是完成状态,直接返回
const isHighlighted = feature.get('highlighted') === true;
2025-05-30 19:51:35 +08:00
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;
}
feature.setStyle(highlightStyle(feature.get('name'), scale));
feature.set('highlighted', true);
submitForm.value.finishedDetailIdList.push(feature.get('id')); // 添加到已完成列表
2025-05-30 19:51:35 +08:00
});
});
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: 70px;
2025-05-30 19:51:35 +08:00
width: 100%;
position: absolute;
z-index: 2;
background: rgba(255, 255, 255, 0.2); /* 半透明白色 */
backdrop-filter: blur(10px); /* 背景模糊 */
-webkit-backdrop-filter: blur(10px); /* 兼容 Safari */
2025-05-30 19:51:35 +08:00
}
.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;
}
.custom-tree-node {
display: flex;
align-items: center;
}
2025-05-30 19:51:35 +08:00
</style>