This commit is contained in:
MR-ZC 2025-03-23 15:16:30 +08:00
parent 115f669655
commit 492df1cb5f
25 changed files with 1389 additions and 237 deletions

6
.cursor/rules/init.mdc Normal file
View File

@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
请使用中文回答我的问题

View File

@ -1 +1 @@
VITE_BASE_URL = "192.168.100.1"
VITE_BASE_URL = "https://169.254.92.7:5173"

1102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,14 +11,17 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"@types/crypto-js": "^4.2.2",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"await-to-js": "^3.0.0",
"axios": "^1.8.1",
"cesium": "^1.127.0",
"crypto-js": "^4.2.0",
"element-plus": "^2.9.5",
"js-cookie": "^3.0.5",
"less": "^4.2.2",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
@ -28,6 +31,7 @@
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0",
"esbuild": "^0.25.1",
"npm-run-all2": "^7.0.2",
"typescript": "~5.7.3",
"vite": "^6.1.0",

14
src/api/task.ts Normal file
View File

@ -0,0 +1,14 @@
import http from "@/http"
import { to } from "await-to-js"
/**航点数据 */
export const queryPointRoutePreviewParam = (taskId: number) => to<any>(
http.get("/pro_api/foreign/task/queryPointRoutePreviewParam", {
params: { taskId }
}))
/**航面数据 */
export const queryRegionalPlanningPreviewParam = (taskId: number) => to<any>(
http.get('/pro_api/foreign/task/queryRegionalPlanningPreviewParam', {
params: { taskId }
}))

View File

@ -1,10 +1,10 @@
import request from "@/http";
import http from "@/http";
import to from "await-to-js"
/**登录 */
export const login = (data: any) => to(
request.post("/login", {
}
))
export const login = (data: any) => to<any>(
http.post("/api/login", {
username: data.username,
password: data.password
}))

View File

