:feat:运营区域功能

This commit is contained in:
5g0Wp7Zy 2025-10-22 16:35:03 +08:00
parent eba5b80fa8
commit b1d513fe56
12 changed files with 857 additions and 2 deletions

8
.env
View File

@ -8,4 +8,10 @@ VITE_GLOB_APP_TITLE = 卓景出行
VITE_IMG_BASE_URL = ''
# 本地mock数据 true开启 false关闭
VITE_APP_OPEN_MOCK = false
VITE_APP_OPEN_MOCK = false
#高德securityJsCode
VITE_GLOB_AMAP_SECURITY_JS_CODE = 'b85d17b7e0fa98864d495a9a52f162e4'
# 高德密钥
VITE_GLOB_AMAP_KEY = '0e6910fae6848722b0e57f0c01597499'

View File

@ -9,4 +9,4 @@ VITE_ROUTER_MODE = hash
VITE_PUBLIC_PATH = './'
# 请求路径 管理系统/开发环境
VITE_APP_BASE_URL = 'http://192.168.2.200:10010'
VITE_APP_BASE_URL = 'http://192.168.101.20:10010'

View File

@ -32,6 +32,7 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@arco-design/color": "^0.4.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",

9
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@amap/amap-jsapi-loader':
specifier: ^1.0.1
version: 1.0.1
'@arco-design/color':
specifier: ^0.4.0
version: 0.4.0
@ -237,6 +240,9 @@ importers:
packages:
'@amap/amap-jsapi-loader@1.0.1':
resolution: {integrity: sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==}
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
@ -4941,6 +4947,8 @@ packages:
snapshots:
'@amap/amap-jsapi-loader@1.0.1': {}
'@antfu/utils@0.7.10': {}
'@arco-design/color@0.4.0':
@ -9270,6 +9278,7 @@ snapshots:
print-js: 1.6.0
qrcode: 1.5.4
recorder-core: 1.3.25011100
sm-crypto: 0.3.13
sortablejs: 1.15.6
uuid: 11.1.0
vue: 3.5.15(typescript@5.8.3)

View File

@ -0,0 +1,56 @@
import axios from "@/api";
import { RegionPageParams, AddRegion } from "./types";
// 分页查询运营区域表
export const getRegionByPageAPI = (params: RegionPageParams) => {
return axios({
url: "/operations/ebikeRegion/page",
method: "get",
params
});
};
// 添加运营区域
export const addRegionAPI = (data: AddRegion) => {
return axios({
url: "/operations/ebikeRegion/save",
method: "post",
data
});
};
// 删除运营区域表
export const deleteRegionAPI = (regionId: string) => {
return axios({
url: `/operations/ebikeRegion/remove`,
method: "get",
params: { regionId }
});
};
// 更新运营区域表
export const updateRegionAPI = (data: AddRegion) => {
return axios({
url: "/operations/ebikeRegion/update",
method: "post",
data
});
};
// 开始运营
export const startRegionAPI = (regionId: string) => {
return axios({
url: `/operations/ebikeRegion/commenceOperation`,
method: "get",
params: { regionId }
});
};
// 停止运营
export const stopRegionAPI = (regionId: string) => {
return axios({
url: `/operations/ebikeRegion/stopOperation`,
method: "get",
params: { regionId }
});
};

View File

@ -0,0 +1,25 @@
interface ListType {
pageNum: number;
pageSize: number;
}
// 分页查询运营区域表参数
export interface RegionPageParams extends ListType {
regionName?: string; // 运营区名称
regionSimpleName?: string; //运营区简称
}
// 添加运营区域
type Coordinate = [number, number];
interface RegionPolygon {
type: "polygon" | "point";
coordinates: Coordinate[];
}
export interface AddRegion {
regionId?: string;
regionName: string;
regionPolygon: RegionPolygon;
regionSimpleName?: string | null;
status?: number;
operatorId: string;
}

View File

