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 Bus from './Bus/index'
import MyCesium from './Cesium/index'
import MyWebsocket from './Websocket/index'
export default {
install(Vue) {
@ -26,6 +28,8 @@ export default {
Vue.directive('loading', Loading)
window.$bus = Bus
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'" />
<div class="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 class="zt-item flex ai-c jc-sb">
<!-- <div class="zt-item flex ai-c jc-sb">
<span style="min-width: 100px">推演方</span>
<a-select
style="width: 120px"
@ -61,14 +61,18 @@
<a-select-option value="1">红方</a-select-option>
<a-select-option value="2">蓝方</a-select-option>
</a-select>
</div> -->
<div class="zt-item flex ai-c">
<span style="min-width: 100px">推演方</span>
<span>{{ right.detail.position.deduceTypeName }}</span>
</div>
<div class="zt-item flex ai-c">
<span style="min-width: 100px">类型</span>
<span></span>
<span>{{ right.detail.position.logisticType | logisticTypeFormat }}</span>
</div>
<div class="zt-item flex ai-c">
<span style="min-width: 100px">方向</span>
<span></span>
<span>{{ right.detail.position.direction | numberFormat }}°</span>
</div>
<div class="zt-item flex ai-c">
<span style="min-width: 100px">速度</span>
@ -77,17 +81,17 @@
<div class="zt-item flex ai-c">
<span style="min-width: 100px">经度</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 class="zt-item flex ai-c">
<span style="min-width: 100px">纬度</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 class="zt-item flex ai-c">
<span style="min-width: 100px">高度/深度</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>
</a-collapse-panel>
<a-collapse-panel class="simulation-collapse-item" key="2" header="人员状态属性">
@ -200,31 +204,40 @@
<a-collapse-panel class="simulation-collapse-item" key="9" header="保障配置">
<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> -->
</div>
</a-collapse-panel>
</a-collapse>
</div>
<div v-if="right.radioType === 'zzxd'">
<div class="zzxd-wrapper flex-v">
<div class="zzxd-header">
<div class="zzxd-title">作战行动</div>
<div>
<a-button type="text-primary" icon="menu"></a-button>
<a-button type="text-primary" icon="plus"></a-button>
<a-button type="text-primary" icon="edit"></a-button>
<a-button type="text-primary" icon="delete"></a-button>
</div>
<div v-if="right.radioType === 'zzxd'" class="zzxd-wrapper flex-v">
<div class="zzxd-header flex ai-c jc-sb">
<div class="zzxd-title">作战行动</div>
<div>
<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="edit" @click="handleOpenEditActionModal"></a-button>
<a-button type="text-primary" icon="delete" @click="handleDeleteAction"></a-button>
</div>
<div class="flex-1">
<a-steps progress-dot :current="1" direction="vertical">
<a-step
v-for="item in right.detail.actionList"
:key="item.id"
:title="item.typeName"
:description="`开始时间:${item.beginDateTime}. 结束时间:${item.endDateTime}`"
/>
</a-steps>
</div>
<div class="flex-1" style="overflow-y: auto">
<div
v-for="item in right.detail.actionList"
:key="item.id"
class="action-item flex"
@click="right.checkedAction = item"
>
<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>
@ -246,7 +259,14 @@
</div>
</div>
</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-col :span="6">
<span>名称</span>
@ -261,6 +281,7 @@
title="修改单元经纬度"
:maskClosable="false"
width="600px"
:destroyOnClose="true"
@ok="handleSubmilLonlat"
>
<LonLatInput :lon.sync="lonlatModal.lon" :lat.sync="lonlatModal.lat" />
@ -270,6 +291,7 @@
title="设置单元高度/深度"
:maskClosable="false"
width="400px"
:destroyOnClose="true"
@ok="handleSubmilHd"
>
<a-row>
@ -281,6 +303,15 @@
</a-col>
</a-row>
</a-modal>
<a-modal
v-model="actionModal.visible"
:title="actionModal.title"
:maskClosable="false"
:destroyOnClose="true"
@ok="handleSubmilAction"
>
<div></div>
</a-modal>
</Grid>
</template>
@ -293,6 +324,18 @@ export default {
LonLatInput,
},
filters: {
logisticTypeFormat(v) {
return {
1: '信息对抗分队',
2: '边防作战分队',
3: '防化保障分队',
4: '火力打击分队',
5: '餐饮保障分队',
6: '运输保障分队',
7: '医疗分队',
8: '工兵分队',
}[v]
},
numberFormat(v) {
if (typeof v === 'number' && v) {
return +v.toFixed(2)
@ -386,6 +429,7 @@ export default {
actionList: [],
equipmentList: [],
},
checkedAction: null,
},
mcModal: {
visible: false,
@ -400,8 +444,16 @@ export default {
visible: false,
hd: 0,
},
actionModal: {
visible: false,
},
}
},
computed: {
hasDetail() {
return Boolean(this.right.detail.id)
},
},
mounted() {
this.cesium = new window.MyCesium('cesium-container')
this.scenarioId = 2733
@ -442,6 +494,7 @@ export default {
},
handleSelectTree(selectedKeys, { node }) {
this.right.detail = node.dataRef
this.right.checkedAction = null
},
onSearch(e) {
console.log('----', e, e.target.value)
@ -454,13 +507,14 @@ export default {
async handleSubmilMc() {
try {
this.$http({
url: '/save',
url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post',
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.mcModal.visible = false
} catch (error) {
@ -476,14 +530,17 @@ export default {
async handleSubmilLonlat() {
try {
this.$http({
url: '/save',
url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post',
data: {
lon: this.lonlatModal.lon,
lat: this.lonlatModal.lat,
id: this.right.detail.id,
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.lat = '' + this.lonlatModal.lat
this.lonlatModal.visible = false
@ -499,19 +556,59 @@ export default {
async handleSubmilHd() {
try {
this.$http({
url: '/save',
url: `/scenario/power/modifyUnit/${this.scenarioId}`,
method: 'post',
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.hdModal.visible = false
} catch (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>
@ -593,6 +690,41 @@ export default {
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 lang="less">