This commit is contained in:
JayJiaJun 2025-05-27 15:22:50 +08:00
parent 9911c4d8c5
commit b4a2661c04
6 changed files with 1754 additions and 44 deletions

View File

@ -6,5 +6,8 @@
"i18n-ally.localesPaths": ["src/lang"], // "i18n-ally.localesPaths": ["src/lang"], //
"i18n-ally.keystyle": "flat", "i18n-ally.keystyle": "flat",
"i18n-ally.sourceLanguage": "en-US", // "i18n-ally.sourceLanguage": "en-US", //
"i18n-ally.displayLanguage": "zh-CN" // "i18n-ally.displayLanguage": "zh-CN",
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
} //
} }

View File

@ -127,3 +127,6 @@ Element.Table.props.border = {
//设置点击所有弹窗的遮罩不会关闭弹窗 //设置点击所有弹窗的遮罩不会关闭弹窗
Element.Dialog.props.closeOnClickModal.default = false; Element.Dialog.props.closeOnClickModal.default = false;

1119
src/views/iot/device/.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -69,7 +69,7 @@
<template slot="prepend">Version</template> <template slot="prepend">Version</template>
<template slot="append">{{ form.firmwareType === 1 ? <template slot="append">{{ form.firmwareType === 1 ?
$t('firmware.index.222541-52') : $t('firmware.index.222541-53') $t('firmware.index.222541-52') : $t('firmware.index.222541-53')
}}</template> }}</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<!-- 设备影子 --> <!-- 设备影子 -->
@ -187,6 +187,8 @@
ref="gatewayRunningStatus" :device="form" @statusEvent="getDeviceStatusData($event)" /> ref="gatewayRunningStatus" :device="form" @statusEvent="getDeviceStatusData($event)" />
<relay v-else-if="form.productName && form.productName.toLowerCase().includes('多路控制器')" ref="relay" <relay v-else-if="form.productName && form.productName.toLowerCase().includes('多路控制器')" ref="relay"
:device="form" @statusEvent="getDeviceStatusData($event)" /> :device="form" @statusEvent="getDeviceStatusData($event)" />
<gatewaypre v-else-if="form.productName && form.productName.toLowerCase().includes('网关卡预配置')" ref="gatewaypre"
:device="form" @statusEvent="getDeviceStatusData($event)" />
<running-status v-else ref="runningStatus" :device="form" <running-status v-else ref="runningStatus" :device="form"
@statusEvent="getDeviceStatusData($event)" /> @statusEvent="getDeviceStatusData($event)" />
@ -405,7 +407,7 @@
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button class="btns" type="primary" @click="doCopy(2)">{{ $t('device.device-edit.148398-59') <el-button class="btns" type="primary" @click="doCopy(2)">{{ $t('device.device-edit.148398-59')
}}</el-button> }}</el-button>
<el-button @click="closeSummaryDialog">{{ $t('device.device-edit.148398-57') }}</el-button> <el-button @click="closeSummaryDialog">{{ $t('device.device-edit.148398-57') }}</el-button>
</div> </div>
</el-dialog> </el-dialog>
@ -450,6 +452,7 @@ import { clientOut } from '@/api/iot/netty';
import defaultSettings from '@/settings'; import defaultSettings from '@/settings';
import gatewayRunningStatus from './gatewayrunning-status.vue'; import gatewayRunningStatus from './gatewayrunning-status.vue';
import relay from './relay.vue' import relay from './relay.vue'
import gatewaypre from './gatewaypre.vue'
export default { export default {
name: 'DeviceEdit', name: 'DeviceEdit',
dicts: ['iot_device_status', 'iot_location_way'], dicts: ['iot_device_status', 'iot_location_way'],
@ -482,7 +485,8 @@ export default {
deviceModbusTask, deviceModbusTask,
deviceInlineVideo, deviceInlineVideo,
gatewayRunningStatus, gatewayRunningStatus,
relay relay,
gatewaypre
}, },
watch: { watch: {
activeName(val) { activeName(val) {

View File

@ -0,0 +1,516 @@
<template>
<div class="professional-control-panel">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="status-item">
<span class="status-label">当前产品</span>
<el-tag type="info">{{ selectedProductName }}</el-tag>
</div>
<div class="status-item">
<span class="status-label">设备状态</span>
<el-tag :type="statusTagType">{{ statusText }}</el-tag>
</div>
<div class="status-item">
<span class="status-label">信号强度</span>
<el-rate
v-model="signalStrength"
disabled
:colors="['#99A9BF', '#F7BA2A', '#FF9900']"
:max="3"
/>
</div>
</div>
<!-- 主控制区 -->
<div class="main-control-area">
<div class="control-section">
<h3 class="section-title">
<i class="el-icon-s-platform"></i> 产品选择
</h3>
<el-select
v-model="selectedProduct"
placeholder="请选择产品"
class="enhanced-select"
size="medium"
filterable
@focus="loadProductList"
>
<el-option
v-for="product in productModels"
:key="product.id"
:label="`${product.name} (${product.id})`"
:value="product.id"
/>
</el-select>
</div>
<div class="control-section">
<h3 class="section-title">
<i class="el-icon-menu"></i> 功能选择
</h3>
<div class="page-control-grid">
<div
v-for="(page, index) in pageList"
:key="index"
class="page-item"
:class="{ 'active': selectedPages[index] }"
@click="togglePage(index)"
>
<div class="page-icon">
<i :class="page.icon"></i>
</div>
<div class="page-name">{{ page.name }}</div>
<div class="page-checkmark">
<i class="el-icon-check" v-if="selectedPages[index]"></i>
</div>
</div>
</div>
</div>
<!-- 调试信息可选 -->
<div class="control-section" v-if="showDebugInfo">
<h3 class="section-title">
<i class="el-icon-document"></i> 控制编码
</h3>
<div class="binary-preview">
<div class="binary-code">
<span v-for="(bit, index) in binaryCode" :key="index" class="bit">
{{ bit }}
</span>
</div>
<div class="product-code">/{{ selectedProduct }}</div>
</div>
<div class="full-command">
完整指令: <strong>{{ fullCommand }}</strong>
</div>
</div>
</div>
<!-- 操作按钮区 -->
<div class="action-buttons">
<el-button
type="primary"
icon="el-icon-s-promotion"
@click="sendControlCommand"
class="action-button"
:loading="sending"
:disabled="!selectedProduct || !deviceInfo.deviceId"
>
发送指令
</el-button>
<el-button
type="info"
icon="el-icon-refresh"
@click="resetSettings"
class="action-button"
>
重置选择
</el-button>
</div>
</div>
</template>
<script>
import { listShortProduct } from '@/api/iot/product';
import { getOrderControl } from '@/api/iot/control';
import { serviceInvoke, serviceInvokeReply } from '@/api/iot/runstatus';
export default {
name: 'ProfessionalControlPanel',
props: {
device: {
type: Object,
default: () => ({
deviceId: null,
productId: null,
serialNumber: null,
status: null,
isShadow: null,
protocolCode: null,
thingsModels: []
})
},
},
data() {
return {
selectedProduct: '',
selectedPages: [0, 0, 0, 0, 0, 0, 0, 0],
sending: false,
signalStrength: 2,
productModels: [],
pageList: [
{ name: '显示设置', icon: 'el-icon-data-line' },
{ name: '声卡设置', icon: 'el-icon-headset' },
{ name: '车牌识别', icon: 'el-icon-camera' },
{ name: '预警设置', icon: 'el-icon-warning' },
{ name: '远程喊话', icon: 'el-icon-microphone' },
{ name: '网关设置', icon: 'el-icon-connection' },
{ name: '路锥控制', icon: 'el-icon-place' },
{ name: '翻转屏', icon: 'el-icon-monitor' }
],
loadingProducts: false,
deviceInfo: {
deviceId: null,
productId: null,
serialNumber: null,
status: null,
isShadow: null,
protocolCode: null,
thingsModels: []
},
showDebugInfo: false // true
};
},
computed: {
statusTagType() {
switch (this.deviceInfo?.status) {
case 3: return 'success'; // 线
case 1: return 'warning'; // 线
case 2: return 'danger'; //
default: return 'info'; //
}
},
statusText() {
switch (this.deviceInfo?.status) {
case 3: return '在线';
case 1: return '离线';
case 2: return '异常';
default: return '未知';
}
},
binaryCode() {
return this.selectedPages.map(bit => bit ? '1' : '0').join('');
},
fullCommand() {
return `${this.binaryCode}/${this.selectedProduct}`;
},
selectedProductName() {
if (!this.selectedProduct) return '未选择';
const product = this.productModels.find(p => p.id === this.selectedProduct);
return product ? `${product.name} (${product.id})` : this.selectedProduct;
}
},
watch: {
device: {
handler(newVal) {
if (newVal && newVal.deviceId) {
this.deviceInfo = {
...newVal,
thingsModels: newVal.thingsModels || [],
//
deviceId: newVal.deviceId,
productId: newVal.productId,
serialNumber: newVal.serialNumber,
status: newVal.status,
isShadow: newVal.isShadow,
protocolCode: newVal.protocolCode
};
// distributeshadow
const distributeModel = this.deviceInfo.thingsModels.find(
model => model.id === 'distribute'
);
if (distributeModel) {
distributeModel.shadow = this.fullCommand;
}
}
},
immediate: true,
deep: true
}
},
created() {
this.loadProductList();
},
methods: {
async loadProductList() {
if (this.productModels.length > 0 || this.loadingProducts) return;
this.loadingProducts = true;
try {
const params = {
pageSize: 999,
showSenior: true
};
const res = await listShortProduct(params);
if (res.code === 200) {
this.productModels = res.data || [];
}
} catch (error) {
console.error('获取产品列表失败:', error);
this.$message.error('获取产品列表失败');
} finally {
this.loadingProducts = false;
}
},
togglePage(index) {
this.$set(this.selectedPages, index, this.selectedPages[index] ? 0 : 1);
// distributeshadow
this.updateDistributeModel();
},
updateDistributeModel() {
const distributeModel = this.deviceInfo.thingsModels.find(
model => model.id === 'distribute'
);
if (distributeModel) {
distributeModel.shadow = this.fullCommand;
}
},
async sendControlCommand() {
if (!this.selectedProduct) {
this.$message.warning('请选择产品');
return;
}
if (!this.deviceInfo.deviceId) {
this.$message.warning('设备信息未初始化');
return;
}
this.sending = true;
try {
const distributeModel = this.deviceInfo.thingsModels.find(
model => model.id === 'productpram'
);
if (!distributeModel) {
throw new Error('未找到分发控制模型');
}
// shadow
distributeModel.shadow = this.fullCommand;
await this.mqttPublish(this.deviceInfo, distributeModel);
this.$message.success('控制指令发送成功');
} catch (error) {
console.error('发送控制指令失败:', error);
this.$message.error(`控制指令发送失败: ${error.message}`);
} finally {
this.sending = false;
}
},
async mqttPublish(device, model) {
try {
if (!device || !device.deviceId) {
throw new Error('无效的设备信息');
}
if (!model || !model.id) {
throw new Error('无效的模型信息');
}
const command = {};
command[model.id] = model.shadow;
const params = {
deviceId: device.deviceId,
modelId: model.modelId,
};
const response = await getOrderControl(params);
if (response.code != 200) {
throw new Error(response.msg || '获取控制指令失败');
}
const data = {
serialNumber: device.serialNumber,
productId: device.productId,
remoteCommand: command,
identifier: model.id,
modelName: model.name,
isShadow: device.status != 3,
type: model.type,
};
if (device.status !== 3 && device.isShadow !== 1) {
throw new Error('设备不在线且未启用影子模式');
}
if ((device.protocolCode === 'MODBUS-TCP' || device.protocolCode === 'MODBUS-RTU') && device.status === 3) {
await serviceInvokeReply(data);
} else {
await serviceInvoke(data);
}
} catch (error) {
console.error('MQTT发布失败:', error);
throw error;
}
},
resetSettings() {
this.selectedPages = [0, 0, 0, 0, 0, 0, 0, 0];
this.updateDistributeModel();
this.$message.info('已重置所有页面选择');
}
}
}
</script>
<style scoped>
.professional-control-panel {
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background-color: #f5f7fa;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.status-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.status-item {
display: flex;
align-items: center;
}
.status-label {
font-weight: bold;
margin-right: 10px;
color: #606266;
}
.main-control-area {
background-color: #fff;
padding: 25px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.control-section {
margin-bottom: 25px;
}
.section-title {
color: #409EFF;
border-bottom: 1px solid #ebeef5;
padding-bottom: 10px;
margin-bottom: 15px;
font-size: 16px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 8px;
}
.enhanced-select {
width: 100%;
}
.page-control-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
.page-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 10px;
border-radius: 8px;
background-color: #f8f9fa;
cursor: pointer;
transition: all 0.3s;
border: 1px solid #ebeef5;
}
.page-item:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.page-item.active {
background-color: #ecf5ff;
border-color: #d9ecff;
}
.page-icon {
font-size: 24px;
margin-bottom: 10px;
color: #909399;
}
.page-name {
font-size: 14px;
margin-bottom: 8px;
text-align: center;
}
.page-checkmark {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 50%;
background-color: #f1f1f1;
color: #67c23a;
}
.page-item.active .page-checkmark {
background-color: #f0f9eb;
}
.binary-preview {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
padding: 15px;
background-color: #f1f1f1;
border-radius: 8px;
font-family: monospace;
}
.binary-code {
font-size: 24px;
letter-spacing: 2px;
}
.bit {
display: inline-block;
width: 24px;
text-align: center;
}
.product-code {
font-size: 24px;
margin-left: 10px;
color: #409EFF;
}
.full-command {
text-align: center;
font-size: 16px;
padding: 10px;
background-color: #f8f8f8;
border-radius: 4px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
}
.action-button {
padding: 12px 30px;
font-size: 16px;
}
</style>

View File

@ -21,10 +21,13 @@
<el-button type="primary" size="mini" :plain="true" @click="viewVersion()">{{ <el-button type="primary" size="mini" :plain="true" @click="viewVersion()">{{
$t('device.running-status.866086-44') $t('device.running-status.866086-44')
}}</el-button> }}</el-button>
<el-button type="primary" size="mini" :plain="true" @click="sendDataToIframe()">{{ <el-button type="primary" size="mini" :plain="true" @click="sendDataToIframe()">{{
$t('device.running-status.866086-44') $t('device.running-status.866086-44')
}}</el-button> }}</el-button>
<el-button type="primary" size="mini" :plain="true" @click="sendProductParamOnce()">{{
$t('device.running-status.866086-44')
}}</el-button>
</el-descriptions-item> </el-descriptions-item>
<!-- 设备物模型--> <!-- 设备物模型-->
@ -428,17 +431,17 @@
</el-col> </el-col>
</el-row> </el-row>
</el-col> </el-col>
<!-- <iframe ref="childFrame" src="https://www.jayjiajun.cn/" width="23%" height="800px"
<iframe ref="childFrame" src="https://iot-xcwl.cn/h5/gateway/index.html#/" width="23%" height="800px"
style="border: none;"></iframe>
<!-- <iframe ref="childFrame" src="http://localhost:81/" width="23%" height="800px"
style="border: none;"></iframe> --> style="border: none;"></iframe> -->
<iframe ref="childFrame" src="http://localhost:81/" width="23%" height="800px"
style="border: none;"></iframe>
</el-row> </el-row>
@ -484,17 +487,17 @@
$t('device.running-status.866086-12') }}</el-link> $t('device.running-status.866086-12') }}</el-link>
</template> </template>
<el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName <el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName <el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version <el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-16')"> <el-descriptions-item :label="$t('device.running-status.866086-16')">
<el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{ <el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{
getDownloadUrl(firmware.filePath) }}</el-link> getDownloadUrl(firmware.filePath) }}</el-link>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark <el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark
}}</el-descriptions-item> }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="success" @click="otaUpgrade" <el-button type="success" @click="otaUpgrade"
@ -621,27 +624,83 @@ export default {
this.initData(); this.initData();
} }
// this.mqttCallback(); // this.mqttCallback();
// mountedproductpram
this.$nextTick(() => {
this.waitForIframeLoad();
});
// //
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
// //
if (event.origin === 'http://localhost:81') {
console.log('收到来自子页面的消息:', event.data);
// id distribute
const distributeModel = this.deviceInfo.thingsModels.find(model => model.id === 'distribute'); // id distribute
if (distributeModel) { const distributeModel = this.deviceInfo.thingsModels.find(model => model.id === 'distribute');
// shadow 使 if (distributeModel) {
distributeModel.shadow = event.data.data; // shadow 使
// mqttPublish distributeModel.shadow = JSON.stringify(event.data.data);
this.mqttPublish(this.deviceInfo, distributeModel); console.log('收到来自子页面的消息:', JSON.stringify(event.data.data));
}
// mqttPublish
this.mqttPublish(this.deviceInfo, distributeModel);
} }
}); });
}, },
methods: { methods: {
// iframe
waitForIframeLoad() {
const maxAttempts = 5; //
let attempts = 0;
const checkInterval = setInterval(() => {
const iframe = this.$refs.childFrame;
// 1. iframe
if (iframe && iframe.contentWindow) {
// 2.
// iframe.contentWindow.postMessage({ type: 'ping' }, '*');
clearInterval(checkInterval);
this.sendProductParamOnce();
return;
}
// 3.
if (++attempts >= maxAttempts) {
clearInterval(checkInterval);
console.warn('Iframe not ready after multiple attempts');
return;
}
}, 500); // 500ms
},
// productpram
sendProductParamOnce() {
const iframe = this.$refs.childFrame;
if (!iframe || !iframe.contentWindow) {
console.error('Iframe not ready');
return;
}
const productParamModel = this.deviceInfo.thingsModels.find(model => model.id === 'productpram');
if (!productParamModel) {
console.warn('productpram model not found');
return;
}
let paramData = productParamModel.shadow;
if (typeof paramData === 'string' && paramData.startsWith('JSON=')) {
paramData = paramData.substring(5);
}
const paramDataString = typeof paramData === 'string' ? paramData : JSON.stringify(paramData);
iframe.contentWindow.postMessage(
paramDataString,
'https://iot-xcwl.cn/h5/gateway/index.html#/'
);
console.log('Initial productpram data sent:', paramDataString);
},
// //
handleDeviceChange(device) { handleDeviceChange(device) {
@ -841,6 +900,7 @@ export default {
isShadow: device.status != 3, isShadow: device.status != 3,
type: model.type, type: model.type,
}; };
// console.log(':', JSON.stringify(data));
//线 //线
if (this.device.status !== 3 && this.device.isShadow !== 1) { if (this.device.status !== 3 && this.device.isShadow !== 1) {
if (this.device.status === 1) { if (this.device.status === 1) {
@ -1111,22 +1171,27 @@ export default {
option && this.monitorChart[i].chart.setOption(option); option && this.monitorChart[i].chart.setOption(option);
} }
}, },
// iframe // productpram
sendDataToIframe() { sendDataToIframe() {
const iframe = this.$refs.childFrame; const iframe = this.$refs.childFrame;
// console.log("thingsmodel",JSON.stringify(this.deviceInfo.thingsModels)); if (iframe && iframe.contentWindow) {
if (iframe && iframe.contentWindow) { // upload
// id upload const uploadModel = this.deviceInfo.thingsModels.find(model => model.id === 'upload');
const uploadModel = this.deviceInfo.thingsModels.find(model => model.id === 'upload'); if (uploadModel) {
const dataToSend = uploadModel ? { value: uploadModel.shadow } : null; let shadowData = uploadModel.shadow;
if (typeof shadowData === 'string' && shadowData.startsWith('JSON=')) {
shadowData = shadowData.substring(5);
}
const uploadDataString = typeof shadowData === 'string' ? shadowData : JSON.stringify(shadowData);
// 便 iframe.contentWindow.postMessage(
console.log('Sending data to iframe:', JSON.stringify(dataToSend)); uploadDataString,
'https://iot-xcwl.cn/h5/gateway/index.html#/'
// iframe );
iframe.contentWindow.postMessage(dataToSend, 'http://localhost:81/#/'); console.log('Sent upload data:', uploadDataString);
} }
}, }
}
}, },
}; };
</script> </script>