websocket

This commit is contained in:
liaoboping 2025-08-20 10:23:39 +08:00
parent 8d8797f54a
commit 39dd3a2f8b
4 changed files with 307 additions and 38 deletions

View File

@ -0,0 +1,2 @@
import Vue from 'vue'
export default new Vue()

View File

@ -0,0 +1,131 @@
export default class MyWebSocket {
/**
* 创建 WebSocket 实例
* @param {string} path - WebSocket 服务路径
* @param {Function} messageCallback - 消息处理回调函数 (error, data) => void
*/
constructor(path, messageCallback) {
// 拼接完整 WebSocket 地址
const baseUrl = process.env.VUE_APP_WEBSOCKET_URL
this.url = new URL(path, baseUrl).href.replace(/^http/, 'ws')
this.messageCallback = messageCallback
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
this.reconnectInterval = 2000
this.messageQueue = []
this.connectionPromise = null
this._connect()
}
/** 建立 WebSocket 连接 */
_connect() {
this.ws = new WebSocket(this.url)
this.ws.addEventListener('open', () => {
this.reconnectAttempts = 0 // 重置重连计数
this._flushQueue() // 发送队列中的消息
})
this.ws.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data)
this.messageCallback(null, data)
} catch (error) {
this.messageCallback(new Error(`消息解析失败: ${error.message} | 原始数据: ${event.data}`), null)
}
})
this.ws.addEventListener('error', (error) => {
this.messageCallback(new Error(`连接错误: ${error.message || '未知错误'}`), null)
})
this.ws.addEventListener('close', (event) => {
// 非正常关闭时尝试重连
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++
this._connect()
}, this.reconnectInterval)
}
})
}
/** 发送队列中缓存的消息 */
_flushQueue() {
while (this.messageQueue.length > 0 && this.isConnected()) {
const message = this.messageQueue.shift()
this._sendInternal(message)
}
}
/** 内部发送方法 */
_sendInternal(data) {
if (!this.isConnected()) return
try {
const payload = JSON.stringify(data)
this.ws.send(payload)
} catch (error) {
this.messageCallback(new Error(`消息发送失败: ${error.message}`), null)
}
}
/**
* 检查连接状态
* @returns {boolean} 是否已连接
*/
isConnected() {
return this.ws?.readyState === WebSocket.OPEN
}
/**
* 发送消息
* @param {Object} data - 要发送的数据
*/
send(data) {
if (this.isConnected()) {
this._sendInternal(data)
} else {
// 连接未就绪时缓存消息
this.messageQueue.push(data)
// 首次连接时创建连接等待
if (!this.connectionPromise) {
this.connectionPromise = new Promise((resolve) => {
const check = () => {
if (this.isConnected()) {
resolve()
} else {
setTimeout(check, 100)
}
}
check()
})
}
}
}
/** 关闭连接 */
close() {
if (this.ws) {
this.ws.close(1000, '用户主动关闭')
this.reconnectAttempts = this.maxReconnectAttempts // 禁止重连
}
}
}
// // 使用示例
// const ws = new MyWebSocket('/notification', (err, data) => {
// if (err) {
// console.error('WebSocket 错误:', err);
// return;
// }
// console.log('收到消息:', data);
// });
// // 发送消息
// ws.send({ type: 'ping', timestamp: Date.now() });
// // 关闭连接(在组件销毁时调用)
// // ws.close();

View File