@ -8,7 +8,7 @@
<div class="header_main">
<div class="tabs">
<div v-for="tab in props.tabs" @click="changeType(tab)" class="tab"
:class="{ checked: tab.id == props.id }">
:class="{ checked: tab.id == id }">
<span>{{ tab.label }}</span>
</div>
</div>
@ -18,13 +18,14 @@
</template>
<script setup lang="ts">
import { ref } from "vue"
// import { ref } from "vue"
const props = defineProps(['tabs', 'id'])
const emit = defineEmits(["update:id"])
function changeType({ id, disabled }: { id: number, disabled: boolean }) {
const props = defineProps(['tabs'])
const id = defineModel<number>('id')
function changeType({ id:id_, disabled }: { id: number, disabled: boolean }) {
if (disabled) return;
emit("update:id", id)
console.log("id_", id_)
id.value = id_
}
</script>
@ -40,7 +41,6 @@ function changeType({ id, disabled }: { id: number, disabled: boolean }) {
z-index: -1;
display: flex;
height: 57px;
background-color: antiquewhite;
width: 100%;
img {

View File

@ -37,7 +37,6 @@
width: 249px;
object-fit: cover;
display: block;
background-color: antiquewhite;
}
.content {

View File

@ -51,6 +51,7 @@ const options = ref([
value: 4
},
])
</script>
<style lang="less" scoped>

View File

@ -10,9 +10,7 @@
</div>
<div class="container_center">
<div class="map_wrap">
<!-- 悬浮窗 -->
<img src="@/assets/img/image@1x.png" alt="">
<!-- <mainMap></mainMap> -->
<mainMap></mainMap>
</div>
<div class="event_list_wrap">
<eventList></eventList>
@ -25,11 +23,30 @@
</template>
<script setup lang="ts">
import { ref, provide, onMounted } from "vue"
import taskList from "@/components/zfControlPlatform/taskList/index.vue"
import positionView from "@/components/zfControlPlatform/positionView/index.vue"
import eventList from "@/components/zfControlPlatform/eventList/index.vue"
import taskController from "@/components/zfControlPlatform/taskController/index.vue"
import mainMap from "@/components/zfControlPlatform/mainMap/index.vue"
import { queryPointRoutePreviewParam, queryRegionalPlanningPreviewParam } from "@/api/task"
//
const pointRoutePreviewParam = ref<any[]>([])
provide("pointRoutePreviewParam", pointRoutePreviewParam)
//
async function getPointRoutePreviewParam() {
const [error, res] = await queryPointRoutePreviewParam(1)
if (error) return console.log("获取航点数据失败==>>", error);
pointRoutePreviewParam.value = res.data.waypointList
}
onMounted(() => {
getPointRoutePreviewParam()
})
</script>
<style lang="less" scoped>

View File

@ -38,30 +38,46 @@
</div>
</div>
</div>
<div width="600px" height="500px" class="cesiumContainer" id="cesiumContainer"></div>
<div class="cesiumContainer" id="cesiumContainer" ref="cesiumContainerRef"></div>
</div>
</template>
<script setup lang="ts">
import { inject, watch, ref as vueRef, nextTick } from "vue"
import * as Cesium from "cesium"
import { onMounted, ref } from "vue"
import type { Viewer, Property } from "cesium"
import { onMounted, onUnmounted, ref } from "vue"
import type { Ref } from "vue"
import "cesium/Build/Cesium/Widgets/widgets.css";
import type { PointRoute } from "@/types/map"
let viewer;
let handler;
let polyline;
let positions = [];
let isDrawing = ref(false);
let viewer: Viewer | null = null;
const afterRender = new Set<() => void>()
const cesiumContainerRef = vueRef<HTMLElement | null>(null);
const pointRoutePreviewParam = inject<Ref<PointRoute[]>>("pointRoutePreviewParam", ref([]))
async function init() {
// CesiumURL访
(window as any).CESIUM_BASE_URL = "/assets/Cesium/"
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkNWM3YTc2OS1iMjQwLTRiYTUtOGFhNC1lNmFhNDRlN2QyODIiLCJpZCI6MjgxNzc2LCJpYXQiOjE3NDEyNTI2Mjh9.6H_pbpxLPYMBGveKGq9pek2rYlrFvQeWcnHGzVkmb00'
Cesium.Ion.defaultAccessToken = token
await nextTick();
//
if (!cesiumContainerRef.value) {
console.error('cesiumContainer元素不存在');
return;
}
viewer = new Cesium.Viewer('cesiumContainer',
{
// terrain: Terrain.fromWorldTerrain(),
// terrainProvider: undefined,
try {
// Cesium
if (viewer) {
return;
}
// 使refDOM
viewer = new Cesium.Viewer(cesiumContainerRef.value, {
selectionIndicator: false, //
infoBox: false, //
navigationHelpButton: false, //
@ -72,46 +88,120 @@ async function init() {
animation: false, //
geocoder: false, //
fullscreenButton: false //
}
);
});
//
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(113.35, 23.11, 10000),
orientation: {
heading: Cesium.Math.toRadians(0.0),
pitch: Cesium.Math.toRadians(-15.0),
roll: Cesium.Math.toRadians(0.0)
//
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(106.64219088, 38.75395285, 2000),
orientation: {
heading: Cesium.Math.toRadians(0.0),
pitch: Cesium.Math.toRadians(-15.0),
roll: Cesium.Math.toRadians(0.0)
}
});
viewer.scene.mode = Cesium.SceneMode.SCENE2D; // 2D
viewer.scene.screenSpaceCameraController.maximumZoomDistance = 20000;
console.log('Cesium初始化成功');
} catch (error) {
console.error('初始化Cesium Viewer时出错:', error);
}
}
/**画出预览航线 */
function drawPointRoutePreview(pointRoute: PointRoute[]) {
if (!viewer) return
//
viewer.entities.removeAll();
// 线
const polyline = viewer.entities.add({
polyline: {
positions: pointRoute.map(item => {
return Cesium.Cartesian3.fromDegrees(item.wayPointLongitude, item.wayPointLatitude, item.wayPointAltitude)
}),
width: 4,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString("#3a76ff"),
dashLength: 20
}),
clampToGround: true
}
});
// viewer.fullscreenButton()
// setTimeout(() => {
// viewer.camera.flyTo({
// destination: Cartesian3.fromDegrees(113.35, 23.11, 10000),
// orientation: {
// heading: CesiumMath.toRadians(0.0),
// pitch: CesiumMath.toRadians(-15.0),
// roll: CesiumMath.toRadians(0.0)
// }
// });
// }, 5000)
//3d
// const buildingTileset = await createOsmBuildingsAsync();
// viewer.scene.primitives.add(buildingTileset);
viewer.scene.mode = Cesium.SceneMode.SCENE2D; // 2D
// viewer.scene.mode = SceneMode.SCENE3D; // 3D
// viewer.scene.screenSpaceCameraController.minimumZoomDistance = 100;
// viewer.scene.screenSpaceCameraController.maximumZoomDistance = 20000;
// viewer.zoomTo(300)
flyToPointRoute(viewer, pointRoute)
}
onMounted(() => {
init()
/**
* 将地图视角飞行到航线点的范围内
* @param viewer Cesium Viewer实例
* @param pointRoute 航线点数组
*/
function flyToPointRoute(viewer: Viewer, pointRoute: PointRoute[]) {
// 线
const positions = pointRoute.map(item => ({
longitude: item.wayPointLongitude,
latitude: item.wayPointLatitude
}));
//
const rectangle = Cesium.Rectangle.fromCartographicArray(
positions.map(pos => Cesium.Cartographic.fromDegrees(pos.longitude, pos.latitude))
);
// ,20%
const width = rectangle.east - rectangle.west;
const height = rectangle.north - rectangle.south;
const padding = {
x: width * 0.2, // 20%
y: height * 0.2 // 20%
};
//
const paddedRectangle = new Cesium.Rectangle(
rectangle.west - padding.x, // 西
rectangle.south - padding.y, //
rectangle.east + padding.x, //
rectangle.north + padding.y //
);
// 使
viewer.camera.flyTo({
destination: paddedRectangle, //
duration: 2 // 2
});
}
watch(() => pointRoutePreviewParam.value, () => {
if (pointRoutePreviewParam.value.length > 0) {
if (viewer) {
drawPointRoutePreview(pointRoutePreviewParam.value as PointRoute[])
} else {
afterRender.add(() => {
drawPointRoutePreview(pointRoutePreviewParam.value as PointRoute[])
})
}
}
})
onMounted(async () => {
// 使nextTickDOM
await nextTick();
// Cesium
await init();
//
afterRender.forEach(fn => fn());
});
// Cesium
onUnmounted(() => {
if (viewer) {
viewer.destroy();
viewer = null;
}
});
</script>

View File

@ -30,7 +30,6 @@ import titleView from "@/components/zfControlPlatform/titleView/index.vue"
.content {
flex: 1;
height: 0;
background-color: antiquewhite;
img{
width: 100%;
height: 100%;

View File

@ -1,49 +1,52 @@
<template>
<div class="step_view">
<div class="step">
<div class="step" v-for="item in props.taskProgress">
<div class="content">
<div class="left">
<div class="time">11:49</div>
<div class="time">{{ item.time }}</div>
<div class="circle"></div>
<div class="label">任务前检测</div>
<div class="label">{{ item.label }}</div>
</div>
<div class="status_icon">
<img src="@/assets/img/circle_check.svg" alt="">
</div>
</div>
</div>
<div class="step">
<div class="content">
<div class="left">
<div class="time">11:49</div>
<div class="circle"></div>
<div class="label">任务前检测</div>
</div>
<div class="status_icon">
<img src="@/assets/img/circle_check.svg" alt="">
</div>
</div>
</div>
<div class="step">
<div class="content">
<div class="left">
<div class="time">11:49</div>
<div class="circle"></div>
<div class="label">任务前检测</div>
</div>
<div class="status_icon">
<img src="@/assets/img/zf_task_loading_icon.svg" alt="">
<div :class="['status_icon', { loading: item.status === 'loading' }]">
<img v-if="item.status === 'success'" src="@/assets/img/circle_check.svg" alt="">
<img v-else src="@/assets/img/zf_task_loading_icon.svg" alt="">
</div>
</div>
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
type TaskProgress = {
time: string,
label: string,
status: string
}
type Props = {
taskProgress: TaskProgress[]
}
const props = withDefaults(defineProps<Props>(), {
taskProgress: () => []
})
</script>
<style lang="less" scoped>
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.step_view {
// background-color: antiquewhite;
width: 100%;
@ -100,6 +103,25 @@
}
}
}
.status_icon {
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
&.loading {
img {
animation: loading 1s linear infinite;
}
}
img {
width: 100%;
height: 100%;
}
}
}
}
}

View File

@ -18,7 +18,7 @@
</template>
<script setup lang="ts">
// import taskItem from './taskItem.vue';
import taskItem from './taskItem.vue';
import ongoingTask from './ongoingTask.vue';
import titleView from "@/components/zfControlPlatform/titleView/index.vue"
</script>

View File

@ -1,6 +1,5 @@
<template>
<div class="task_main">
<div class="base_info">
<div class="info_item">
<span class="label">任务名称</span>
@ -21,9 +20,8 @@
</div>
</div>
<div class="step">
<stepView></stepView>
<stepView :taskProgress="taskProgress"></stepView>
</div>
</div>
</template>
@ -31,6 +29,29 @@
import { ref } from "vue"
import stepView from "@/components/zfControlPlatform/stepView/index.vue"
const progress = ref(50)
type TaskProgress = {
time: string
label: string
status: "success" | "loading"
}
const taskProgress = ref<TaskProgress[]>([
{
time: "11:49",
label: "任务前检测",
status: "success"
},
{
time: "11:49",
label: "任务前检测",
status: "success"
},
{
time: "11:49",
label: "任务前检测",
status: "loading"
}
])
</script>
<style lang="less" scoped>

View File

@ -22,7 +22,6 @@
display: flex;
align-items: center;
justify-content: space-between;
background-color: antiquewhite;
border-bottom: 1px #3c7bff solid;
padding: 0 20px;
background: linear-gradient(90deg, #0A5CC8 3%, #053B7B 95%);

View File

@ -1,17 +1,37 @@
import axios from "axios";
import type { AxiosResponse, AxiosError } from "axios";
import useUserInfoStore from "@/stores/user"
import { ElMessage } from "element-plus"
const userInfoStore = useUserInfoStore()
const request = axios.create({
baseURL: import.meta.env.VITE_BASE_URL
})
request.interceptors.request.use((config) => {
config.headers.Authorization = userInfoStore.token
return config
})
request.interceptors.response.use((res) => {
return res
})
request.interceptors.response.use(
(res: AxiosResponse) => {
if (res.data.code === 200) {
return res.data
} else {
ElMessage({
type: "error",
message: res.data.msg || "请求失败"
})
return Promise.reject(res?.data?.msg || "请求失败")
}
},
(err: AxiosError) => {
ElMessage({
type: "error",
message: (err.response?.data as any)?.msg || "请求失败"
})
return Promise.reject((err.response?.data as any)?.msg || "请求失败")
}
)
export default request

View File

@ -2,6 +2,7 @@ import './assets/style/base.less'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
@ -10,7 +11,10 @@ import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(createPinia())
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

View File

@ -1,5 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/index/index.vue'
import useUserInfoStore from "@/stores/user"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -22,5 +24,19 @@ const router = createRouter({
],
})
router.beforeEach((to, from, next) => {
const userInfoStore = useUserInfoStore()
console.log("token==>>", userInfoStore.token)
if (to.path === "/login") {
if (userInfoStore.token) {
next({ path: "/" })
} else {
next()
}
}
if (!userInfoStore.token) return next({ path: "/login" })
next()
})
export default router