@ -5,6 +5,8 @@
// Generated by unplugin-auto-import
export {}
declare global {
const AMap: typeof import('@arco-design/web-vue')['Map']
const AMaps: typeof import('@arco-design/web-vue')['Maps']
const EffectScope: typeof import('vue')['EffectScope']
const Message: (typeof import("./globals/index"))["Message"]
const arcoMessage: typeof import('./globals/index')['arcoMessage']

View File

@ -25,6 +25,8 @@ import Layout from "@/layout/index.vue";
* @description mock/_data/system_menu
* @returns
*/
// @/views/login/login.vue
export const staticRoutes = [
{
path: "/",

42
src/utils/map-tools.ts Normal file
View File

@ -0,0 +1,42 @@
/**
*
*/
type Point = [number, number];
/**
*
*/
export const PointDataHandler = {
/**
*
* @param points - [, ]
* @returns
*/
prepareForUpload(points: Point[]): Point[] {
if (!Array.isArray(points) || points.length < 2) {
return points;
}
return [...points, points[0]];
},
/**
*
* @param points -
* @returns
*/
restoreForDisplay(points: Point[]): Point[] {
if (!Array.isArray(points) || points.length <= 1) {
return points;
}
const first = points[0];
const last = points[points.length - 1];
// 仅当最后一个点与第一个点完全相同时才移除
if (first[0] === last[0] && first[1] === last[1]) {
return points.slice(0, -1);
}
return points;
}
};

View File

@ -0,0 +1,219 @@
<script lang="ts" setup>
declare global {
interface Window {
_AMapSecurityConfig: any;
}
}
import AMapLoader from "@amap/amap-jsapi-loader";
const { VITE_GLOB_AMAP_KEY, VITE_GLOB_AMAP_SECURITY_JS_CODE } = import.meta.env; //
const emits = defineEmits(["ok"]);
const open = ref<boolean>(false);
//
const map = shallowRef<any>(null);
const mouseTool = shallowRef<any>(null);
const polyEditor = shallowRef<any>(null);
const path = ref<any>([]);
const mapAmap = shallowRef<any>(null);
const type = ref<string>("add");
const polygons = ref<any>([]);
const options__polygon = {
strokeColor: "#007bff",
strokeWeight: 3,
strokeOpacity: 1,
fillColor: "rgba(0, 123, 255, 0.1)",
fillOpacity: 0.8,
strokeStyle: "solid"
};
const openMap = async (parmas: any) => {
console.log("parmas", parmas);
try {
open.value = true;
await initMap();
if (parmas) {
const { points } = parmas;
path.value = points;
console.log("points", points);
type.value = "edit";
editMap();
return;
}
addPolygons();
} catch (err) {
console.error("err", err);
}
};
//
const initMap = () => {
return new Promise((resolve, reject) => {
window._AMapSecurityConfig = {
securityJsCode: VITE_GLOB_AMAP_SECURITY_JS_CODE // ,
};
AMapLoader.load({
key: VITE_GLOB_AMAP_KEY,
version: "2.0",
plugins: [
"AMap.Marker",
"AMap.Pixel",
"AMap.Polyline",
"AMap.Geolocation",
"AMap.MouseTool",
"AMap.Polygon",
"AMap.PolygonEditor"
]
})
.then((mapRes: any) => {
mapAmap.value = mapRes;
map.value = new mapRes.Map("container", {
zoom: 12,
center: [104.066541, 30.572269]
});
//
if (map.value) {
resolve(true);
}
})
.catch((e: any) => {
console.error(e);
reject(false);
});
});
};
//
function addPolygons() {
if (mapAmap.value) {
mouseTool.value = new mapAmap.value.MouseTool(map.value);
if (mouseTool.value) {
mouseTool.value.polygon({
...options__polygon
});
mouseTool.value.on("draw", onDraw);
}
}
}
function onDraw(e: any) {
const { obj } = e;
const mapPathList = obj.getPath();
path.value = [];
mapPathList.map((item: any) => {
path.value.push([item.lng, item.lat]);
});
}
//
function editMap() {
if (map.value) {
map.value.clearMap();
if (polyEditor.value) {
polyEditor.value.close();
}
const polygon = new mapAmap.value.Polygon(getPolygonOption());
map.value.add([polygon]);
polyEditor.value = new mapAmap.value.PolygonEditor(map.value, polygon);
polyEditor.value.open();
}
}
function getPolygonOption() {
return { ...options__polygon, path: path.value };
}
//
function clearMap() {
map.value.clearMap();
path.value = [];
}
//
function handleOk() {
const points = getPolygons();
if (points.length > 2) {
emits("ok", points);
afterClose();
return true;
}
arcoMessage("error", "请绘制一个有效的区域");
return false;
}
function getPolygons() {
switch (type.value) {
case "add":
return path.value;
case "edit":
const polygon = polyEditor.value.getTarget();
const paths = polygon.getPath();
return paths.map((item: any) => [item.lng, item.lat]);
default:
return [];
}
}
//
function afterClose() {
clearMap();
map.value = null;
mouseTool.value = null;
polyEditor.value = null;
mapAmap.value = null;
polygons.value = [];
type.value = "add";
}
defineExpose({
openMap
});
</script>
<template>
<a-modal v-model:visible="open" fullscreen :body-style="{ height: '100%' }" @before-ok="handleOk" @cancel="afterClose">
<template #title>
<div class="title_bar">
<div style="font-weight: bold">区域电子围栏编辑</div>
<div style="color: red; font-weight: bold">在地图上请点击鼠标左键进行绘制多边形完成绘制后请点击鼠标右键</div>
</div>
</template>
<div class="map_bar">
<div id="container" style="height: 100%; width: 100%"></div>
</div>
<!-- 按钮区域 -->
<div class="button_bar">
<a-button type="primary" style="width: 100%" @click="clearMap" v-if="type == 'add'">清除</a-button>
<a-button type="primary" style="width: 100%" @click="editMap" v-if="type == 'edit'">复原</a-button>
</div>
</a-modal>
</template>
<style scoped lang="scss">
.title_bar {
width: 100%;
display: flex;
justify-content: space-between;
padding-right: 50px;
}
.map_bar {
width: 100%;
height: 100%;
}
.button_bar {
position: absolute;
z-index: 9999;
width: 200px;
background-color: #fffc;
padding: 15px;
right: 50px;
bottom: 60px;
}
</style>

View File

@ -0,0 +1,116 @@
<script lang="ts" setup>
import { ref } from "vue";
import RegionalMap from "@/views/component/RegionalMap/RegionalMap.vue";
import { deepClone } from "@/utils";
const props = defineProps({
operatorList: {
type: Array,
default: () => []
}
});
const addFormRef = ref<any>();
const operatorAllList = ref<any>([]);
const addForm = reactive<any>({
regionId: "",
operatorId: "",
regionName: "",
regionSimpleName: "",
coordinates: []
});
const rules = {
operatorId: [{ required: true, message: "请选择运营商", trigger: "change" }],
regionName: [{ required: true, message: "请输入运营区名称", trigger: "blur" }],
coordinates: [
{
required: true,
validator: (value: any, cb: any) => {
console.log(value);
if (value.length === 0) {
cb("请编辑区域电子围栏");
} else {
cb();
}
}
}
]
};
const RegionalMapRef = ref();
const handleOpenMap = () => {
const path = addForm.coordinates.length === 0 ? null : { points: deepClone(addForm.coordinates) };
RegionalMapRef.value?.openMap(path);
};
const updateForm = (data: any) => {
addForm.regionId = data.regionId;
addForm.operatorId = data.operatorId;
addForm.regionName = data.regionName;
addForm.regionSimpleName = data.regionSimpleName;
addForm.coordinates = data.coordinates || [];
};
//
const getPoint = (paths: any[]) => {
addForm.coordinates = paths;
};
//
const submit = () => {
return new Promise(async resolve => {
let status = await addFormRef.value.validate();
return resolve({
status,
data: deepClone(addForm)
});
});
};
const resetForm = () => {
addForm.regionId = "";
addForm.operatorId = "";
addForm.regionName = "";
addForm.regionSimpleName = "";
addForm.coordinates = [];
if (addFormRef.value) {
addFormRef.value.resetFields();
}
};
watch(
() => props.operatorList,
val => {
operatorAllList.value = val;
},
{ immediate: true }
);
defineExpose({
submit,
resetForm,
updateForm
});
</script>
<template>
<a-form ref="addFormRef" :model="addForm" :rules="rules">
<a-form-item field="operatorId" label="运营商" validate-trigger="blur">
<a-select v-model="addForm.operatorId" placeholder="请选择运营商" validate-trigger="change">
<a-option v-for="it in operatorAllList" :key="it.operatorId" :value="it.operatorId">{{ it.operatorName }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="运营区名称" field="regionName" validate-trigger="blur">
<a-input placeholder="请输入运营区名称" allow-clear v-model="addForm.regionName" />
</a-form-item>
<a-form-item label="简称" field="regionSimpleName">
<a-input placeholder="请输入简称" allow-clear v-model="addForm.regionSimpleName" />
</a-form-item>
<a-form-item label="区域电子围栏" field="coordinates" validate-trigger="blur">
<a-tag bordered color="orange" v-if="addForm.coordinates.length === 0">未绘制</a-tag>
<a-tag bordered color="green" v-else>已绘制</a-tag>
<a-link @click="handleOpenMap" style="margin-left: 15px">编辑地图</a-link>
</a-form-item>
</a-form>
<RegionalMap ref="RegionalMapRef" @ok="getPoint" />
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,377 @@
<script lang="ts" setup>
import AddForm from "./component/addForm.vue";
import {
getRegionByPageAPI,
addRegionAPI,
deleteRegionAPI,
updateRegionAPI,
startRegionAPI,
stopRegionAPI
} from "@/api/modules/region";
import { deepClone, dictTranslate } from "@/utils";
import { useSystemStore } from "@/store/modules/system";
import { PointDataHandler } from "@/utils/map-tools";
import { getOperatorAllListAPI } from "@/api/modules/system";
import { Modal } from "@arco-design/web-vue";
//
const form = ref<any>({
regionSimpleName: "",
regionName: ""
});
const system = useSystemStore();
const moduleList = ref<any>([]);
const regionStatusList = ref<any>([]);
const loading = ref<boolean>(false);
const tableRef = ref();
const AddFormRef = ref<any>(null);
const operatorAllList = ref<any>([]);
const title = ref<string>("");
const pagination = ref({
pageNum: 1,
total: 0,
pageSize: 10,
showPageSize: true,
showTotal: true
});
const handlePageSizeChange = (pageSize: number) => {
pagination.value.pageSize = pageSize;
pagination.value.pageNum = 1;
getRegionByPage();
};
const handlePageChange = (page: number) => {
pagination.value.pageNum = page;
getRegionByPage();
};
const search = () => {
pagination.value.pageNum = 1;
getRegionByPage();
};
const reset = () => {
form.value = {
regionSimpleName: "",
regionName: ""
};
search();
};
const onAdd = () => {
open.value = true;
title.value = "新增运营区";
};
const getRegionByPage = async () => {
loading.value = true;
try {
const res: any = await getRegionByPageAPI({
regionName: form.value.regionName,
regionSimpleName: form.value.regionSimpleName,
pageNum: pagination.value.pageNum,
pageSize: pagination.value.pageSize
});
loading.value = false;
if (res.code !== 200) {
return;
}
const { records, pageNumber, pageSize } = res.data;
pagination.value.pageNum = pageNumber;
pagination.value.pageSize = pageSize;
pagination.value.total = res.data.totalRow;
moduleList.value = records;
} catch (error) {
console.error("获取列表失败:", error);
} finally {
loading.value = false;
}
};
// tag
const tagColor = (record: any) => {
const { status } = record;
switch (status) {
case 0:
return "gray";
case 1:
return "green";
case 2:
return "red";
default:
return "gray";
}
};
//
const dictFormat = (list: any[], key: string, value: string | number, valueKey: string = "dicValueName") => {
return dictTranslate(list, key, value, valueKey);
};
//
const getAllOperatorList = async () => {
return new Promise<void>(async resolve => {
try {
const res: any = await getOperatorAllListAPI();
if (res.code === 200) {
operatorAllList.value = res.data;
resolve();
}
} catch (error) {
console.error("获取运营商列表失败:", error);
resolve();
}
});
};
//
const handleDelete = async (record: any) => {
const { regionId } = record;
try {
const res: any = await deleteRegionAPI(regionId);
if (res.code === 200) {
arcoMessage("success", "删除成功");
getRegionByPage();
}
} catch (error) {
console.error("删除失败:", error);
}
};
const handleSelect = (val: any, record: any) => {
switch (val) {
case "start":
handleStart(record);
break;
case "stop":
handleStop(record);
break;
default:
break;
}
};
//
const handleStart = (record: any) => {
const { regionId } = record;
Modal.info({
title: "开始运营",
content: "请确认是否启动运营区开始运营?",
hideCancel: false,
closable: true,
onBeforeOk: async () => {
try {
await startRegionAPI(regionId);
arcoMessage("success", "启动成功");
reset();
return true;
} catch (error) {
console.error("启动运营区失败:", error);
return false;
}
}
});
};
//
const handleStop = (record: any) => {
const { regionId } = record;
Modal.error({
title: "停止运营",
content: "请确认是否停止运营区运营?",
hideCancel: false,
closable: true,
onBeforeOk: async () => {
try {
await stopRegionAPI(regionId);
arcoMessage("success", "停止成功");
reset();
return true;
} catch (error) {
console.error("停止运营区失败:", error);
return false;
}
}
});
};
/**
*新增模态框
* **/
const open = ref<boolean>(false);
const afterClose = () => {
resetForm();
};
//
const handleUpdate = (record: any) => {
open.value = true;
title.value = "修改运营区";
const data = deepClone(record);
data.coordinates = PointDataHandler.restoreForDisplay(data.regionPolygon.coordinates);
AddFormRef.value.updateForm(data);
};
//
const formatData = (data: any, type: string = "add") => {
if (type === "add") {
const regionPolygon: any = {
type: "Polygon",
coordinates: []
};
regionPolygon.coordinates = PointDataHandler.prepareForUpload(data.coordinates);
return {
operatorId: data.operatorId,
regionName: data.regionName,
regionId: data.regionId ? data.regionId : "",
regionSimpleName: data.regionSimpleName,
regionPolygon
};
}
if (type === "edit") {
}
};
//
const resetForm = () => {
open.value = false;
AddFormRef.value?.resetForm();
};
//
const handleOk = async () => {
const { status, data } = await AddFormRef.value?.submit();
if (status) return false;
try {
const parmas = formatData(data, "add");
if (!parmas) {
arcoMessage("error", "数据格式有误");
return false;
}
if (data.regionId) {
await updateRegionAPI(parmas);
} else {
await addRegionAPI(parmas);
}
arcoMessage("success", data.regionId ? "修改成功" : "新增成功");
resetForm();
getRegionByPage();
return false;
} catch (error) {
console.error(error);
return false;
}
};
onMounted(() => {
getRegionByPage();
});
onBeforeMount(async () => {
regionStatusList.value = deepClone(system.getDictByCode("regionStatus"));
await getAllOperatorList();
});
</script>
<template>
<div class="snow-page">
<div class="snow-inner">
<s-layout-tools>
<template #left>
<a-space wrap>
<a-input v-model="form.regionName" placeholder="运营区名称" allow-clear />
<a-input v-model="form.regionSimpleName" placeholder="运营区简称" allow-clear />
<a-button type="primary" @click="search">
<template #icon><icon-search /></template>
<span>查询</span>
</a-button>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<span>重置</span>
</a-button>
</a-space>
</template>
<template #right>
<a-space wrap>
<a-button type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<span>新增</span>
</a-button>
</a-space>
</template>
</s-layout-tools>
<a-table
ref="tableRef"
row-key="batteryId"
:data="moduleList"
:bordered="{ cell: true }"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #columns>
<a-table-column title="序号" :width="60" align="center">
<template #cell="cell">
<span>{{ (pagination.pageNum - 1) * pagination.pageSize + cell.rowIndex + 1 }}</span>
</template>
</a-table-column>
<a-table-column title="运营区名称" data-index="regionName" align="center"></a-table-column>
<a-table-column title="运营区简称" data-index="regionSimpleName" align="center"></a-table-column>
<a-table-column title="运营商" data-index="operatorId" align="center">
<template #cell="{ record }">
<span>{{ dictFormat(operatorAllList, "operatorId", record.operatorId, "operatorName") }}</span>
</template>
</a-table-column>
<a-table-column title="状态" data-index="status" align="center">
<template #cell="{ record }">
<a-tag :color="tagColor(record)" bordered>{{ dictFormat(regionStatusList, "dicValue", record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="创建时间" data-index="createdAt" align="center"></a-table-column>
<a-table-column title="操作" align="center" :fixed="'right'">
<template #cell="{ record }">
<a-space>
<a-button type="primary" size="mini" @click="handleUpdate(record)" :disabled="record.status === 1">
<template #icon><icon-link /></template>
<span>修改</span>
</a-button>
<a-popconfirm type="warning" content="确定删除该运营区?" @ok="handleDelete(record)">
<a-button type="primary" status="danger" size="mini">
<template #icon><icon-delete /></template>
<span>删除</span>
</a-button>
</a-popconfirm>
<a-dropdown @select="(v: any) => handleSelect(v, record)">
<a-button size="mini" type="primary">
更多
<template #icon><icon-down /></template>
</a-button>
<template #content>
<a-doption value="start" :disabled="record.status === 1">开始运营</a-doption>
<a-doption value="stop" :disabled="record.status !== 1">停止运营</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</a-table-column>
</template>
</a-table>
</div>
<!-- 新增 -->
<a-modal v-model:visible="open" @close="afterClose" @before-ok="handleOk" @cancel="afterClose" :width="700">
<template #title> {{ title }} </template>
<AddForm ref="AddFormRef" :operatorList="operatorAllList" />
</a-modal>
</div>
</template>
<style scoped lang="scss"></style>