Commit 3d8b0a0b authored by daniel's avatar daniel

fix(location): 修复赛事配置

parent 25984274
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fns" uri="/WEB-INF/tlds/fns.tld" %>
<%@ taglib prefix="fnc" uri="/WEB-INF/tlds/fnc.tld" %>
<c:set var="ctx" value="${pageContext.request.contextPath}${fns:getAdminPath()}" />
<c:set var="path" value="${pageContext.request.contextPath}${fns:getPath()}" />
<c:set var="jaaproot" value="${pageContext.request.contextPath}" />
<c:set var="ctxStatic" value="${pageContext.request.contextPath}/static" />
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>领物导航</title>
<!-- ... 保留原有的 Vue 引入 ... -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- 添加 Naive UI 的引入 -->
<!-- 会使用最新版本,你最好指定一个版本 -->
<script src="https://unpkg.com/naive-ui"></script>
<!-- <script nomodule
src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> -->
<script type="module"
src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule
src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<!-- Favicon-->
<link rel="shortcut icon" href="https://img.geexek.com/files/home/favicon.ico">
<!-- 自定义css -->
<link rel="stylesheet" href="${ctxStatic}/css/base.css">
<!-- Bootstrap CSS-->
<link rel="stylesheet"
href="${ctxStatic}/newdermon/vendor/bootstrap/css/bootstrap.min.css">
<!-- theme stylesheet-->
<link rel="stylesheet"
href="${ctxStatic}/newdermon/css/style.default.css?v=<%=System.currentTimeMillis()%>"
id="theme-stylesheet">
<!-- Custom stylesheet - for your changes-->
<link rel="stylesheet"
href="${ctxStatic}/newdermon/css/custom.css?v=<%=System.currentTimeMillis()%>">
<!-- Favicon-->
<link rel="shortcut icon" href="https://img.geexek.com/files/home/favicon.ico">
<link rel="stylesheet"
href="${ctxStatic}/cmpt/setting.css?v=<%=System.currentTimeMillis()%>">
<script src="${ctxStatic}/newdermon/vendor/jquery/jquery.min.js"></script>
<style>
#app {
padding: 0 20px 20px 20px;
}
.layout {
min-height: 100vh;
}
.carousel-img {
width: 100%;
height: 240px;
object-fit: cover;
}
[v-cloak] {
display: none;
}
.n-message .n-message__icon>* {
position: absolute;
left: 0;
top: -5px !important;
right: 0;
bottom: 0;
}
</style>
</head>
<body>
<jsp:include page="../common/header.jsp"></jsp:include>
<div class="page-content d-flex align-items-stretch main-body">
<jsp:include page="../common/menu.jsp"></jsp:include>
<div id="app" class="flex-1" v-cloak>
<n-config-provider :locale="locale" :date-locale="dateLocale">
<n-modal-provider>
<n-message-provider>
<n-notification-provider>
<my-app></my-app>
</n-notification-provider>
</n-message-provider>
</n-modal-provider>
</n-config-provider>
</div>
</div>
<script type="text/x-template" id="my-app">
<n-space vertical >
<!-- 顶部的搜索框 -->
<n-card :bordered="false">
<div class="w-full flex flex-row items-center gap-x-2">
<div style="width: 200px">
<n-tabs type="segment" @update:value="updateStatus" :value="query.status" animated pane-style="padding:0;">
<n-tab-pane v-for="item in [{name:'全部',value:''},...StatusEnum.data.map(v=>({name:v.name,value:v.value}))]" :name="item.value" :tab="item.name"></n-tab-pane>
</n-tabs>
</div>
<n-button type="primary" @click="edit(null)">新增分类</n-button>
<div class="ml-auto">
<n-input v-model:value="keyword" placeholder="请输入搜索内容" :clearable="true" @change="fetchData" />
</div>
</div>
</n-card>
<!-- 搜索框 -->
<n-data-table :columns="columns" :loading="loading" default-expand-all :row-key="rowKey" :data="listTree" :pagination="pagination"
:bordered="false"
:remote="true"/>
<n-modal :mask-closable="false" :loading="loading" style="width: 600px" :title="formData.id?'编辑分类':'新增分类'"
preset="card"
draggable v-model:show="showModal" >
<n-form
ref="formRef"
:model="formData"
:rules="rules"
size="small"
label-placement="left"
label-width="auto"
>
<n-form-item label="分类名" path="name">
<n-input v-model:value="formData.name" placeholder="请输入分类" />
</n-form-item>
<n-form-item label="分类图标" path="icon">
<n-upload
:action="uploadUrl"
:max="1"
v-model:file-list="fileList"
list-type="image-card"
@finish="uploadFinish"
>
点击上传
</n-upload>
</n-form-item>
<n-form-item label="排序" path="sortOrder">
<n-input-number v-model:value="formData.sortOrder" placeholder="请输入排序" />
</n-form-item>
<n-form-item label="上级" path="parentId" >
<n-tree-select v-model:value="formData.parentId" placeholder="请选择上级" :options="treeData"/>
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formData.status" name="radiogroup">
<n-space>
<n-radio v-for="item in StatusEnum.data" :key="item.value" :value="item.value">
{{ item.name }}
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
</n-form>
<template #footer>
<div class="flex gap-x-2 flex-row items-center" >
<n-button class="flex-1" :loading="loading" @click="onCancel">
取消
</n-button>
<n-button class="flex-1"
:loading="loading" type='primary' @click="onSubmit">
保存
</n-button>
</div>
</template>
</n-modal>
<n-modal
v-model:show="showDelModal"
preset="dialog"
title="确认"
content="你确定要删除该分类吗?"
positive-text="确认"
negative-text="算了"
@positive-click="onDelete"
/>
</n-space>
</script>
<script>
// 枚举class
class SuperEnum {
constructor(param) {
const keyObj = {};
const valueObj = {};
const nameObj = {};
if (!Array.isArray(param)) {
throw new TypeError('param is not an array!');
}
param.forEach((element) => {
if (!element.key || !element.name) {
return;
}
// 使用对象存储键值对,方便快速查找
keyObj[element.key] = element;
valueObj[element.value] = element;
nameObj[element.name] = element;
// 动态添加属性
this[element.key] = element;
this[element.name] = element;
if (element.key !== element.value) {
this[element.value] = element;
}
});
// 保存源数组
this.data = param;
this.keyObj = keyObj;
this.valueObj = valueObj;
this.nameObj = nameObj;
// 防止被修改
Object.freeze(this);
}
getColorByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.color;
}
getData() {
return this.data;
}
getKeyByName(name) {
const prop = this.nameOf(name);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.key;
}
getNameByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.name;
}
getNameByValue(value) {
const prop = this.valueOf(value);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.name;
}
getValueByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.value;
}
getValueByName(name) {
const prop = this.nameOf(name);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.value;
}
keyOf(key) {
return this.keyObj[key];
}
nameOf(name) {
return this.nameObj[name];
}
valueOf(value) {
return this.valueObj[value];
}
}
// to 方法,封装try catch
const to = async (
promise,
errorExt,
) => {
try {
const data = await promise
const result = [null, data]
return result
} catch (error) {
if (errorExt) {
const parsedError = Object.assign({}, error, errorExt)
return [parsedError, undefined]
}
return [error, undefined]
}
}
// 状态
const StatusEnum =
new SuperEnum([
{ key: 'OPEN', name: '启用', color: 'success', value: 1 },
// 增值
{ key: 'CLOSE', name: '禁用', color: 'warning', value: 0 },
])
// 添加树形数据构建方法
const buildTreeData = (categories) => {
// 创建分类ID到节点的映射
const nodeMap = {}
// 创建顶级节点(保持您现有的顶级节点结构)
const root = {
label: '顶级',
key: 0,
children: []
}
nodeMap[0] = root
// 首先创建所有节点并添加到映射
categories.forEach(category => {
const node = {
label: category.name,
key: category.id,
icon: category.icon || undefined,
children: []
}
nodeMap[category.id] = node
})
// 然后建立父子关系
categories.forEach(category => {
const node = nodeMap[category.id]
const parentId = category.parentId || 0 // 默认为顶级节点的子节点
const parentNode = nodeMap[parentId] || root
if (parentNode) {
parentNode.children.push(node)
// 按sortOrder排序子节点
parentNode.children.sort((a, b) => (a.sortOrder || 100) - (b.sortOrder || 100))
}
})
// 创建包含顶级节点和其子节点的同级数组
const topLevelNodes = [root, ...root.children];
// 清空顶级节点的children(避免重复显示)
root.children = [];
// 对所有顶级节点按sortOrder排序
topLevelNodes.sort((a, b) => (a.sortOrder || 100) - (b.sortOrder || 100));
return topLevelNodes;
}
// 新增:将categories转换为标准树形结构(子级嵌套在children中)
const buildNestedTreeData = (categories) => {
const nodeMap = {};
const rootNodes = [];
// 1. 创建所有节点并存储到映射表
categories.forEach(category => {
const node = {
...category, // 复制原始属性(id, parentId, name等)
children: [] // 初始化children数组
};
nodeMap[category.id] = node;
});
// 2. 建立父子关系
categories.forEach(category => {
const node = nodeMap[category.id];
const parentId = category.parentId;
if (parentId === null || parentId === undefined || !nodeMap[parentId]) {
// 无父节点或父节点不存在,作为根节点
rootNodes.push(node);
} else {
// 添加到父节点的children数组
nodeMap[parentId].children.push(node);
// 按sortOrder排序子节点
nodeMap[parentId].children.sort((a, b) =>
(a.sortOrder || 100) - (b.sortOrder || 100)
);
}
});
// 3. 对根节点按sortOrder排序
return rootNodes.sort((a, b) =>
(a.sortOrder || 100) - (b.sortOrder || 100)
);
}
</script>
<script>
const { createApp, defineComponent, reactive, ref, onMounted, h } = Vue;
const { useMessage, useNotification, dateZhCN, NButton, NTag, zhCN, NSpace, NImageGroup, NImage, NText, NSwitch } = naive
// 添加全局 loading 指令
const uploadUrl = window.location.origin + '/api-common/v1/upload?folder=' + 'gk/location' + '&cmptId=' + getCId()
const MyApp = defineComponent({
name: 'MyApp',
template: '#my-app',
setup() {
const message = useMessage()
const notification = useNotification()
const loading = ref(false)
/***----------------分类相关 start -----------------*/
const columns = [
{
title: '分类名称',
key: 'name',
},
{
title: 'parent',
key: 'parentId',
render: (row) => {
return h(NText, {
}, row.parentId === 0 ? '顶级' : list.value.find(v => v.id === row.parentId)?.name ?? '--')
}
},
{
title: '排序',
key: 'sortOrder',
},
{
title: '图标',
key: 'icon',
render: (row) => {
return h(NImage, {
height: "50",
src: row.icon
})
}
},
{
title: '状态',
key: 'status',
width: 250,
render: (row) => {
// return h(NSpace, {
// vertical: true,
// }, [
// h(NSpace, {}, [
// h(NTag, {
// size: "small",
// type: StatusEnum[row.status].color
// }, StatusEnum[row.status].name),
// ]),
// ])
return h(NSwitch, {
defaultValue: row.status,
uncheckedValue: StatusEnum.CLOSE.value,
checkedValue: StatusEnum.OPEN.value,
'onUpdate:value': async (val) => {
// 更新数据
const params = {
id: row.id,
status: val
}
loading.value = true
await fetch('/gk/location-category', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
})
message.success("更新成功")
loading.value = false
row.status = val
}
})
}
},
{
title: '操作',
key: 'actions',
width: 250,
align: 'center',
render: (row) => {
// 两个按钮,一个 编辑 一个 删除
return h(NSpace, {}, [
h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
// 编辑
edit(row)
}
}, '编辑'),
h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
let params = window.location.href.split('?')[1]
window.location.href = '/gk/location-point/index/' + getCId() + '?categoryId=' + row.id + "&" + params
}
}, '地图标点'),
h(NButton, {
type: 'error',
size: 'small',
onClick: () => {
editRecord.value = row
showDelModal.value = true
}
}, '删除')
])
}
}
]
// 分页
const pagination = reactive({
pageSize: 20,
page: 1,
pageCount: 0,
showSizePicker: true,
itemCount: 0,
pageSizes: [20, 50, 200, 500],
prefix: (info) => "共" + info.itemCount + "条数据",
onChange: (page) => {
pagination.page = page
fetchData()
},
onUpdatePageSize: (pageSize) => {
pagination.pageSize = pageSize
pagination.page = 1
fetchData()
}
})
// 查询的参数
const query = reactive({
status: '',
})
const keyword = ref('')
// 结果列表
const fileList = ref([])
// 分类弹框
const showModal = ref(false)
// 删除分类
const showDelModal = ref(false)
const editRecord = ref()
// 分类的formRef
const formRef = ref(null)
// 分类的数据列表
const list = ref([])
const listTree = ref([])
// 分类的树数据
const treeData = ref([])
// 分类表单
const formData = reactive({
id: null,
parentId: 0,
name: "",
icon: "",
sortOrder: 100,
status: StatusEnum.OPEN.value,
})
// 分类的校验规则
const rules = {
name: {
required: true,
message: '请输入分类名称',
trigger: 'blur',
},
}
// 状态更新
const updateStatus = (value) => {
pagination.page = 1
query.status = value
fetchData()
}
// 上传完成
const uploadFinish = ({
file,
event
}) => {
const response = JSON.parse(event?.target?.response)
file.url = response.content
return file
}
// 重置form数据
const resetForm = () => {
Object.assign(formData, {
id: null,
parentId: 0,
name: "",
icon: "",
sortOrder: 100,
status: StatusEnum.OPEN.value,
})
}
// 删除分类
const onDelete = () => {
if (!editRecord.value) {
showDelModal.value = false
return
}
loading.value = true
const messageReactive = message.loading('删除中...', {
duration: 0
})
// 发起请求
fetch("/gk/location-category/" + editRecord.value.id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(data => {
if (data.code === 200) {
message.success('删除成功')
fetchData()
} else {
message.error(data.msg)
}
}).finally(() => {
loading.value = true
messageReactive.destroy()
showDelModal.value = false
})
}
// 编辑分类
const edit = (record) => {
resetForm()
fileList.value = []
editRecord.value = record
// 数据初始化
record && Object.assign(formData, record)
if (record?.icon) {
fileList.value = [
{
url: record?.icon,
status: 'finished',
name: record?.icon,
id: record.id,
uid: record.id
}
]
}
showModal.value = true
}
// 取消操作
const onCancel = () => {
showModal.value = false
resetForm()
}
// 提交
const onSubmit = (e) => {
e.preventDefault()
formRef.value?.validate(async (errors) => {
if (errors) return message.error('请检查表单是否填写完整')
// 使用fetch发起请求
const params = {
...editRecord.value,
...formData,
cmptId: getCId(),
icon: fileList.value.map(v => v.url).join()
}
loading.value = true
const messageReactive = message.loading(formData.id ? '数据更新中...' : '数据添加中...', {
duration: 0
})
if (editRecord.value && editRecord.value.parentId === editRecord.value.id) {
return message.error('不能将分类设置为自己的父分类')
}
// 发起请求
const [err, data] = await to(fetch("/gk/location-category", {
method: editRecord.value ? 'PUT' : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(res => res.json()))
loading.value = false
messageReactive.destroy()
if (data.code === 200) {
message.success('操作成功')
showModal.value = false
resetForm()
fetchData()
} else {
message.error(data.msg)
}
})
}
// 获取分类分页列表
const fetchData = async () => {
loading.value = true
let params = {
...query,
page: pagination.page,
size: pagination.pageSize,
cmptId: getCId(),
keyword: keyword.value
}
// 发起请求
const [err, res] = await to(fetch("/gk/location-category/page", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(res => res.json()))
loading.value = false
if (res.code === 200) {
list.value = res.content.content
listTree.value = buildNestedTreeData(res.content.content)
treeData.value = buildTreeData(res.content.content)
pagination.itemCount = res.content.total
} else {
message.error(res.msg)
}
}
/***---------------- 分类相关 end -----------------*/
onMounted(() => {
fetchData()
})
return {
// 分类相关
updateStatus,
showModal,
formRef,
formData,
onCancel,
onSubmit,
fileList,
listTree,
rowKey: (row) => row.id,
query,
pagination,
columns,
onDelete,
showDelModal,
rules,
fileList,
keyword,
fetchData,
treeData,
edit,
// 公共数据
uploadUrl,
uploadFinish,
loading,
StatusEnum,
}
}
})
const app = Vue.createApp({
setup() {
return {
locale: zhCN,
dateLocale: dateZhCN
}
}
})
app.component('my-app', MyApp)
app.use(naive)
app.mount('#app')
</script>
</body>
</html>
\ No newline at end of file
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fns" uri="/WEB-INF/tlds/fns.tld" %>
<%@ taglib prefix="fnc" uri="/WEB-INF/tlds/fnc.tld" %>
<c:set var="ctx" value="${pageContext.request.contextPath}${fns:getAdminPath()}" />
<c:set var="path" value="${pageContext.request.contextPath}${fns:getPath()}" />
<c:set var="jaaproot" value="${pageContext.request.contextPath}" />
<c:set var="ctxStatic" value="${pageContext.request.contextPath}/static" />
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点位标注</title>
<!-- ... 保留原有的 Vue 引入 ... -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- 添加 Naive UI 的引入 -->
<!-- 会使用最新版本,你最好指定一个版本 -->
<script src="https://unpkg.com/naive-ui"></script>
<script type="module"
src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule
src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<!-- 添加Mapbox GL JS CSS -->
<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet">
<!-- 添加Mapbox GL JS -->
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<!-- Favicon-->
<link rel="shortcut icon" href="https://img.geexek.com/files/home/favicon.ico">
<!-- 自定义css -->
<link rel="stylesheet" href="${ctxStatic}/css/base.css">
<!-- 添加天地图API -->
<script
src="https://api.tianditu.gov.cn/api?v=4.0&tk=16a595e56ea0cec57ad8c07368d7f1ac"></script>
<!-- 引入togeojson -->
<script src="https://unpkg.com/@tmcw/togeojson@7.1.2/dist/togeojson.umd.js"></script>
<!-- 引入turf -->
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
<style>
#app {
padding: 20px;
}
.layout {
min-height: 100vh;
}
.carousel-img {
width: 100%;
height: 240px;
object-fit: cover;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="page-content d-flex align-items-stretch main-body">
<div id="app" class="flex-1" v-cloak>
<n-config-provider :locale="locale" :date-locale="dateLocale">
<n-modal-provider>
<n-message-provider>
<n-notification-provider>
<my-app></my-app>
</n-notification-provider>
</n-message-provider>
</n-modal-provider>
</n-config-provider>
</div>
</div>
<script type="text/x-template" id="my-app">
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0; padding: 0;">
<div id="map" style="width: 100%; height: 100%;z-index: 0;"></div>
<div class="fixed left-0 top-0 z-10 flex flex-row justify-center items-center p-3">
<!-- <input
type="file"
accept=".gpx,.kml"
@change="handleFileChange"
style="display: none"
ref="fileInputRef"
/>
<n-card>
<div class="flex flex-col items-start gap-y-2">
<n-button @click="fileInputRef?.click()" type="primary">
导入轨迹
</n-button>
<span class="text-gray text-sm">
只能上传 GPX 或 KML 文件,且不超过 10M。
</span>
<span class="text-gray text-blue-600" v-if="fileName">
{{ fileName }}
</span>
</div>
</n-card> -->
<n-button @click="onBack" type="primary">
返回
</n-button>
</div>
<div class="fixed right-3 top-0 z-10 p-3 flex flex-col gap-y-1">
<div class="flex flex-row items-center mb-2 w-full gap-x-1 bg-white p-2 rounded-sm">
<span>点位拾取</span>
<n-switch v-model:value="isAddMode" >
<template #checked>
开启
</template>
<template #unchecked>
关闭
</template>
</n-switch>
</div>
<n-collapse accordion class="bg-white p-2 rounded-sm" style="min-width:250px;">
<n-collapse-item title="点位列表" >
<n-space vertical style="width:100%;">
<div
v-for="(marker, index) in markers"
:key="marker.id"
class="flex flex-row items-center gap-x-2 py-1"
>
<span class="flex flex-1 flex-row items-center gap-x-1 text-xs">
<span class="line-clamp-1 flex-1 font-bold text-md">
{{ marker.name }}
</span>
<n-tag size="small" :bordered="false" :type="StatusEnum[marker.status].color">
{{ StatusEnum[marker.status].name }}
</n-tag>
</span>
<ion-icon @click="editCheckPoint(marker)" class="cursor-pointer" name="create-outline"></ion-icon>
<n-popconfirm
@positive-click="deleteCheckPoint(index)"
>
<template #trigger>
<ion-icon class="text-red cursor-pointer" name="trash-outline"></ion-icon>
</template>
是否删除该标注点
</n-popconfirm>
</div>
</n-space>
</n-collapse-item>
</n-collapse>
</div>
<n-modal
:mask-closable="false"
:loading="loading"
style="width: 400px"
:title="isEditMarker?'编辑标记点':'新增标记点'"
preset="card"
draggable v-model:show="showModal"
>
<n-form
ref="markerFormRef"
:model="markerForm"
:rules="markerRules"
label-placement="left"
label-width="auto"
>
<n-form-item label="点位名称" path="name">
<n-input v-model:value="markerForm.name" placeholder="请输入点位名称" />
</n-form-item>
<n-form-item label="排序" path="sortOrder">
<n-input-number v-model:value="markerForm.sortOrder" placeholder="请输入排序" />
</n-form-item>
<n-form-item label="经度" path="longitude">
<n-input v-model:value="markerForm.longitude" placeholder="请输入经度" />
</n-form-item>
<n-form-item label="纬度" path="latitude">
<n-input v-model:value="markerForm.latitude" placeholder="请输入纬度" />
</n-form-item>
<n-form-item label="分类图标" path="icon">
<n-upload
:action="uploadUrl"
:max="1"
v-model:file-list="fileList"
list-type="image-card"
@finish="uploadFinish"
>
点击上传
</n-upload>
<span>推荐像素50x50px</span>
</n-form-item>
<n-form-item label="描述" path="description">
<n-input v-model:value="markerForm.description" placeholder="请输入描述" />
</n-form-item>
<n-form-item label="状态" path="status">
<n-switch
:checked-value="1"
:unchecked-value="0"
v-model:value="markerForm.status"
>
<template #checked>
开启
</template>
<template #unchecked>
关闭
</template>
</n-switch>
</n-form-item>
</n-form>
<template #footer>
<div class="flex gap-x-2 flex-row items-center" >
<n-button class="flex-1" :loading="loading" @click="onMarkerCancel">
取消
</n-button>
<n-button class="flex-1"
:loading="loading" type='primary' @click="onSubmit">
保存
</n-button>
</div>
</template>
</n-modal>
</div>
</script>
<script>
const cmpt = JSON.parse('${fns:toJson(cmpt)}');
// 枚举class
class SuperEnum {
constructor(param) {
const keyObj = {};
const valueObj = {};
const nameObj = {};
if (!Array.isArray(param)) {
throw new TypeError('param is not an array!');
}
param.forEach((element) => {
if (!element.key || !element.name) {
return;
}
// 使用对象存储键值对,方便快速查找
keyObj[element.key] = element;
valueObj[element.value] = element;
nameObj[element.name] = element;
// 动态添加属性
this[element.key] = element;
this[element.name] = element;
if (element.key !== element.value) {
this[element.value] = element;
}
});
// 保存源数组
this.data = param;
this.keyObj = keyObj;
this.valueObj = valueObj;
this.nameObj = nameObj;
// 防止被修改
Object.freeze(this);
}
getColorByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.color;
}
getData() {
return this.data;
}
getKeyByName(name) {
const prop = this.nameOf(name);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.key;
}
getNameByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.name;
}
getNameByValue(value) {
const prop = this.valueOf(value);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.name;
}
getValueByKey(key) {
const prop = this.keyOf(key);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.value;
}
getValueByName(name) {
const prop = this.nameOf(name);
if (!prop) {
const error = new Error('No enum constant');
error.code = 'ENUM_NOT_FOUND';
throw error;
}
return prop.value;
}
keyOf(key) {
return this.keyObj[key];
}
nameOf(name) {
return this.nameObj[name];
}
valueOf(value) {
return this.valueObj[value];
}
}
// to 方法,封装try catch
const to = async (
promise,
errorExt,
) => {
try {
const data = await promise
const result = [null, data]
return result
} catch (error) {
if (errorExt) {
const parsedError = Object.assign({}, error, errorExt)
return [parsedError, undefined]
}
return [error, undefined]
}
}
const tk = "16a595e56ea0cec57ad8c07368d7f1ac"
// 地图用到的图标
const mapIcons = {
0: 'https://geexekimgoss-sxk.oss-cn-hangzhou.aliyuncs.com/gk/common/map-icon/icon-cp.png',
1: 'https://geexekimgoss-sxk.oss-cn-hangzhou.aliyuncs.com/gk/common/map-icon/icon-start.png',
2: 'https://geexekimgoss-sxk.oss-cn-hangzhou.aliyuncs.com/gk/common/map-icon/icon-end.png'
}
// 地图图标枚举
const MapIconEnum = {
'ICON_CP': mapIcons[0],
'ICON_START_POINT': mapIcons[1],
'ICON_END_POINT': mapIcons[2],
}
// 打卡点类型
const CheckPointTypeEnum =
new SuperEnum([
{ key: 'MARKER', name: '标记点', color: 'warning', value: 0 },
{ key: 'START', name: '起点', color: 'primary', value: 1 },
{ key: 'END', name: '终点', color: 'success', value: 2 },
{ key: 'CHECK_POINT', name: '打卡点', color: 'info', value: 3 },
])
// 状态枚举
const StatusEnum =
new SuperEnum([
{ key: 'CLOSE', name: '关闭', color: 'error', value: 0 },
{ key: 'OPEN', name: '开启', color: 'success', value: 1 },
])
const mapBoxToken = 'pk.eyJ1IjoieGluZ3NhaSIsImEiOiJjbTc0ZXJ4ZjgwOWF3MmtyMjFxN2E2MHo0In0.pknVy3Lq6fLcXiwvrVLcFg'
// 添加全局 loading 指令
const uploadUrl = window.location.origin + '/api-common/v1/upload?folder=' + 'gk/location' + '&cmptId=' + cmpt.cmptId
// mapKey
const MapKeyEnum = {
// cp点标注相关
CP_ID: 'cp_id',
CP_KEY: 'cp_',
CP_SOURCE_ID: 'cp_source_id',
KM_CIRCLE_ID: 'km_circle_id',
// 公里数相关
KM_KEY: 'km_',
KM_SOURCE_ID: 'km_source_id',
KM_TEXT_ID: 'km_text_id',
// 轨迹相关
LINE_ID: 'line_id',
LINE_KEY: 'line_',
LINE_SOURCE_ID: 'line_source_id',
// 定义的字段
NAME_KEY: 'name',
}
// 点位类型
const pointTypes = [
{
id: 0,
name: '计时点',
},
{
id: 1,
name: '起点',
},
{
id: 2,
name: '终点',
}
]
/**
* GPS公共配置信息
*/
const GpsConfig = {
// 地图配置
map: {
zoom: 8,
center: [106.67573046471102, 26.411047728805663],
centerPadding: [100, 100, 100, 100],
maxZoom: 20,
minZoom: 3,
centerZoom: 10,
speed: 0.5,
curve: 1.42,
threePitch: 45,
defaultPitch: 0,
minTileCacheSize: 20 * 1024 * 1024, // 20MB
maxTileCacheSize: 100 * 1024 * 1024, // 100MB
duration: 1000,
},
// 文字配置
text: {
textColor: '#ffffff',
padding: 2,
textAlign: 'center',
textBaseline: 'middle',
lineWidth: 1,
lineHeight: 1.5,
backgroundRadius: 5,
haloWidth: 25,
haloPadding: 8,
},
km: {
circleOpacity: 0.5,
circleRadius: 12,
font: ['Arial Unicode MS Regular'],
fontSize: 14,
circleBorderWidth: 4,
circleBorderColor: '#94e697',
textColor: '#6defde',
circleColor: '#ffffff',
textOffset: [0, 0.1],
},
// cp点的配置信息
cp: {
anchor: [0.5, 1],
scale: 0.15,
fontSize: 14,
iconSize: 1,
iconCommonSize: 0.2,
textColor: '#eba236',
haloWidth: 2,
font: ['Arial Unicode MS Regular'],
textOffset: [0, -0.2],
},
// 轨迹配置
path: {
width: 5,
color: "#7367F0"
},
};
// 获取天地图的地址
const getTDTUrls = () => {
const services = Array.from({ length: 7 }, (_, i) => i + 1);
const baseUrl = 'https://t%s.tianditu.gov.cn';
const generateUrls = (path) => {
return services.map((v) => baseUrl.replace('%s', v.toString()) + path);
};
// 天地图卫星影像服务地址
const satellitePath = "/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=" + tk;
const TDTSatelliteUrls = generateUrls(satellitePath);
// 获取原始地图数据集合
const dataServerPath = "/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + tk;
const TDTDataServerUrls = generateUrls(dataServerPath);
// 天地图注记服务地址
const annotationPath = "/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=" + tk;
const TDTAnnotationUrls = generateUrls(annotationPath);
return {
TDTSatelliteUrls,
TDTDataServerUrls,
TDTAnnotationUrls,
};
}
// mapbox的字体
const glyphs = 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf';
// 获取地址
const { TDTDataServerUrls, TDTAnnotationUrls, TDTSatelliteUrls } =
getTDTUrls();
// 构建样式
const defaultStyle = {
version: 8,
sources: {
tianmap: {
type: 'raster',
tiles: TDTSatelliteUrls,
tileSize: 256,
attribution: '天地图影像图',
},
// 添加注记图层的数据源
tianmapAnnotation: {
type: 'raster',
tiles: TDTAnnotationUrls,
tileSize: 256,
attribution: '天地图注记图层',
},
},
layers: [
{
id: 'tianmap-layer',
type: 'raster',
source: 'tianmap',
},
{
id: 'tianmap-annotation-layer',
type: 'raster',
source: 'tianmapAnnotation',
},
],
// 添加 glyphs 属性到地图样式
glyphs,
};
// 获取url参数
const getQueryParams = () => {
const params = {};
window.location.search.substring(1).split('&').forEach(pair => {
const [key, value] = pair.split('=');
if (key) params[key] = decodeURIComponent(value || '');
});
return params;
}
const urlParams = getQueryParams()
</script>
<script>
const { createApp, defineComponent, ref, onMounted, reactive } = Vue;
const { useMessage, useNotification, dateZhCN, zhCN } = naive
const MyApp = defineComponent({
name: 'MyApp',
template: '#my-app',
setup() {
const loading = ref(false)
const message = useMessage()
const notification = useNotification()
onMounted(() => {
fetchData()
});
const fetchData = async () => {
loading.value = true
let params = {
categoryId: urlParams.categoryId,
page: 1,
size: 100,
}
// 发起请求
const [err, res] = await to(fetch("/gk/location-point/page", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(res => res.json()))
loading.value = false
if (res.code === 200) {
markers.value = res.content.content
trackCoordinates.value = res.content.content.map(v => [v.longitude, v.latitude])
} else {
message.error(res.msg)
}
// 初始化地图
initMap();
}
/**------------------------ 地图初始化 start ------------------------**/
// 地图对象
let map = ref(null);
// 初始化地图
const initMap = () => {
// 确保容器已完全渲染
setTimeout(() => {
mapboxgl.config.MAX_PARALLEL_IMAGE_REQUESTS = 256;
if (!map.value) {
map.value = new mapboxgl.Map({
container: 'map',
style: defaultStyle,
container: 'map',
center: GpsConfig.map.center,
zoom: GpsConfig.map.zoom,
accessToken: mapBoxToken,
minZoom: GpsConfig.map.minZoom,
maxZoom: GpsConfig.map.maxZoom,
pitch: GpsConfig.map.defaultPitch, // 初始倾斜角度,0 表示 2D 模式
antialias: true, // 抗锯齿
bearing: 0, // 初始旋转角度
interactive: true, // 可交互
attributionControl: false,
minTileCacheSize: GpsConfig.map.minTileCacheSize,
maxTileCacheSize: GpsConfig.map.maxTileCacheSize, // 调整瓦片缓存大小
});
// 添加resize事件监听器
window.addEventListener('resize', () => {
map.value.resize();
});
// 监听 pitchChange 事件
map.value.on('load', () => {
loadMapIcons().then(() => {
refreshLiveMap();
destroyEvent();
initMapEvent();
initMapControls();
});
});
} else {
loadMapIcons().then(() => {
refreshLiveMap();
destroyEvent();
initMapEvent();
});
}
}, 0);
}
/**
* 刷新地图
*/
const refreshLiveMap = () => {
clearMap();
// initPolyline();
initMarkers();
initCenter();
};
/**
* 清除地图
*/
const clearMap = () => {
if (!map.value) return;
const layers = map.value.getStyle()?.layers;
layers.forEach((layer) => {
if (
layer.id.includes(MapKeyEnum.KM_KEY) ||
layer.id.includes(MapKeyEnum.CP_KEY) ||
layer.id.includes(MapKeyEnum.LINE_KEY)
) {
// 删除图层
map.value?.removeLayer(layer.id);
// 检查并删除对应的数据源
if (map.value?.getSource(layer.id)) {
map.value.removeSource(layer.id);
}
}
});
};
/**
* 初始化中心
* */
const initCenter = () => {
const allCoordinates = trackCoordinates.value.map(
(v) => [v[0], v[1]],
);
if (allCoordinates.length > 0) {
const bounds = allCoordinates?.reduce(
(bounds, coord) => {
return bounds.extend(coord);
},
new mapboxgl.LngLatBounds(allCoordinates[0], allCoordinates[0]),
);
map.value?.fitBounds(bounds, {
padding: GpsConfig.map.centerPadding,
duration: GpsConfig.map.duration,
essential: true,
maxZoom: 15 // 添加最大缩放级别限制
});
}
};
/**
* 初始化轨迹
*/
const initPolyline = () => {
if (!map.value) return;
if (!trackCoordinates.value.length) return
// 添加 polyline 到地图
const coordinates = trackCoordinates.value.map((v) => [v[0], v[1]]);
const sourceId = MapKeyEnum.LINE_SOURCE_ID;
const layerId = MapKeyEnum.LINE_ID
// 移除旧的轨迹图层和数据源
if (map.value?.getLayer(layerId)) {
map.value?.removeLayer(layerId);
}
if (map.value?.getSource(sourceId)) {
map.value?.removeSource(sourceId);
}
if (!map.value?.getSource(sourceId)) {
map.value?.addSource(sourceId, {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates,
},
},
});
map.value?.addLayer({
id: layerId,
type: 'line',
source: sourceId,
paint: {
'line-color': GpsConfig.path.color,
'line-width': GpsConfig.path.width,
},
});
}
};
/**
* 初始化标记点
*/
const initMarkers = () => {
// const kms = trackCoordinates.value?.length ? calculateDistanceWithTurf(
// trackCoordinates.value.map((v) => [v.longitude, v.latitude]),
// ) : [];
// 构建cp点
const cpFeatures = markers.value.map((cp) => {
const { latitude, longitude, name, ID, type } = cp;
let icon = cp.icon;
if (!icon) {
switch (type) {
case pointTypes[0].value: {
icon = MapIconEnum.ICON_CP;
break;
}
case pointTypes[2].value: {
icon = MapIconEnum.ICON_END_POINT;
break;
}
case pointTypes[1].value: {
icon = MapIconEnum.ICON_START_POINT;
break;
}
default: {
icon = MapIconEnum.ICON_CP;
}
}
}
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [longitude, latitude],
},
properties: {
text: name,
color: GpsConfig.cp.textColor,
[MapKeyEnum.NAME_KEY]: cp.id,
icon,
},
};
});
// 公里数的展示
// const kmFeatures = kms.map((km, kmIndex) => {
// const kmId = MapKeyEnum.KM_KEY + kmIndex;
// return {
// type: 'Feature',
// geometry: {
// type: 'Point',
// coordinates: km,
// },
// properties: {
// text: kmIndex + 1,
// color: GpsConfig.text.textColor,
// id: kmId,
// circleColor: GpsConfig.km.circleColor,
// },
// };
// });
// 异步添加/更新 数据
if (!map.value) return;
// 强制移除旧的数据源和图层
if (map.value.getLayer(MapKeyEnum.KM_CIRCLE_ID)) {
map.value.removeLayer(MapKeyEnum.KM_CIRCLE_ID);
}
if (map.value.getLayer(MapKeyEnum.KM_TEXT_ID)) {
map.value.removeLayer(MapKeyEnum.KM_TEXT_ID);
}
if (map.value.getLayer(MapKeyEnum.CP_ID)) {
map.value.removeLayer(MapKeyEnum.CP_ID);
}
if (map.value.getSource(MapKeyEnum.KM_SOURCE_ID)) {
map.value.removeSource(MapKeyEnum.KM_SOURCE_ID);
}
if (map.value.getSource(MapKeyEnum.CP_SOURCE_ID)) {
map.value.removeSource(MapKeyEnum.CP_SOURCE_ID);
}
// 添加公里数图层
// if (map.value.getSource(MapKeyEnum.KM_SOURCE_ID)) {
// // 如果 sourceId 存在,设置数据
// (map.value.getSource(MapKeyEnum.KM_SOURCE_ID)).setData({
// type: 'FeatureCollection',
// features: kmFeatures,
// });
// } else {
// // 如果 sourceId 不存在,添加 source
// map.value.addSource(MapKeyEnum.KM_SOURCE_ID, {
// type: 'geojson',
// data: {
// type: 'FeatureCollection',
// features: kmFeatures,
// },
// });
// // 添加图层
// map.value.addLayer({
// id: MapKeyEnum.KM_CIRCLE_ID,
// type: 'circle',
// source: MapKeyEnum.KM_SOURCE_ID,
// paint: {
// 'circle-radius': GpsConfig.km.circleRadius,
// // 允许文本相互重叠
// 'circle-color': ['get', 'circleColor'],
// 'circle-stroke-width': GpsConfig.km.circleBorderWidth, // 边框宽度
// 'circle-stroke-color': GpsConfig.km.circleBorderColor, // 边框颜色,这里设置为白色
// },
// });
// map.value.addLayer({
// id: MapKeyEnum.KM_TEXT_ID,
// type: 'symbol',
// source: MapKeyEnum.KM_SOURCE_ID,
// minzoom: 0,
// maxzoom: 24,
// layout: {
// 'text-field': ['get', 'text'],
// 'text-anchor': 'center',
// 'text-size': GpsConfig.km.fontSize,
// 'text-font': GpsConfig.km.font,
// 'text-padding': GpsConfig.text.padding,
// // 允许图标相互重叠
// 'icon-allow-overlap': true,
// // 允许文本相互重叠
// 'text-allow-overlap': true,
// },
// paint: {
// 'text-color': GpsConfig.km.textColor,
// },
// });
// }
// 添加cp点图层
if (map.value.getSource(MapKeyEnum.CP_SOURCE_ID)) {
// 如果 sourceId 存在,设置数据
(map.value.getSource(MapKeyEnum.CP_SOURCE_ID)).setData({
type: 'FeatureCollection',
features: cpFeatures,
});
} else {
// 如果 sourceId 不存在,添加 source
map.value.addSource(MapKeyEnum.CP_SOURCE_ID, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: cpFeatures,
},
});
map.value.addLayer({
id: MapKeyEnum.CP_ID,
type: 'symbol',
source: MapKeyEnum.CP_SOURCE_ID,
// 设置图层在所有缩放级别显示
layout: {
'icon-image': ['get', 'icon'],
'icon-anchor': 'bottom',
'text-anchor': 'top',
'text-size': GpsConfig.cp.fontSize,
'text-padding': GpsConfig.text.padding,
'text-font': GpsConfig.cp.font,
'text-field': ['get', 'text'],
'text-justify': 'center',
'text-pitch-alignment': 'viewport',
'text-line-height': GpsConfig.text.lineHeight,
'icon-size': GpsConfig.cp.iconSize,
'text-offset': GpsConfig.cp.textOffset,
// 允许图标相互重叠
'icon-allow-overlap': true,
// 允许文本相互重叠
'text-allow-overlap': true,
},
paint: {
'text-color': GpsConfig.cp.textColor,
// 添加字体描边颜色
'text-halo-color': GpsConfig.text.textColor, // 可根据需要修改颜色
// 添加字体描边宽度
'text-halo-width': GpsConfig.cp.haloWidth, // 可根据需要修改宽度
},
});
}
};
/**
* 计算总距离和公里点
*/
function calculateDistanceWithTurf(
coordinates,
) {
// 1. 创建LineString
const line = turf.lineString(coordinates);
// 2. 计算总长度(公里)
const totalDistance = turf.length(line, { units: 'kilometers' });
// 3. 计算每公里的点位
const kmPoints = [];
for (let km = 1; km <= Math.floor(totalDistance); km++) {
const along = turf.along(line, km, { units: 'kilometers' });
kmPoints.push(along.geometry.coordinates);
}
return kmPoints;
}
/**
* 初始化地图单位
*/
const initMapControls = () => {
if (!map.value) return;
// 添加比例尺控件到地图
map.value.addControl(new mapboxgl.ScaleControl(), 'bottom-right');
};
/**
* 加载静态资源
*/
const loadMapIcons = () => {
// 加载图标图片
const icons = Object.values(mapIcons);
const loadIcons = [...markers.value.filter(v => v.icon).map(v => v.icon), ...icons].map((icon) => {
// 检查图片是否已经存在
if (map.value?.hasImage(icon)) {
return Promise.resolve(true);
}
return new Promise((resolve, reject) => {
map.value?.loadImage(icon, (error, image) => {
if (error) {
// console.error(`Failed to load image: ${icon}`, error);
reject(error);
} else {
if (map.value?.hasImage(icon)) {
return resolve(true);
}
map.value?.addImage(icon, image);
resolve(true);
}
});
});
});
return Promise.all(loadIcons);
};
// 销毁之前的事件监听器
const destroyEvent = () => {
if (!map.value) return;
// 移除地图事件
map.value.off('click', MapKeyEnum.CP_ID, onCp);
map.value.off('click', onMapClick);
map.value.off('mouseenter');
map.value.off('mouseleave');
map.value?.off('mousemove', handleMapMouseMove);
};
/**
* 添加地图点击事件处理函数
* @param event 地图点击事件
*/
const onCp = (event) => {
event.preventDefault()
try {
if (!map.value) return;
// 点击标记点·
// 筛选出 players-icons 图层的要素
const features = map.value.queryRenderedFeatures(event.point, {
layers: [MapKeyEnum.CP_ID],
});
if (features.length > 0) {
const feature = features[0];
if (!feature?.properties) return;
const id = feature.properties[MapKeyEnum.NAME_KEY];
// emits('onMarker', id);
let marker = markers.value.find(v => v.id === id)
editCheckPoint(marker)
}
} catch (error) {
console.error('地图点击事件处理出错:', error);
}
};
// 点位拾取
const onMapClick = (e) => {
if (!map.value || !isAddMode.value) return;
const features = map.value.queryRenderedFeatures(e.point, {
layers: [MapKeyEnum.CP_ID]
});
if (features.length) return
// 获取点击位置的坐标
const coordinates = e.lngLat;
markerForm.latitude = coordinates.lat;
markerForm.longitude = coordinates.lng;
markerForm.name = '新增标记点'
editCheckPoint(null)
// 可以在这里添加自定义逻辑,如:
// 1. 显示坐标信息
// 2. 在地图上添加标记
// 3. 触发其他业务逻辑
}
/**
* 提取 mousemove 事件处理函数
* @param e
*/
const handleMapMouseMove = (e) => {
// 否则光标是十字准线。
if (map.value?.getCanvas()) {
map.value.getCanvas().style.cursor = !isAddMode.value ? 'pointer'
: 'crosshair';
}
};
// 初始化 地图事件
const initMapEvent = () => {
if (!map.value) return;
map.value.on('click', [MapKeyEnum.CP_ID], onCp);
// 鼠标悬停在聚合点上时改变鼠标样式
map.value.on('mouseenter', [MapKeyEnum.CP_ID], () => {
if (!map.value) return;
map.value.getCanvas().style.cursor = 'pointer';
});
// 鼠标离开聚合点时恢复鼠标样式
map.value.on('mouseleave', [MapKeyEnum.CP_ID], () => {
if (!map.value) return;
map.value.getCanvas().style.cursor = '';
});
// 测距的时候监听鼠标悬浮事件
map.value?.on('mousemove', handleMapMouseMove);
// 添加地图点击拾取点位事件
map.value.on('click', onMapClick);
};
/**------------------------ 地图初始化 end ------------------------**/
/**------------------------ 轨迹导入 start ------------------------**/
// 轨迹的名称
const fileName = ref('')
// 轨迹上传的input
const fileInputRef = ref(null);
// 确保是二维数组类型
const trackCoordinates = ref([]);
const markers = ref([]);
// 处理文件选择
const handleFileChange = async (event) => {
const input = event.target;
if (input.files && input.files.length > 0) {
const file = input.files[0];
const fileType = file.name.split('.').pop()?.toLowerCase();
if (fileType === 'gpx' || fileType === 'kml') {
if (file.size <= 10 * 1024 * 1024) {
// 检查文件大小不超过 10M
message.success(`${file.name} 文件选择成功`);
} else {
message.error(`${file.name} 文件大小超过 10M,请重新选择`);
}
} else {
message.error(`只能上传 GPX 或 KML 文件,请重新选择`);
}
if (file) {
fileName.value = file.name;
const text = await file.text();
if (file.name.endsWith('.gpx')) {
parseGPX(text);
} else if (file.name.endsWith('.kml')) {
parseKML(text);
}
}
}
};
// 解析gpx
async function parseGPX(gpxData) {
try {
const goxDom = new DOMParser().parseFromString(gpxData, "text/xml");
const geoJSON = toGeoJSON.gpx(goxDom);
trackCoordinates.value =
geoJSON.features
.filter((f) => f.geometry?.type === 'LineString')
.flatMap((f) =>
f.geometry?.coordinates?.map((coord) => [
coord[0],
coord[1],
coord[2] || 0,
]),
) ?? [];
markers.value = geoJSON.features
.filter((f) => f.geometry?.type === 'Point' && f.properties?.name)
.map(
(p, index) =>
({
name: p.properties?.name || '未命名',
longitude: p.geometry.coordinates[0],
latitude: p.geometry.coordinates[1],
altitude: p.geometry.coordinates[2] || 0,
status: StatusEnum.OPEN.value,
type: CheckPointTypeEnum.MARKER.value,
id: index + 1
}),
);
if (trackCoordinates.value.length > 0) {
importMarkersAndTrackers();
} else {
message.error('GPX 文件中未找到有效的轨迹数据');
}
importMarkersAndTrackers();
} catch (error) {
console.error('Error parsing GPX:', error);
}
}
// 解析kml
async function parseKML(kmlData) {
try {
const kmlDom = new DOMParser().parseFromString(kmlData, "text/xml");
const geoJSON = toGeoJSON.kml(kmlDom);
// 安全提取并转换轨迹坐标
trackCoordinates.value =
geoJSON.features
.filter((f) => f.geometry?.type === 'LineString')
.flatMap((f) =>
f.geometry?.coordinates?.map((coord) => [
coord[0],
coord[1],
coord[2] || 0,
]),
) ?? [];
markers.value = geoJSON.features
.filter((f) => f.geometry?.type === 'Point' && f.properties?.name)
.map(
(p, index) =>
({
name: p.properties?.name || '未命名',
longitude: p.geometry.coordinates[0],
latitude: p.geometry.coordinates[1],
altitude: p.geometry.coordinates[2] || 0,
status: StatusEnum.OPEN.value,
type: CheckPointTypeEnum.MARKER.value,
id: index + 1
}),
)
.filter(Boolean);
if (trackCoordinates.value.length > 0) {
importMarkersAndTrackers();
} else {
message.error('KML 文件中未找到有效的轨迹数据');
}
} catch (error) {
console.error('Error parsing KML:', error);
}
}
// 保存标记点
const importMarkersAndTrackers = async () => {
// TODO 保存标记点
refreshLiveMap();
}
/**------------------------ 轨迹导入 end ------------------------**/
/**------------------------ marker start ------------------------**/
// 结果列表
const fileList = ref([])
// 上传完成
const uploadFinish = ({
file,
event
}) => {
const response = JSON.parse(event?.target?.response)
file.url = response.content
return file
}
const isEditMarker = ref(false)
const defaultMarkerForm = {
id: undefined,
name: '',
longitude: 0,
latitude: 0,
altitude: 0,
icon: '',
status: StatusEnum.OPEN.value,
sortOrder: 100,
categoryId: urlParams.categoryId,
}
const markerForm = reactive({
id: undefined,
name: '',
longitude: 0,
latitude: 0,
altitude: 0,
icon: '',
status: StatusEnum.OPEN.value,
sortOrder: 100,
categoryId: urlParams.categoryId,
})
// 点位表单
const markerFormRef = ref()
// 是否显示弹框
const showModal = ref(false)
// 是否是点位添加模式
const isAddMode = ref(false)
// 表单规则
const markerRules = {
name: {
required: true,
message: '请输入点位名称',
trigger: ['blur', 'input'],
},
longitude: {
required: true,
message: '请输入经度',
trigger: ['blur', 'input'],
type: 'number'
},
latitude: {
required: true,
message: '请输入纬度',
trigger: ['blur', 'input'],
type: 'number'
},
}
// 重置
const resetForm = () => {
fileList.value = []
Object.assign(markerForm, defaultMarkerForm)
}
// 编辑点位
const editCheckPoint = (record) => {
isEditMarker.value = !!record
if (record) {
Object.assign(markerForm, { ...record })
}
if (record?.icon) {
fileList.value = [
{
url: record?.icon,
status: 'finished',
name: record?.icon,
id: record.id,
uid: record.id
}
]
}
showModal.value = true
}
// 删除点位
const deleteCheckPoint = (index) => {
let marker = markers.value[index]
loading.value = true
const messageReactive = message.loading('删除中...', {
duration: 0
})
// 发起请求
fetch("/gk/location-point/" + marker.id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(data => {
if (data.code === 200) {
message.success('删除成功')
fetchData()
} else {
message.error(data.msg)
}
}).finally(() => {
loading.value = true
messageReactive.destroy()
refreshLiveMap()
})
}
// 取消编辑点
const onMarkerCancel = () => {
showModal.value = false
resetForm()
}
// 提交
const onSubmit = (e) => {
e.preventDefault()
markerFormRef.value?.validate(async (errors) => {
if (errors) return message.error('请检查表单是否填写完整')
// 使用fetch发起请求
const params = {
...markerForm,
categoryId: urlParams.categoryId,
icon: fileList.value.map(v => v.url).join()
}
loading.value = true
const messageReactive = message.loading(isEditMarker.value ? '数据更新中...' : '数据添加中...', {
duration: 0
})
// 发起请求
const [err, data] = await to(fetch("/gk/location-point", {
method: isAddMode.value ? 'POST' : 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(res => res.json()))
loading.value = false
messageReactive.destroy()
if (data.code === 200) {
message.success('操作成功')
showModal.value = false
resetForm()
fetchData()
} else {
message.error(data.msg)
}
})
}
// 返回
const onBack = () => {
// 回退
window.history.back()
}
/**------------------------ marker end ------------------------**/
// 对外返回的信息
return {
// 轨迹导入模块
fileInputRef,
fileName,
handleFileChange,
// 公共
CheckPointTypeEnum,
StatusEnum,
// 点位编辑模块
markers,
markerForm,
markerFormRef,
isEditMarker,
showModal,
editCheckPoint,
deleteCheckPoint,
onMarkerCancel,
onSubmit,
markerRules,
isAddMode,
fileList,
onBack,
uploadUrl,
uploadFinish
}
}
})
const app = Vue.createApp({
setup() {
return {
locale: zhCN,
dateLocale: dateZhCN
}
}
})
app.component('my-app', MyApp)
app.use(naive)
app.mount('#app')
</script>
</body>
</html>
\ No newline at end of file
......@@ -18,7 +18,7 @@ $(function () {
// 为当前点击的图片添加 active-style 样式
$(this).addClass('active-style');
// 将选中的样式值存储到隐藏的 input 中
$("#detailStyle").val(selectedStyle);
$("#detailStyle").val(selectedStyle ?? '1');
});
// by ggb 2025-05-30 是否显示 底部的tab
$('#isShowTabSwitch').change(function () {
......@@ -38,7 +38,7 @@ $(function () {
return;
}
const param = {
detailStyle: detailStyle ?? 'style-1',
detailStyle: detailStyle ?? '1',
cmptId,
logo,
cmptbgColor: "#" + bg,
......@@ -165,7 +165,7 @@ function changeLogoImg(imgDomNName, inputDomName, top, left, width, height) {
$('#cmptLogo').addClass('cmpt-logo');
$('#cmptLogo').removeClass("logo");
} else {
layer.msg(res.msg, {icon: 2});
layer.msg(res.msg, { icon: 2 });
}
},
beforeSend: () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment