@@ -16,9 +16,11 @@ | |||
"axios": "^0.26.1", | |||
"dayjs": "^1.11.2", | |||
"mockjs": "^1.1.0", | |||
"ol": "^6.15.1", | |||
"pinia": "^2.0.13", | |||
"pinia-plugin-persist": "^1.0.0", | |||
"tinymce": "^5.10.2", | |||
"vite-plugin-svg-icons": "^2.0.1", | |||
"vue": "^3.2.16", | |||
"vue-router": "^4.0.14", | |||
"vuedraggable": "^4.1.0" |
@@ -0,0 +1,133 @@ | |||
<template> | |||
<div class="overlay_container"> | |||
<div class="overlay_head"> | |||
<img class="close_img" src="@/assets/point/close.png" alt="" @click="updateVisible"> | |||
</div> | |||
<div class="overlay_main"> | |||
<n-form | |||
ref="formRef" | |||
:model="form" | |||
:label-width="80" | |||
label-placement="left" | |||
require-mark-placement="left" | |||
> | |||
<n-form-item label="问题类型:"> | |||
<n-select | |||
v-model:value="form.type" | |||
:options="typeList" | |||
placeholder="请选择问题类型" | |||
/> | |||
</n-form-item> | |||
<n-form-item label="问题描述:"> | |||
<span>{{ form.content }}</span> | |||
</n-form-item> | |||
<n-form-item label="经纬度:"> | |||
<span>{{ form.longitude }},{{ form.latitude }}</span> | |||
</n-form-item> | |||
<n-form-item label="问题图片:"> | |||
<img class="image_size" :src="form.fileImage" alt="" style="width: 108px; height: auto; margin: 0 5px 5px 0"> | |||
</n-form-item> | |||
</n-form> | |||
</div> | |||
<div class="btn_list"> | |||
<n-button class="btn_item" type="primary" @click="makeSure">确定</n-button> | |||
<n-button class="btn_item" @click="pass">忽略</n-button> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { toRefs, reactive, watch, inject } from 'vue' | |||
export default { | |||
name: 'OverLay', | |||
props: { | |||
data: { | |||
type: Object, | |||
default: () => null | |||
} | |||
}, | |||
emits: { | |||
'close': null | |||
}, | |||
setup(props, { emit }) { | |||
const data = reactive({ | |||
form: {}, | |||
typeList: [ | |||
{ value: 1, label: '林斑' }, | |||
{ value: 2, label: '病虫害' } | |||
] | |||
}) | |||
/* 引入祖先组件注册的方法 */ | |||
const confirm = inject('confirm', null) | |||
const ignore = inject('ignore', null) | |||
watch(() => props.data, () => { | |||
data.form = Object.assign({}, props.data) | |||
}) | |||
const updateVisible = function() { | |||
emit('close') | |||
} | |||
function makeSure() { | |||
confirm(data.form) | |||
updateVisible() | |||
} | |||
function pass() { | |||
ignore(data.form) | |||
updateVisible() | |||
} | |||
return { | |||
...toRefs(data), | |||
updateVisible, | |||
makeSure, | |||
pass | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.overlay_container { | |||
width: 100%; | |||
height: 436px; | |||
border-radius: 6px; | |||
padding: 0; | |||
overflow-y: scroll; | |||
background-color: #fff; | |||
position: relative; | |||
} | |||
.overlay_head { | |||
width: 100%; | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
position: fixed; | |||
top: 0; | |||
left: 0; | |||
} | |||
.close_img { | |||
width: 40px; | |||
height: 40px; | |||
cursor: pointer; | |||
} | |||
.overlay_main { | |||
width: 100%; | |||
margin-top: 10px; | |||
padding: 40px 30px 60px 40px; | |||
} | |||
.btn_list { | |||
width: 100%; | |||
padding-right: 32px; | |||
position: fixed; | |||
bottom: 20px; | |||
right: 0; | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
} | |||
.btn_item { | |||
width: 64px; | |||
height: 32px; | |||
margin-left: 20px; | |||
} | |||
</style> |
@@ -0,0 +1,250 @@ | |||
<template> | |||
<div class="main_container"> | |||
<div id="track" ref="map" /> | |||
<div id="pointOverlay" class="point_overlay"> | |||
<over-lay :data="problemData" @close="closeOverlay" /> | |||
</div> | |||
<ul class="legend_list"> | |||
<li v-for="(item, index) in legendList" :key="index" class="legend_item"> | |||
<div class="legend_point" :style="{background: item.color}" /> | |||
<span class="legend_name">{{ item.name }}</span> | |||
</li> | |||
</ul> | |||
</div> | |||
</template> | |||
<script> | |||
import { reactive, toRefs, onMounted, watch, ref } from 'vue' | |||
import { styleList, legendList } from './util.js' | |||
import { Map, View, Feature, Overlay } from 'ol' | |||
import 'ol/ol.css' | |||
import { Tile, Vector as VectorLayer } from 'ol/layer' | |||
import LineString from 'ol/geom/LineString' | |||
import { XYZ, Vector as VectorSource } from 'ol/source' | |||
import { transform, fromLonLat } from 'ol/proj' | |||
import * as control from 'ol/control' | |||
import { Select } from 'ol/interaction' | |||
import Point from 'ol/geom/Point' | |||
import OverLay from './OverLay.vue' | |||
export default { | |||
name: 'PositionMsg', | |||
components: { OverLay }, | |||
props: { | |||
data: { | |||
type: Array, | |||
default: () => {} | |||
} | |||
}, | |||
emits: {}, | |||
setup(props, { emit }) { | |||
const data = reactive({ | |||
problemList: props.data, // 问题矢量点列表 | |||
problemData: {}, // 问题点数据 | |||
legendList: legendList | |||
}) | |||
const mapdata = ref(null) | |||
const select = ref(null) | |||
const problemLayer = ref(null) | |||
const overlay = ref(null) | |||
const view = ref(null) | |||
/** | |||
* 初始化底图 | |||
*/ | |||
const initMap = function() { | |||
const layers = [ | |||
new Tile({ | |||
visible: true, | |||
source: new XYZ({ | |||
url: 'https://tianditu.t-aaron.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=f82bb2fd8115c7fb576a8b2fcf738523' | |||
}) | |||
}) | |||
] | |||
view.value = new View({ | |||
maxZoom: 18, | |||
zoom: 4, | |||
center: transform([108.94043, 31.008173], 'EPSG:4326', 'EPSG:3857') | |||
}) | |||
mapdata.value = new Map({ | |||
layers: layers, | |||
target: 'track', | |||
view: view.value, | |||
controls: control.defaults({ | |||
attribution: false, | |||
rotate: false, | |||
zoom: false | |||
}) | |||
}) | |||
mapdata.value.render() | |||
// 添加select并设置选中样式 | |||
select.value = new Select({ | |||
style: (feature) => { | |||
if (feature.getGeometry() instanceof Point) { | |||
return styleList[0] | |||
} | |||
} | |||
}) | |||
mapdata.value.addInteraction(select.value) | |||
// 新建overlay弹出框 | |||
overlay.value = new Overlay({ | |||
element: document.getElementById('pointOverlay'), | |||
autoPan: true | |||
}) | |||
} | |||
onMounted(() => { | |||
initMap() | |||
getLayer(props.data, view.value, mapdata.value) | |||
}) | |||
/** | |||
* 加载矢量图层 | |||
*/ | |||
const getLayer = function(data) { | |||
const pointFeatures = [] | |||
const points = [] | |||
if (data.length) { | |||
data.forEach((item) => { | |||
const pointItem = [] | |||
pointItem.push(parseFloat(item.longitude)) | |||
pointItem.push(parseFloat(item.latitude)) | |||
const featureItem = new Feature(new Point(fromLonLat(pointItem))) | |||
points.push(fromLonLat(pointItem)) | |||
featureItem.setProperties({ item: item, type: 'problem' }, false) | |||
featureItem.setStyle(styleList[item.status]) | |||
pointFeatures.push(featureItem) | |||
}) | |||
const pointSource = new VectorSource({ | |||
wrapX: false, | |||
features: pointFeatures | |||
}) | |||
problemLayer.value = new VectorLayer({ | |||
source: pointSource, | |||
zIndex: 12 | |||
}) | |||
let geom | |||
if (points.length > 1) { | |||
geom = new LineString(points) | |||
} else if (points.length > 0) { | |||
geom = new Point(points[0]) | |||
} | |||
if (geom) { | |||
view.value.fit(geom, { padding: [150, 200, 150, 200] }) | |||
} | |||
mapdata.value.addLayer(problemLayer.value) | |||
} | |||
} | |||
/** | |||
* 删除矢量图层 | |||
*/ | |||
const removeVector = function(layer) { | |||
layer.getSource().clear() | |||
mapdata.value.removeLayer(layer) | |||
problemLayer.value = null | |||
} | |||
/** | |||
* 选择矢量点数据 | |||
*/ | |||
const setOverlay = function() { | |||
select.value.on('select', (e) => { | |||
if (e.selected.length > 0) { | |||
const properties = e.selected[0].getProperties() | |||
const geom = properties.geometry | |||
if (geom instanceof Point) { | |||
data.problemData = properties.item | |||
const coordinate = e.mapBrowserEvent.coordinate | |||
overlay.value.setPosition(coordinate) | |||
overlay.value.setOffset([30, -400]) | |||
mapdata.value.addOverlay(overlay.value) | |||
} else { | |||
closeOverlay() | |||
} | |||
} else { | |||
closeOverlay() | |||
} | |||
}) | |||
} | |||
watch( | |||
() => select.value, | |||
(value) => { | |||
setOverlay(mapdata.value, overlay.value) | |||
} | |||
) | |||
watch(() => props.data, (value) => { | |||
if (value) { | |||
if (mapdata.value) { | |||
removeVector(problemLayer.value) | |||
getLayer(props.data) | |||
} | |||
} | |||
}) | |||
/** | |||
* 关闭overlay | |||
*/ | |||
const closeOverlay = () => { | |||
data.problemData = {} | |||
select.value.getFeatures().clear() | |||
mapdata.value.removeOverlay(overlay.value) | |||
} | |||
return { | |||
...toRefs(data), | |||
removeVector, | |||
closeOverlay | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.main_container { | |||
width: 100%; | |||
height: 100%; | |||
position: relative; | |||
overflow: hidden; | |||
} | |||
#track { | |||
width: 100%; | |||
height: 100%; | |||
} | |||
.point_overlay { | |||
width: 400px; | |||
} | |||
.legend_list { | |||
width: 172px; | |||
height: 173px; | |||
padding: 10px 20px; | |||
background: rgba(31, 31, 31, 0.5); | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: space-between; | |||
align-items: flex-start; | |||
position: absolute; | |||
bottom: 0; | |||
right: 0; | |||
} | |||
.legend_item { | |||
width: 100%; | |||
display: flex; | |||
justify-content: flex-start; | |||
align-items: center; | |||
} | |||
.legend_point { | |||
width: 24px; | |||
height: 24px; | |||
margin-right: 24px; | |||
border-radius: 50%; | |||
border: 1px solid #fff; | |||
} | |||
.legend_name { | |||
font-size: 16px; | |||
color: #fff; | |||
} | |||
</style> |
@@ -0,0 +1,65 @@ | |||
import 'ol/ol.css' | |||
import { Style, Icon } from 'ol/style' | |||
import imgAwait from '../../assets/point/await.png' | |||
import imgChecked from '../../assets/point/checked.png' | |||
import imgIgnored from '../../assets/point/ignored.png' | |||
import imgSelected from '../../assets/point/selected.png' | |||
// 矢量图标样式 | |||
export const styleList = [ | |||
new Style({ | |||
image: new Icon({ | |||
anchor: [0.5, 0.5], | |||
src: imgSelected, | |||
crossOrigin: '', | |||
scale: [1, 1], | |||
rotateWithView: true | |||
}) | |||
}), | |||
new Style({ | |||
image: new Icon({ | |||
anchor: [0.5, 0.5], | |||
src: imgAwait, | |||
crossOrigin: '', | |||
scale: [1, 1], | |||
rotateWithView: true | |||
}) | |||
}), | |||
new Style({ | |||
image: new Icon({ | |||
anchor: [0.5, 0.5], | |||
src: imgChecked, | |||
crossOrigin: '', | |||
scale: [1, 1], | |||
rotateWithView: true | |||
}) | |||
}), | |||
new Style({ | |||
image: new Icon({ | |||
anchor: [0.5, 0.5], | |||
src: imgIgnored, | |||
crossOrigin: '', | |||
scale: [1, 1], | |||
rotateWithView: true | |||
}) | |||
}) | |||
] | |||
export const legendList = [ | |||
{ | |||
name: '待确认问题', | |||
color: '#F1EA0B' | |||
}, | |||
{ | |||
name: '当前问题', | |||
color: '#1AFA29' | |||
}, | |||
{ | |||
name: '已忽略问题', | |||
color: '#FF8A15' | |||
}, | |||
{ | |||
name: '已确定问题', | |||
color: '#FF3333' | |||
} | |||
] |
@@ -11,7 +11,7 @@ export default [ | |||
}, | |||
children: [ | |||
{ | |||
path: 'allTask', | |||
path: 'all', | |||
component: () => import('@/views/task-manage/all-task/index.vue'), | |||
name: 'TaskAll', | |||
title: '所有任务', |
@@ -29,7 +29,9 @@ export default defineComponent({ | |||
'update:visible': null | |||
}, | |||
setup(props, { emit }) { | |||
const data = reactive({}) | |||
const data = reactive({ | |||
}) | |||
/* 获取抽屉的信息 */ | |||
const getDrawerOptions = computed(() => { |
@@ -5,7 +5,7 @@ | |||
<DataTable | |||
ref="tableRef" | |||
:columns="columns" | |||
:request="loadDataTable" | |||
:data="data" | |||
:row-key="(row) => row.id" | |||
size="large" | |||
> | |||
@@ -55,7 +55,8 @@ export default { | |||
setup() { | |||
const data = reactive({ | |||
search, | |||
...toRefs(table) | |||
...toRefs(table), | |||
data: [{}] | |||
}) | |||
/** |
@@ -0,0 +1,101 @@ | |||
<template> | |||
<n-drawer v-bind="getDrawerOptions" @update:show="handleDrawerColse"> | |||
<n-drawer-content closable title="图片位置"> | |||
<position-msg :data="problemList" /> | |||
</n-drawer-content> | |||
</n-drawer> | |||
</template> | |||
<script> | |||
import { defineComponent, computed, reactive, toRefs, provide } from 'vue' | |||
import PositionMsg from '@/components/PositionMsg/index.vue' | |||
export default defineComponent({ | |||
name: 'PositionDrawer', | |||
components: { PositionMsg }, | |||
props: { | |||
/* 可见 */ | |||
visible: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
/* 选中的数据 */ | |||
data: { | |||
type: Object, | |||
default: () => {} | |||
} | |||
}, | |||
emits: { | |||
'update:visible': null | |||
}, | |||
setup(props, { emit }) { | |||
const data = reactive({ | |||
problemList: [ | |||
{ content: '河道内存在水生植被', | |||
fileImage: 'https://image.t-aaron.com/XJRW20220725103839/2022-07-25-10-41-46_frame-1500-1680_type-水生植被_VMgRwh05s4clHXCu_s-live-XJRW20220725103839-b73c470768f74422b287981fc75c31c3_AI.jpg', | |||
handlerImage: 'https://image.t-aaron.com/imagedir/kw6vv4m1yw_1658717157035.png', | |||
handlerResult: '', | |||
handlerTime: '2022-07-25 10:45:56', | |||
latitude: '31.829037194418085', | |||
location: '江苏省南京市江宁区秣陵街道东吉大道', | |||
longitude: '118.770222690945', | |||
name: 1, | |||
status: 1, | |||
type: 1, | |||
userName: '运管单位' }, | |||
{ content: '水生植被', | |||
fileImage: 'https://image.t-aaron.com/XJRW20220725103839/2022-07-25-10-41-46_frame-1500-1680_type-水生植被_VMgRwh05s4clHXCu_s-live-XJRW20220725103839-b73c470768f74422b287981fc75c31c3_AI.jpg', | |||
handlerImage: 'https://image.t-aaron.com/imagedir/kw6vv4m1yw_1658717157035.png', | |||
handlerResult: '', | |||
handlerTime: '2022-07-25 10:45:56', | |||
latitude: '31.129037194418085', | |||
location: '江苏省南京市江宁区秣陵街道东吉大道', | |||
longitude: '118.880222690945', | |||
name: 1, | |||
status: 2, | |||
type: 2, | |||
userName: '运管单位' } | |||
] | |||
}) | |||
const confirm = function(params) { | |||
console.log(params, '确认++++++++++++') | |||
} | |||
const ignore = function(params) { | |||
console.log(params, '忽略------------') | |||
} | |||
/* 注册确认方法 */ | |||
provide('confirm', confirm) | |||
/* 注册忽略方法 */ | |||
provide('ignore', ignore) | |||
/* 获取抽屉的信息 */ | |||
const getDrawerOptions = computed(() => { | |||
return { | |||
show: props.visible, | |||
width: '100%', | |||
placement: 'right' | |||
} | |||
}) | |||
/* 关闭抽屉 */ | |||
function handleDrawerColse() { | |||
emit('update:visible', false) | |||
} | |||
return { | |||
...toRefs(data), | |||
getDrawerOptions, | |||
handleDrawerColse | |||
} | |||
} | |||
}) | |||
</script> | |||
<style scoped lang='scss'> | |||
.n-button+.n-button{ | |||
margin-left: 30px; | |||
} | |||
</style> |
@@ -20,6 +20,9 @@ | |||
<ConfirmModal v-if="confirmModal" v-model:visible="confirmModal" :data="pageData" @update-data="tableReload" /> | |||
<!-- 图片位置 --> | |||
<position-drawer v-model:visible="positionDrawer" :data="rowData" /> | |||
</template> | |||
<script> | |||
@@ -28,19 +31,21 @@ import search from './tools/search.js' | |||
import HeadSearch from '@/components/Search/index.vue' | |||
import DataTable from '@/components/DataTable/index.vue' | |||
import ConfirmModal from './components/ConfirmModal.vue' | |||
import { h, computed, ref, reactive, toRefs } from 'vue' | |||
import PositionDrawer from './components/PositionDrawer.vue' | |||
import { ref, reactive, toRefs } from 'vue' | |||
export default { | |||
name: 'QuestionPage', | |||
components: { HeadSearch, DataTable, ConfirmModal }, | |||
components: { HeadSearch, DataTable, ConfirmModal, PositionDrawer }, | |||
setup() { | |||
const data = reactive({ | |||
search, | |||
...toRefs(table), | |||
data: [ | |||
{ id: 1, image: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg', type: 1 }, | |||
{ id: 2, image: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel2.jpeg', type: 2 } | |||
{ id: 1, image: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel1.jpeg', position: '软件园', type: 1 }, | |||
{ id: 2, image: 'https://naive-ui.oss-cn-beijing.aliyuncs.com/carousel-img/carousel2.jpeg', position: '东吉大道一号', type: 2 } | |||
], | |||
pageData: [] | |||
}) | |||
/** |
@@ -15,6 +15,12 @@ function handleRowConfirm(row) { | |||
console.log('问题确认') | |||
} | |||
/* 位置 */ | |||
function handlePositionDrawer(row) { | |||
data.rowData = row | |||
data.positionDrawer = true | |||
} | |||
function handleImgPreview() { | |||
data.confirmModal = true | |||
} | |||
@@ -22,6 +28,7 @@ function handleImgPreview() { | |||
const data = reactive({ | |||
rowData: {}, | |||
confirmModal: false, | |||
positionDrawer: false, | |||
columns: [ | |||
{ | |||
type: 'selection' | |||
@@ -59,8 +66,26 @@ const data = reactive({ | |||
}, | |||
{ | |||
title: '位置', | |||
key: 'name', | |||
align: 'center' | |||
key: 'position', | |||
align: 'center', | |||
render(row) { | |||
return h(TableAction, { | |||
actions: [ | |||
{ | |||
label: '图片位置', | |||
type: 'button', | |||
props: { | |||
type: 'primary', | |||
text: true, | |||
onClick: handlePositionDrawer.bind(null, row) | |||
}, | |||
auth: 'basic_list', | |||
show: row.status !== 1 | |||
} | |||
], | |||
align: 'center' | |||
}) | |||
} | |||
}, | |||
{ | |||
title: '备注', |