View File

@ -2,14 +2,22 @@ import { defineStore } from 'pinia'
const useUserInfoStore = defineStore('userInfo', {
state: () => ({
token: ""
token: "",
user_info: {
last_login_at: "",
last_login_ip: "",
login_count: 0,
username: ""
}
}),
getters: {
},
actions: {
}
},
persist: true
})
export default useUserInfoStore

5
src/types/map.ts Normal file
View File

@ -0,0 +1,5 @@
export type PointRoute = {
wayPointLatitude: number,
wayPointLongitude: number,
wayPointAltitude: number
}

12
src/utils/crypto.ts Normal file
View File

@ -0,0 +1,12 @@
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'zx_web_secret_key_2024'
export const encrypt = (text: string): string => {
return CryptoJS.AES.encrypt(text, SECRET_KEY).toString()
}
export const decrypt = (ciphertext: string): string => {
const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY)
return bytes.toString(CryptoJS.enc.Utf8)
}

View File

@ -42,11 +42,11 @@ export default [
label: "智能助手",
disabled: true,
},
{
id: 7,
type: "IFRAME",
label: "测试",
// disabled: true,
src:"http://127.0.0.1:5500/iframe%E9%80%9A%E8%AE%AF.html"
},
// {
// id: 7,
// type: "IFRAME",
// label: "测试",
// disabled: true,
// src:"http://127.0.0.1:5500/iframe%E9%80%9A%E8%AE%AF.html"
// },
]