@ -10,7 +10,9 @@ import WangEditor from './WangEditor/Index.vue'
import Loading from './Directives/Loading' import Loading from './Directives/Loading'
import Bus from './Bus/index'
import MyCesium from './Cesium/index' import MyCesium from './Cesium/index'
import MyWebsocket from './Websocket/index'
export default { export default {
install(Vue) { install(Vue) {
@ -26,6 +28,8 @@ export default {
Vue.directive('loading', Loading) Vue.directive('loading', Loading)
window.$bus = Bus
window.MyCesium = MyCesium window.MyCesium = MyCesium
window.MyWebsocket = MyWebsocket
}, },
} }

View File

@ -48,9 +48,9 @@
<img class="image" :src="right.detail.image || '/mockData/fe6ad2d8-da11-04d8-f447-0d8175826e28.png'" /> <img class="image" :src="right.detail.image || '/mockData/fe6ad2d8-da11-04d8-f447-0d8175826e28.png'" />
<div class="name"> <div class="name">
{{ right.detail.name }} {{ right.detail.name }}
<a-button type="text-primary" icon="edit" @click="handleOpenMcModal"></a-button> <a-button v-if="hasDetail" type="text-primary" icon="edit" @click="handleOpenMcModal"></a-button>
</div> </div>
<div class="zt-item flex ai-c jc-sb"> <!-- <div class="zt-item flex ai-c jc-sb">
<span style="min-width: 100px">推演方</span> <span style="min-width: 100px">推演方</span>
<a-select <a-select
style="width: 120px" style="width: 120px"
@ -61,14 +61,18 @@
<a-select-option value="1">红方</a-select-option> <a-select-option value="1">红方</a-select-option>
<a-select-option value="2">蓝方</a-select-option> <a-select-option value="2">蓝方</a-select-option>
</a-select> </a-select>
</div> -->
<div class="zt-item flex ai-c">
<span style="min-width: 100px">推演方</span>
<span>{{ right.detail.position.deduceTypeName }}</span>
</div> </div>
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">类型</span> <span style="min-width: 100px">类型</span>
<span></span> <span>{{ right.detail.position.logisticType | logisticTypeFormat }}</span>
</div> </div>
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">方向</span> <span style="min-width: 100px">方向</span>
<span></span> <span>{{ right.detail.position.direction | numberFormat }}°</span>
</div> </div>
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">速度</span> <span style="min-width: 100px">速度</span>
@ -77,17 +81,17 @@
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">经度</span> <span style="min-width: 100px">经度</span>
<span class="flex-1">{{ right.detail.position.lng | lonFormat }}</span> <span class="flex-1">{{ right.detail.position.lng | lonFormat }}</span>
<a-button type="text-primary" icon="edit" @click="handleOpenLonlatModal"></a-button> <a-button v-if="hasDetail" type="text-primary" icon="edit" @click="handleOpenLonlatModal"></a-button>
</div> </div>
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">纬度</span> <span style="min-width: 100px">纬度</span>
<span class="flex-1">{{ right.detail.position.lat | latFormat }}</span> <span class="flex-1">{{ right.detail.position.lat | latFormat }}</span>
<a-button type="text-primary" icon="edit" @click="handleOpenLonlatModal"></a-button> <a-button v-if="hasDetail" type="text-primary" icon="edit" @click="handleOpenLonlatModal"></a-button>
</div> </div>
<div class="zt-item flex ai-c"> <div class="zt-item flex ai-c">
<span style="min-width: 100px">高度/深度</span> <span style="min-width: 100px">高度/深度</span>
<span class="flex-1">{{ right.detail.position.height | numberFormat }} (海拔)</span> <span class="flex-1">{{ right.detail.position.height | numberFormat }} (海拔)</span>
<a-button type="text-primary" icon="edit" @click="handleOpenHdModal()"></a-button> <a-button v-if="hasDetail" type="text-primary" icon="edit" @click="handleOpenHdModal()"></a-button>
</div> </div>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel class="simulation-collapse-item" key="2" header="人员状态属性"> <a-collapse-panel class="simulation-collapse-item" key="2" header="人员状态属性">
@ -200,31 +204,40 @@
<a-collapse-panel class="simulation-collapse-item" key="9" header="保障配置"> <a-collapse-panel class="simulation-collapse-item" key="9" header="保障配置">
<div class="zt-item flex ai-c jc-sb"> <div class="zt-item flex ai-c jc-sb">
<a-button type="primary">兵力编组</a-button> <a-button type="primary">兵力编组</a-button>
<a-button type="primary">关注</a-button> <!-- <a-button type="primary">关注</a-button> -->
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</div> </div>
<div v-if="right.radioType === 'zzxd'"> <div v-if="right.radioType === 'zzxd'" class="zzxd-wrapper flex-v">
<div class="zzxd-wrapper flex-v"> <div class="zzxd-header flex ai-c jc-sb">
<div class="zzxd-header"> <div class="zzxd-title">作战行动</div>
<div class="zzxd-title">作战行动</div> <div>
<div> <a-button type="text-primary" icon="menu"></a-button>
<a-button type="text-primary" icon="menu"></a-button> <a-button type="text-primary" icon="plus" @click="handleOpenAddActionModal"></a-button>
<a-button type="text-primary" icon="plus"></a-button> <a-button type="text-primary" icon="edit" @click="handleOpenEditActionModal"></a-button>
<a-button type="text-primary" icon="edit"></a-button> <a-button type="text-primary" icon="delete" @click="handleDeleteAction"></a-button>
<a-button type="text-primary" icon="delete"></a-button>
</div>
</div> </div>
<div class="flex-1"> </div>
<a-steps progress-dot :current="1" direction="vertical"> <div class="flex-1" style="overflow-y: auto">
<a-step <div
v-for="item in right.detail.actionList" v-for="item in right.detail.actionList"
:key="item.id" :key="item.id"
:title="item.typeName" class="action-item flex"
:description="`开始时间:${item.beginDateTime}. 结束时间:${item.endDateTime}`" @click="right.checkedAction = item"
/> >
</a-steps> <div class="action-icon">
<div class="action-line"></div>
<a-radio
:checked="right.checkedAction && right.checkedAction.id === item.id"
style="margin-right: 0"
></a-radio>
</div>
<div class="flex-1">
<div class="action-title">{{ item.typeName || '- -' }}</div>
<div class="action-time">开始时间{{ item.beginDateTime }}</div>
<div class="action-time">结束时间{{ item.endDateTime }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -246,7 +259,14 @@
</div> </div>
</div> </div>
</ModuleWrapper> </ModuleWrapper>
<a-modal v-model="mcModal.visible" title="编辑单元名称" :maskClosable="false" width="400px" @ok="handleSubmilMc"> <a-modal
v-model="mcModal.visible"
title="编辑单元名称"
:maskClosable="false"
width="400px"
:destroyOnClose="true"
@ok="handleSubmilMc"
>
<a-row> <a-row>
<a-col :span="6"> <a-col :span="6">
<span>名称</span> <span>名称</span>
@ -261,6 +281,7 @@
title="修改单元经纬度" title="修改单元经纬度"
:maskClosable="false" :maskClosable="false"
width="600px" width="600px"
:destroyOnClose="true"
@ok="handleSubmilLonlat" @ok="handleSubmilLonlat"
> >
<LonLatInput :lon.sync="lonlatModal.lon" :lat.sync="lonlatModal.lat" /> <LonLatInput :lon.sync="lonlatModal.lon" :lat.sync="lonlatModal.lat" />
@ -270,6 +291,7 @@
title="设置单元高度/深度" title="设置单元高度/深度"
:maskClosable="false" :maskClosable="false"
width="400px" width="400px"
:destroyOnClose="true"
@ok="handleSubmilHd" @ok="handleSubmilHd"
> >
<a-row> <a-row>
@ -281,6 +303,15 @@
</a-col> </a-col>
</a-row> </a-row>
</a-modal> </a-modal>
<a-modal
v-model="actionModal.visible"
:title="actionModal.title"
:maskClosable="false"
:destroyOnClose="true"
@ok="handleSubmilAction"
>
<div></div>
</a-modal>
</Grid> </Grid>
</template> </template>
@ -293,6 +324,18 @@ export default {
LonLatInput, LonLatInput,
}, },
filters: { filters: {
logisticTypeFormat(v) {
return {
1: '信息对抗分队',
2: '边防作战分队',
3: '防化保障分队',
4: '火力打击分队',
5: '餐饮保障分队',
6: '运输保障分队',
7: '医疗分队',
8: '工兵分队',
}[v]
},
numberFormat(v) { numberFormat(v) {
if (typeof v === 'number' && v) { if (typeof v === 'number' && v) {
return +v.toFixed(2) return +v.toFixed(2)
@ -386,6 +429,7 @@ export default {
actionList: [], actionList: [],
equipmentList: [], equipmentList: [],
}, },
checkedAction: null,
}, },
mcModal: { mcModal: {
visible: false, visible: false,
@ -400,8 +444,16 @@ export default {
visible: false, visible: false,
hd: 0, hd: 0,
}, },
actionModal: {
visible: false,
},
} }
}, },
computed: {
hasDetail() {
return Boolean(this.right.detail.id)
},
},
mounted() { mounted() {
this.cesium = new window.MyCesium('cesium-container') this.cesium = new window.MyCesium('cesium-container')
this.scenarioId = 2733 this.scenarioId = 2733
@ -442,6 +494,7 @@ export default {
}, },
handleSelectTree(selectedKeys, { node }) { handleSelectTree(selectedKeys, { node }) {
this.right.detail = node.dataRef this.right.detail = node.dataRef
this.right.checkedAction = null
}, },
onSearch(e) { onSearch(e) {
console.log('----', e, e.target.value) console.log('----', e, e.target.value)
@ -454,13 +507,14 @@ export default {
async handleSubmilMc() { async handleSubmilMc() {
try { try {
this.$http({ this.$http({
url: '/save', url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post', method: 'post',
data: { data: {
mc: this.mcModal.mc, id: this.right.detail.id,
name: this.mcModal.mc,
}, },
}) })
this.$message('编辑单元名称成功') this.$message.success('编辑单元名称成功')
this.right.detail.name = this.mcModal.mc this.right.detail.name = this.mcModal.mc
this.mcModal.visible = false this.mcModal.visible = false
} catch (error) { } catch (error) {
@ -476,14 +530,17 @@ export default {
async handleSubmilLonlat() { async handleSubmilLonlat() {
try { try {
this.$http({ this.$http({
url: '/save', url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post', method: 'post',
data: { data: {
lon: this.lonlatModal.lon, id: this.right.detail.id,
lat: this.lonlatModal.lat, position: {
lng: this.lonlatModal.lon,
lat: this.lonlatModal.lat,
},
}, },
}) })
this.$message('修改单元经纬度成功') this.$message.success('修改单元经纬度成功')
this.right.detail.position.lng = '' + this.lonlatModal.lon this.right.detail.position.lng = '' + this.lonlatModal.lon
this.right.detail.position.lat = '' + this.lonlatModal.lat this.right.detail.position.lat = '' + this.lonlatModal.lat
this.lonlatModal.visible = false this.lonlatModal.visible = false
@ -499,19 +556,59 @@ export default {
async handleSubmilHd() { async handleSubmilHd() {
try { try {
this.$http({ this.$http({
url: '/save', url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post', method: 'post',
data: { data: {
hd: this.hdModal.hd, id: this.right.detail.id,
position: {
height: this.hdModal.hd,
},
}, },
}) })
this.$message('修改单元高度/深度成功') this.$message.success('修改单元高度/深度成功')
this.right.detail.position.height = this.hdModal.hd this.right.detail.position.height = this.hdModal.hd
this.hdModal.visible = false this.hdModal.visible = false
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
}, },
handleOpenAddActionModal() {
this.actionModal.title = '添加事件信息'
this.actionModal.formData = {}
this.actionModal.visible = true
},
handleOpenEditActionModal() {
this.actionModal.title = '修改事件信息'
this.actionModal.formData = { ...this.right.checkedAction }
this.actionModal.visible = true
},
async handleSubmilAction() {
try {
await this.$http({
url: '/save',
method: 'post',
data: this.actionModal.formData,
})
this.$message.success(`${this.actionModal.title}成功`)
} catch (error) {
console.log(error)
}
},
async handleDeleteAction() {
try {
await this.$confirm('确认删除所选事件吗?')
await this.$http({
url: '/delete',
method: 'delete',
params: { id: this.right.checkedAction.id },
})
this.$message.success(`删除事件成功`)
} catch (error) {
this.$message.success(`删除事件失败`)
console.log(error)
}
},
}, },
} }
</script> </script>
@ -593,6 +690,41 @@ export default {
color: #bbdded; color: #bbdded;
} }
} }
.zzxd-wrapper {
height: 100%;
.zzxd-header {
padding: 5px 0;
border-bottom: 1px solid #00baff22;
.zzxd-title {
color: #00baff;
}
}
}
.action-item {
cursor: pointer;
padding: 5px 0;
.action-icon {
padding: 5px 16px;
position: relative;
flex-shrink: 0;
.action-line {
position: absolute;
right: 50%;
top: 21px;
width: 1px;
height: 100%;
background-color: #00baff;
}
}
}
.action-item:last-of-type {
.action-line {
display: none;
}
}
.action-item:hover {
background-color: #bae7ff44;
}
</style> </style>
<style lang="less"> <style lang="less">