This commit is contained in:
2026-03-26 11:12:54 +08:00
parent 8436bf60fa
commit 023108c623
6 changed files with 549 additions and 30 deletions

View File

@ -0,0 +1,50 @@
//巡逻方块管理
import request from "@/utils/request";
const api = "/mosty-api/mosty-yjzl";
// 分页查询
export function getPatrolBlockList(params) {
return request({
url: api + '/tbZdxlFgdw/selectPage',
method: 'GET',
params
})
}
//新增
export function addPatrolBlock(data) {
return request({
url: api + '/tbZdxlFgdw/save',
method: "post",
data
})
}
//修改
export function updatePatrolBlock(data) {
return request({
url: api + '/tbZdxlFgdw/update',
method: "post",
data
})
}
//详情
export function getPatrolBlockInfo(id) {
return request({
url: api + `/tbZdxlFgdw/${id}`,
method: "get"
})
}
//删除
export function deletePatrolBlock(id) {
return request({
url: api + `/tbZdxlFgdw/${id}`,
method: "delete"
})
}
// 根据经纬度获取正方形四个点位
export function getSquarePoints(params) {
return request({
url: api + '/tbZdxlFgdw/getSquarePoints',
method: 'GET',
params
})
}

View File

@ -513,11 +513,13 @@ export function MapUtil(map) {
*/
MapUtil.prototype.echoPlane = (res) => {
let { type , coords ,fontColor, text = '' ,radius = 0, isclear ,flag ,id = 1 , color , linecolor} = res;
console.log('echoPlane 接收参数:', res);
if(!coords) return;
if(isclear) _that.removeElement(flag)
if(!_that._self[flag]) _that._self[flag] = [];
let color1 = color ? color : 'rgba(29,237,245,0.6)'
let linecolor1 = linecolor ? linecolor : 'rgba(29,237,245,0.6)'
console.log('echoPlane 实际使用颜色 - fill:', color1, 'border:', linecolor1);
let style = {
color:color1,
outLineColor:linecolor1,

View File

@ -621,6 +621,15 @@ export const privateRoutes = [{
icon: "article-ranking"
},
},
{
path: "/taskPage/patrol-block",
component: () => import("@/views/backOfficeSystem/service/taskPage/patrol-block/index"),
name: "patrol-block",
meta: {
title: '巡逻方块',
icon: "article-ranking"
},
},
]
},
{

View File

@ -93,6 +93,7 @@ const stepActive = ref(0)
const emit = defineEmits(["updateDate"]);
const dialogForm = ref(false); //弹窗
const listQuery = ref({ fgData: [] }); //表单
const fgPlaneList = ref([]);
const fgVisible = ref(false);
const loading = ref(false);
const elform = ref();
@ -143,10 +144,48 @@ onMounted(() => {
zxX: val.zxX,
zxY: val.zxY,
}];
renderBaseFg();
renderSelectedFg(val);
}
})
});
const renderBaseFg = () => {
emitter.emit("deletePointArea", "taskFg-flag");
if (fgPlaneList.value.length === 0) return;
emitter.emit("echoPlane", {
fontColor: "#12fdb8",
coords: fgPlaneList.value,
type: "rectangle",
flag: "taskFg-flag",
color: "rgba(2,20,51,0.5)",
linecolor: "#1C97FF"
});
};
const renderSelectedFg = (fg) => {
if (!fg?.x1 || !fg?.y1 || !fg?.x2 || !fg?.y2) return;
emitter.emit("deletePointArea", "taskFg-info");
const position = [
[Number(fg.x1), Number(fg.y1)],
[Number(fg.x2), Number(fg.y2)]
];
const selected = [{
position,
text: fg.mc1 || fg.mc || fg.fgmc,
id: fg.id,
userData: fg
}];
emitter.emit("echoPlane", {
fontColor: "white",
coords: selected,
type: "rectangle",
flag: "taskFg-info",
color: "red",
linecolor: "red"
});
};
// 初始化数据
const init = (type, row) => {
dialogForm.value = true;
@ -186,32 +225,15 @@ const getDataById = (row) => {
fgmc: res.fgmc,
id: res.fgId,
}]
const fginfo = listQuery.value.fgData?.map((el, index) => {
let position = [
[Number(el.x1), Number(el.y1)],
[Number(el.x2), Number(el.y2)]
];
let text = el.mc;
let obj = { position: position, text, id: el.id, userData: el };
return obj;
});
emitter.emit("echoPlane", {
fontColor: "white",
coords: fginfo,
type: "rectangle",
flag: "taskFg-info",
color: "red",
linecolor: "red"
});
renderSelectedFg(listQuery.value.fgData[0]);
pageData.tableData = res.bxds
});
};
const getData = async () => {
const res = await fetchTbZdxlFgdwSelectList();
if (res && res?.length > 0) {
emitter.emit("deletePointArea", "taskFg-flag");
let cc = [];
const list = res?.map((el, index) => {
fgPlaneList.value = res?.map((el, index) => {
let centerPoint = [el.zxX, el.zxY];
if (index == 0) cc = centerPoint;
let position = [
@ -222,14 +244,7 @@ const getData = async () => {
let obj = { position: position, text, id: el.id, userData: el };
return obj;
});
emitter.emit("echoPlane", {
fontColor: "#12fdb8",
coords: list,
type: "rectangle",
flag: "taskFg-flag",
color: "rgba(2,20,51,0.5)",
linecolor: "#1C97FF"
});
renderBaseFg();
emitter.emit("setMapCenter", { location: cc, zoomLevel: 11 });
}
};
@ -319,6 +334,9 @@ const close = () => {
listQuery.value = { qcys: "#409eff" };
dialogForm.value = false;
loading.value = false;
fgPlaneList.value = [];
emitter.emit("deletePointArea", "taskFg-info");
emitter.emit("deletePointArea", "taskFg-flag");
};
defineExpose({ init });

View File

@ -0,0 +1,440 @@
<template>
<div>
<div class="titleBox">
<div class="title">巡逻方块管理</div>
<div class="btnBox">
<el-button type="primary" @click="addEdit('add', '')">
<el-icon style="vertical-align: middle">
<CirclePlus />
</el-icon>
<span style="vertical-align: middle">新增</span>
</el-button>
<el-button @click="batchDelete" :disabled="ids.length == 0" typeof="danger">
<el-icon style="vertical-align: middle">
<Delete />
</el-icon>
<span style="vertical-align: middle">批量删除</span>
</el-button>
</div>
</div>
<div class="searchBox" ref="searchBox">
<el-form :model="listQuery" :inline="true">
<el-form-item label="名称">
<el-input v-model="listQuery.mc" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item>
<el-button @click="handleFilter"> 查询 </el-button>
<el-button @click="reset"> 重置 </el-button>
</el-form-item>
</el-form>
</div>
<div class="tabBox">
<el-table :data="tableData" border row-key="id" @selection-change="handleSelectionChange"
style="width: 100%" :height="tableHeight" :key="keyCount" v-loading="loadingTable"
element-loading-background="rgba(0,0,0,0.3)" element-loading-text="数据加载中">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" align="center" width="60px" label="序号">
</el-table-column>
<el-table-column prop="mc" show-overflow-tooltip align="center" label="名称">
</el-table-column>
<el-table-column prop="ssbm" show-overflow-tooltip align="center" label="所属部门">
</el-table-column>
<el-table-column prop="x1" label="X1" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="y1" label="Y1" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="x2" label="X2" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="y2" label="Y2" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="zxX" label="中心X" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="zxY" label="中心Y" align="center" show-overflow-tooltip>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="140px">
<template #default="{ row }">
<el-button @click="addEdit('detail', row.id)" size="small">详情</el-button>
<el-button @click="delDictItem(row.id)" type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="fenye" :style="{ top: tableHeight + 'px' }">
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="listQuery.pageCurrent" :page-sizes="[10, 20, 50, 100]" :page-size="listQuery.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
</div>
</div>
<div v-if="dialogFormVisible" class="dialog">
<div class="head_box">
<span class="title">{{ title }}</span>
<div>
<el-button v-if="!isDetail" :loading="loading" type="primary" size="small" @click="submit">保存</el-button>
<el-button size="small" @click="close">关闭</el-button>
</div>
</div>
<el-form ref="elform" :model="form" :rules="rules" :inline="true" label-position="top">
<el-form-item style="width: 48%" prop="mc" label="名称">
<el-input v-model="form.mc" placeholder="请输入名称" style="width: 100%" clearable />
</el-form-item>
<el-form-item style="width: 48%" prop="ssbmdm" label="所属部门">
<MOSTY.Department width="100%" :placeholder="form.ssbm || '请选择部门'" clearable filterable v-model="form.ssbmdm"
@getDepValue="getDepValue" />
</el-form-item>
<el-form-item style="width: 48%" prop="distance" label="方格大小(米)">
<el-input-number v-model="form.distance" :min="100" :max="5000" :step="100" style="width: 100%" />
</el-form-item>
<el-form-item style="width: 48%" prop="x1" label="X1">
<el-input v-model="form.x1" placeholder="X1" style="width: 100%" disabled />
</el-form-item>
<el-form-item style="width: 48%" prop="y1" label="Y1">
<el-input v-model="form.y1" placeholder="Y1" style="width: 100%" disabled />
</el-form-item>
<el-form-item style="width: 48%" prop="x2" label="X2">
<el-input v-model="form.x2" placeholder="X2" style="width: 100%" disabled />
</el-form-item>
<el-form-item style="width: 48%" prop="y2" label="Y2">
<el-input v-model="form.y2" placeholder="Y2" style="width: 100%" disabled />
</el-form-item>
<el-form-item style="width: 48%" prop="zxX" label="中心X">
<el-input v-model="form.zxX" placeholder="中心X" style="width: 100%" disabled />
</el-form-item>
<el-form-item style="width: 48%" prop="zxY" label="中心Y">
<el-input v-model="form.zxY" placeholder="中心Y" style="width: 100%" disabled />
</el-form-item>
</el-form>
<div style="height: 40vh; width: 100%">
<GdMap v-if="dialogFormVisible" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, onUnmounted, nextTick } from "vue";
import { getPatrolBlockList, addPatrolBlock, updatePatrolBlock, getPatrolBlockInfo, deletePatrolBlock, getSquarePoints } from "@/api/basicsmanage/patrolBlock.js";
import GdMap from "@/components/Map/GdMap/index.vue";
import emitter from "@/utils/eventBus.js";
import * as MOSTY from "@/components/MyComponents/index";
const { proxy } = getCurrentInstance();
const ids = ref([]); //批量删除的ID
const listQuery = ref({
pageCurrent: 1,
pageSize: 20,
mc: ''
});
const tableData = ref([]); //表单数据
const searchBox = ref(null); // 搜索盒子
const keyCount = ref(0); //tabel组件刷新值
const tableHeight = ref(); // 表格高度
const loadingTable = ref(true);
const loading = ref(false);
const title = ref("新增巡逻方块");
const total = ref(0); //总数据
const dialogFormVisible = ref(false);
const formDefault = ref({});
const openType = ref(""); // 打开类型 add/edit/detail
const isDetail = ref(false); // 是否为详情模式
const mapClickSeq = ref(0);
//表单数据
const form = ref({
mc: "",
x1: "",
y1: "",
x2: "",
y2: "",
zxX: "",
zxY: "",
ssbm: "",
ssbmdm: "",
distance: 500
});
//表单验证
const rules = reactive({
mc: [{ required: true, message: "请输入名称", trigger: "blur" }],
ssbmdm: [{ required: true, message: "请选择所属部门", trigger: "blur" }]
});
const elform = ref(null);
const listQueryDefault = ref({})
// 方块颜色常量(统一管理)- 与编辑时保持一致
const BLOCK_COLORS = {
fill: "rgba(2,20,51,0.5)", // 填充色
border: "#1C97FF", // 边框色 - 蓝色
text: "#12fdb8" // 文字色 - 青绿色
};
onMounted(() => {
listQueryDefault.value = JSON.parse(JSON.stringify(listQuery.value))
formDefault.value = JSON.parse(JSON.stringify(form.value))
getListData();
tabHeightFn();
proxy.mittBus.on("mittFn", (data) => {
keyCount.value = data;
});
// 监听地图点击事件返回的坐标
emitter.on("mapClickCoordinates", (data) => {
if (openType.value === 'add') {
handleMapClick([data.lng, data.lat]);
}
});
});
onUnmounted(() => {
proxy.mittBus.off("mittFn");
emitter.off("mapClickCoordinates");
});
// 查询
const handleFilter = () => {
listQuery.value.pageCurrent = 1;
getListData();
};
//获取数据
const getListData = () => {
loadingTable.value = true;
getPatrolBlockList(listQuery.value).then((res) => {
tableData.value = res.records || [];
total.value = res.total || 0;
loadingTable.value = false;
}).catch(() => {
loadingTable.value = false;
});
};
// 新增/编辑/详情
function addEdit(type, id) {
dialogFormVisible.value = true;
openType.value = type;
isDetail.value = type === 'detail';
// 清除所有可能的方块图形
emitter.emit("deletePointArea", "patrolBlock");
emitter.emit("deletePointArea", "zdxl_fzyc");
emitter.emit("deletePointArea", "tbZdxlFgdw");
emitter.emit("deletePointArea", "taskFg-info");
emitter.emit("deletePointArea", "taskFg-flag");
emitter.emit("deletePointArea", "addfg");
if (type === 'detail') {
title.value = "巡逻方块详情";
getPatrolBlockInfo(id).then((res) => {
form.value = res;
if (res.x1 && res.y1 && res.x2 && res.y2) {
setTimeout(() => {
showBlockOnMap(res);
}, 500);
}
});
} else if (type === 'edit') {
title.value = "修改巡逻方块";
getPatrolBlockInfo(id).then((res) => {
form.value = res;
form.value.distance = 500;
if (res.x1 && res.y1 && res.x2 && res.y2) {
setTimeout(() => {
showBlockOnMap(res);
}, 500);
}
});
} else {
title.value = "新增巡逻方块";
form.value = JSON.parse(JSON.stringify(formDefault.value));
setTimeout(() => {
emitter.emit("getMapClickCoordinates");
}, 500);
}
}
// 获取部门信息
function getDepValue(val) {
if (val) {
form.value.ssbm = val.orgName || val.deptName || val.label;
form.value.ssbmdm = val.orgCode || val.deptCode || val.value;
// 清除部门字段的验证状态
nextTick(() => {
elform.value?.clearValidate('ssbmdm');
});
}
}
// 处理地图点击
function handleMapClick(coords) {
const [longitude, latitude] = coords;
const seq = ++mapClickSeq.value;
form.value.zxX = longitude.toFixed(6);
form.value.zxY = latitude.toFixed(6);
// 调用接口获取正方形四个点位
getSquarePoints({
longitude: longitude,
latitude: latitude,
distance: form.value.distance || 500
}).then((res) => {
if (seq !== mapClickSeq.value) return;
form.value.x1 = res.x1;
form.value.y1 = res.y1;
form.value.x2 = res.x2;
form.value.y2 = res.y2;
// 新增预览:固定 flag + echoPlane isclear避免 patrolBlock_n 异步竞态导致边框图层残留
showBlockOnMap(form.value, { preview: true });
proxy.$message.success("已获取方格坐标");
}).catch(() => {
proxy.$message.error("获取方格坐标失败");
});
}
// 在地图上显示方块opts.preview 为 true 时表示新增选点预览
function showBlockOnMap(data, opts = {}) {
const currentFlag = "patrolBlock";
let position = [
[Number(data.x1), Number(data.y1)],
[Number(data.x2), Number(data.y2)]
];
let text = data.mc;
let obj = [{ position, text, id: data.id || 1 }];
// 先删除同名 flag 的旧图形
emitter.emit("deletePointArea", currentFlag);
// 延迟绘制,等待删除完成
setTimeout(() => {
emitter.emit("echoPlane", {
fontColor: BLOCK_COLORS.text,
coords: obj,
type: "rectangle",
flag: currentFlag,
isclear: true,
color: BLOCK_COLORS.fill,
linecolor: BLOCK_COLORS.border
});
}, 100);
// 设置地图中心点
const centerLng = data.zxX || (Number(data.x1) + Number(data.x2)) / 2;
const centerLat = data.zxY || (Number(data.y1) + Number(data.y2)) / 2;
emitter.emit("setMapCenter", {
location: [Number(centerLng), Number(centerLat)],
zoomLevel: 14
});
}
//批量数据
const handleSelectionChange = (val) => {
ids.value = val.map(v => { return v.id });
};
// 单个删除
function delDictItem(id) {
proxy.$confirm("确定要删除", "警告", { type: "warning" }).then(() => {
deletePatrolBlock(id).then((res) => {
proxy.$message({ message: "删除成功", type: "success" });
getListData();
});
}).catch(() => {
proxy.$message.info("已取消");
});
}
//批量删除数据
const batchDelete = () => {
proxy.$confirm("确定要删除", "警告", { type: "warning" }).then(() => {
// 批量删除:逐个调用删除接口
const deletePromises = ids.value.map(id => deletePatrolBlock(id));
Promise.all(deletePromises).then(() => {
proxy.$message({ message: "删除成功", type: "success" });
getListData();
});
}).catch(() => {
proxy.$message.info("已取消");
});
};
//提交
function submit() {
elform.value.validate((valid) => {
if (!valid) return false
loading.value = true;
// 新增时验证坐标
if (openType.value === 'add' && (!form.value.x1 || !form.value.y1)) {
proxy.$message.warning("请在地图上选择位置");
loading.value = false;
return;
}
if (title.value == "新增巡逻方块") {
addPatrolBlock(form.value).then(() => {
proxy.$message({ type: "success", message: "新增成功" });
close()
loading.value = false;
getListData();
}).catch(() => {
loading.value = false;
})
} else {
updatePatrolBlock(form.value).then(() => {
proxy.$message({ type: "success", message: "修改成功" });
close()
loading.value = false;
getListData();
}).catch(() => {
loading.value = false;
})
}
});
}
//关闭弹窗
function close() {
// 必须先清除图层再关弹窗GdMap 用 v-if 挂载,若先 dialogFormVisible=false
// 子组件会卸载并 off("deletePointArea"),后面的 emit 无人处理,方块会永远留在地图上。
emitter.emit("deletePointArea", "patrolBlock");
emitter.emit("deletePointArea", "zdxl_fzyc");
emitter.emit("deletePointArea", "tbZdxlFgdw");
emitter.emit("deletePointArea", "taskFg-info");
emitter.emit("deletePointArea", "taskFg-flag");
emitter.emit("deletePointArea", "addfg");
dialogFormVisible.value = false;
form.value = JSON.parse(JSON.stringify(formDefault.value));
mapClickSeq.value = 0;
}
// 重置
const reset = () => {
listQuery.value = JSON.parse(JSON.stringify(listQueryDefault.value))
getListData();
};
// 表格高度计算
const tabHeightFn = () => {
tableHeight.value = window.innerHeight - searchBox.value.offsetHeight - 240;
window.onresize = function () { tabHeightFn(); };
};
/**
* size 改变触发
*/
const handleSizeChange = (currentSize) => {
listQuery.value.pageSize = currentSize;
getListData();
};
/**
* 页码改变触发
*/
const handleCurrentChange = (currentPage) => {
listQuery.value.pageCurrent = currentPage;
getListData();
};
</script>
<style lang="scss" scoped>
@import "~@/assets/css/layout.scss";
@import "~@/assets/css/element-plus.scss";
</style>

View File

@ -29,20 +29,20 @@
<span class="label" @click="show_Dialog('jmll', 'fj')"><span class="xbt">辅警</span><span :title="xz.jmfjsl || 0"
class="num-two">{{ xz.jmfjsl || 0 }}</span></span>
</li>
<!-- <li class="item">
<li class="item">
<div class="bt">当日巡逻</div>
<span class="label" @click="show_Dialog('xlsc', 'xs')"><span class="xbt">时长(h)</span><span :title="xz.xfsc || 0"
class="num-one">{{ xz.xfsc || 0 }}</span></span>
<span class="label" @click="show_Dialog('xlsc', 'lc')"><span class="xbt">里程(km)</span><span :title="xz.xflc || 0"
class="num-two">{{ xz.xflc || 0 }}</span></span>
</li>
<li class="item">
<!-- <li class="item">
<div class="bt">任务情况</div>
<span class="label" @click="show_Dialog('zlzx', 'zs')"><span class="xbt">总数</span><span class="num-one">{{
zlzx.zlzs || 0 }}</span></span>
<span class="label" @click="show_Dialog('zlzx', 'zx')"><span class="xbt">执行</span><span class="num-two">{{
zlzx.zlzxs || 0 }}</span></span>
</li> -->
</li> -->
</ul>
</div>
</template>