View File

@ -7,17 +7,19 @@
</div>
<h2 class="title">交通运输综合执法检查系统</h2>
<form class="form_wrap" @submit="submit">
<div class="form_item" :class="{ error: errors.userName }">
<div class="form_item" :class="{ error: errors.username }">
<div class="icon">
<img src="@/assets/img/login_user_icon.png" alt="">
</div>
<input class="input" v-model="loginFormData.userName" type="text" placeholder="请输入账号" @input="clearError('userName')">
<input class="input" v-model="loginFormData.username" type="text" placeholder="请输入账号"
@input="clearError('username')">
</div>
<div class="form_item" :class="{ error: errors.password }">
<div class="icon">
<img src="@/assets/img/login_password_icon.png" alt="">
</div>
<input class="input" v-model="loginFormData.password" type="password" placeholder="请输入密码" @input="clearError('password')">
<input class="input" v-model="loginFormData.password" type="password" placeholder="请输入密码"
@input="clearError('password')">
</div>
<div class="options">
<label class="save_password">
@ -38,44 +40,53 @@
<script setup lang="ts">
import { ref } from "vue"
import { login } from "@/api/user.ts"
import { login } from "@/api/user"
import { ElMessage } from "element-plus"
import {useRouter} from "vue-router"
import { useRouter } from "vue-router"
import useUserInfoStore from "@/stores/user"
import { encrypt, decrypt } from "@/utils/crypto"
const router = useRouter()
const savePasswordFlag = ref(!!(localStorage.getItem('user')))
const loginFormData = ref(JSON.parse(localStorage.getItem('user') || '{"userName":"","password":""}'))
const savePasswordFlag = ref(!!(localStorage.getItem('username') && localStorage.getItem('_k')))
const loginFormData = ref({
username: localStorage.getItem('username') || '',
password: localStorage.getItem('_k') ? decrypt(localStorage.getItem('_k')!) : ''
})
//
const errors = ref({
userName: false,
username: false,
password: false
})
const userInfoStore = useUserInfoStore()
//
function clearError(field: 'userName' | 'password') {
function clearError(field: 'username' | 'password') {
errors.value[field] = false
}
function changSavePassword() {
if (savePasswordFlag.value) {
localStorage.setItem("user", JSON.stringify(loginFormData.value))
localStorage.setItem("username", loginFormData.value.username)
localStorage.setItem("_k", encrypt(loginFormData.value.password))
} else {
localStorage.removeItem("user")
localStorage.removeItem("username")
localStorage.removeItem("_k")
}
}
//
function submit(event: Event) {
event.preventDefault()
console.log(loginFormData.value)
changSavePassword()
if (!validate()) return;
router.push("/")
toLogin()
}
/**表单校验 */
function validate() {
errors.value.userName = !loginFormData.value.userName
errors.value.username = !loginFormData.value.username
errors.value.password = !loginFormData.value.password
if (errors.value.userName) {
if (errors.value.username) {
ElMessage({
type: "error",
message: "请输入用户名"
@ -95,7 +106,16 @@ function validate() {
async function toLogin() {
const [err, res] = await login({})
const [err, res] = await login({
username: loginFormData.value.username,
password: loginFormData.value.password
})
if (err) {
return
}
userInfoStore.token = res.data.access_token
userInfoStore.user_info = res.data.user_info
router.push("/")
}
</script>

View File

@ -25,5 +25,12 @@ export default defineConfig({
key: fs.readFileSync(path.resolve(__dirname, 'cert/privkey.pem')),
cert: fs.readFileSync(path.resolve(__dirname, 'cert/fullchain.pem')),
},
proxy:{
'/api': {
changeOrigin: true,
target: 'http://192.168.10.176:6001',
rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
},
})
})