You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2477 lines
77 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" min-label-width="68px">
<el-form-item :label="t('DataCollection.Device.deviceCode')" prop="deviceCode">
<el-input
v-model="queryParams.deviceCode"
:placeholder="t('DataCollection.Device.placeholderDeviceCode')"
clearable
@keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('DataCollection.Device.deviceName')" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
:placeholder="t('DataCollection.Device.placeholderDeviceName')"
clearable
@keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<!-- <el-form-item label="连接状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_GATEWAY_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item> -->
<!-- <el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.Device.search') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.Device.reset') }}
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['iot:device:create']">
<Icon icon="ep:plus" class="mr-5px" /> {{ t('DataCollection.Device.create') }}
</el-button>
<el-button
type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['iot:device:export']">
<Icon icon="ep:download" class="mr-5px" /> {{ t('DataCollection.Device.export') }}
</el-button>
<el-button type="danger" plain @click="handleBatchDelete" v-hasPermi="['iot:device:delete']">
<Icon icon="ep:delete" class="mr-5px" /> {{ t('DataCollection.Device.batchDelete') }}
</el-button>
<!-- 视图切换按钮 -->
<el-button
:type="currentView === 'grid' ? 'primary' : 'default'"
:icon="currentView === 'grid' ? Menu : Grid"
@click="toggleView"
class="view-toggle-btn"
>
{{ currentView === 'grid' ? t('DataCollection.Device.gridView') : t('DataCollection.Device.sudoku') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 GridView Sudoku-->
<ContentWrap>
<div v-show="currentView === 'table'" class="simple-table-view">
<el-table
ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"
@selection-change="handleSelectionChange" @row-click="handleShowAttribute">
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column :label="t('DataCollection.Device.deviceCode')" align="left" prop="deviceCode" sortable/>
<el-table-column :label="t('DataCollection.Device.deviceName')" align="left" prop="deviceName" sortable/>
<el-table-column :label="t('DataCollection.Device.operatingStatus')" align="center" prop="operatingStatus" width="120px" sortable>
<template #default="scope">
<el-tag
size="small"
:type="getOperatingStatusType(scope.row.operatingStatus)"
>
{{ getOperatingStatusLabel(scope.row.operatingStatus) }}
</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="设备类型" align="left" prop="deviceType" width="150px" sortable> -->
<!-- <template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column> -->
<el-table-column :label="t('DataCollection.Device.protocol')" align="left" prop="protocol" width="120px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL" :value="scope.row.protocol" />
</template>
</el-table-column>
<!-- <el-table-column :label="t('DataCollection.Device.status')" align="center" prop="status" width="120px" sortable>
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_GATEWAY_STATUS" :value="scope.row.status" />
</template>
</el-table-column> -->
<!-- <el-table-column label="读主题" align="center" prop="readTopic" />
<el-table-column label="写主题" align="center" prop="writeTopic" />
<el-table-column label="网关id" align="center" prop="gatewayId" /> -->
<!-- <el-table-column label="设备品牌id" align="center" prop="deviceBrandId" />-->
<!-- <el-table-column label="离线间隔" align="center" prop="offLineDuration" /> -->
<!-- <el-table-column
label="最后上线时间"
align="center"
prop="lastOnlineTime"
:formatter="dateFormatter"
width="180px"
sortable /> -->
<!-- <el-table-column label="备注" align="center" prop="remark" /> -->
<!-- <el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="170px"
sortable /> -->
<el-table-column :label="t('DataCollection.Device.isEnable')" align="center" prop="isEnable" width="120px">
<template #default="scope">
<el-switch
:model-value="isDeviceEnabled(scope.row)"
@change="(val) => handleDeviceEnableChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column
:label="t('DataCollection.Device.collectionTime')"
align="center"
prop="collectionTime"
width="180px"
:formatter="dateFormatter"
sortable
/>
<el-table-column :label="t('DataCollection.Device.operate')" align="center" fixed="right" width="380px">
<template #default="scope">
<el-button link type="primary" @click.stop="handleShowAttribute(scope.row)">{{ t('DataCollection.Device.attributeModuleName') }}</el-button>
<el-button
link
type="primary"
@click.stop="openForm('setting', scope.row.id)"
v-hasPermi="['iot:device:update']"
>
{{ t('DataCollection.Device.settingDialogTitle') }}
</el-button>
<el-button
link
type="primary"
@click.stop="handleCopy(scope.row.id)"
v-hasPermi="['iot:device:create']"
>
{{ t('action.copy') }}
</el-button>
<el-button
link
type="primary"
@click.stop="handleEdit(scope.row)"
v-hasPermi="['iot:device:update']"
>
{{ t('action.edit') }}
</el-button>
<el-button
link
type="danger"
@click.stop="handleDelete(scope.row.id)"
v-hasPermi="['iot:device:delete']"
>
{{ t('action.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</div>
<!-- 九宫格视图 -->
<div v-show="currentView === 'grid'" class="simple-grid-view">
<div v-if="!list || list.length === 0" class="empty-grid">
<el-empty description="暂无设备数据"/>
</div>
<div v-else class="grid-container">
<div
v-for="(item, index) in list"
:key="item.id || index"
class="grid-card"
@click="openDetailForm(item?.id,item?.deviceName)"
>
<!-- 设备状态指示 status-${item.deviceStatus} -->
<div class="status-indicator" :class="getStatusText(item.operatingStatus)"></div>
<!-- 设备基本信息 -->
<div class="device-card">
<div class="header">
<div class="device-icon" @click.stop>
<!-- <i>📊</i>-->
<el-image
style="width: 60px; height: 60px"
:src="item.images || 'https://p119-minio-upload.ngsk.tech:7001/besure/998a6fc5d7da4ad079cc5afaa1ca5293917c17080c9805dc791039d3534e9dba.jpeg'"
:preview-src-list="[
item.images || 'https://p119-minio-upload.ngsk.tech:7001/besure/998a6fc5d7da4ad079cc5afaa1ca5293917c17080c9805dc791039d3534e9dba.jpeg'
]"
fit="cover"
preview-teleported
/> <!-- <img src="@/assets/images/device.png" alt="Device Icon" />-->
</div>
<div class="device-info">
<div class="device-name">{{ item.deviceName }}</div>
<div class="device-id">{{ item.deviceCode }}</div>
</div>
<div class="status-badge" :class="getStatusText(item.operatingStatus)">
<span class="status-dot" :class="getStatusText(item.operatingStatus)"></span>
{{ item.operatingStatus }}
</div>
</div>
<div class="content">
<div class="info-row">
<div class="info-label">
<i>📡</i>
采集协议
</div>
<div class="info-value">
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL" :value="item.protocol" />
</div>
</div>
<div class="info-row">
<div class="info-label">
<i>⏱️</i>
采集时间
</div>
<div class="info-value">
{{ item.collectionTime }}
</div>
</div>
<div class="toggle-container" @click.stop>
<div class="toggle-label">
<i>🔘️</i>
是否启用
</div>
<el-switch
:model-value="isDeviceEnabled(item)"
@change="(val) => handleDeviceEnableChange(item, val)"
/>
<!-- <label class="toggle-switch">
<input type="checkbox" v-model="item.operatingStatus" @change.stop="enabledChange(item,item?.id)" />
<span class="slider"></span>
</label>-->
</div>
</div>
<div class="footer" @click.stop>
<div class="action-icons">
<el-button class="action-btn btn-primary" @click.stop="openDetailForm(item?.id,item?.deviceName)" >
<i>📍</i>
{{ t('DataCollection.Device.attributeModuleName') }}
</el-button>
<el-button class="action-btn btn-success" @click.stop="openForm('setting', item?.id)" v-hasPermi="['iot:device:update']">
<i>⚙️</i>
{{ t('DataCollection.Device.settingDialogTitle') }}
</el-button>
<el-tooltip
:content="t('action.copy')"
placement="top"
effect="dark"
>
<el-button
class="icon-btn"
link
type="primary"
@click="handleCopy(item?.id)"
v-hasPermi="['iot:device:create']"
>
📋
</el-button>
</el-tooltip>
<el-tooltip
:content="t('action.edit')"
placement="top"
effect="dark"
>
<el-button
link
type="primary"
@click.stop="handleEdit(item)"
v-hasPermi="['iot:device:update']"
>
✏️
</el-button>
</el-tooltip>
<el-tooltip
:content="t('DataCollection.Device.gridView')"
placement="top"
effect="dark"
>
<el-button class="icon-btn" @click.stop="changeTable">🏠</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList"/>
</div>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<DeviceForm ref="formRef" @success="getListOne" />
<!-- 子表的列表 -->
<!-- <ContentWrap v-if="ifShow">
<template v-if="attributeDeviceId">
<div class="mb-10px flex items-center justify-between text-sm text-gray-500">
<div>
{{ t('DataCollection.Device.currentDeviceLabel') }}<span class="font-medium text-gray-700">{{ attributeDeviceName || '-' }}</span>
</div>
<div>
<el-button type="primary" link @click="handleShowDeviceAlarmHistory">
{{ t('DataCollection.Device.alarmHistoryTitle') }}
</el-button>
</div>
</div>
<el-tabs v-model="deviceTabActive">
<el-tab-pane :label="deviceAttributeTabLabel" name="deviceAttribute">
<DeviceAttributeList :device-id="attributeDeviceId" />
</el-tab-pane>
<el-tab-pane :label="deviceRuleTabLabel" name="deviceRule">
<el-form
class="-mb-15px"
:model="ruleQueryParams"
ref="ruleQueryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item :label="t('DataCollection.DeviceModel.ruleIdentifier')" prop="identifier">
<el-input
v-model="ruleQueryParams.identifier"
:placeholder="t('DataCollection.DeviceModel.ruleSearchIdentifierPlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleFieldName')" prop="fieldName">
<el-input
v-model="ruleQueryParams.fieldName"
:placeholder="t('DataCollection.DeviceModel.ruleSearchFieldNamePlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDefaultValue')" prop="defaultValue">
<el-input
v-model="ruleQueryParams.defaultValue"
:placeholder="t('DataCollection.DeviceModel.ruleSearchDefaultValuePlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleRuleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.DeviceModel.ruleSearch') }}
</el-button>
<el-button @click="resetRuleQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.DeviceModel.ruleReset') }}
</el-button>
</el-form-item>
</el-form>
<div class="mb-10px flex justify-end gap-8px">
<el-button type="primary" plain @click="openCreateCountRuleForm">
新增产量
</el-button>
<el-button type="primary" @click="openCreateRuleForm">
{{ t('DataCollection.DeviceModel.ruleCreateButton') }}
</el-button>
</div>
<el-table
v-loading="ruleLoading"
:data="ruleList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column :label="t('DataCollection.DeviceModel.ruleIdentifier')" align="center" prop="identifier" />
<el-table-column :label="t('DataCollection.DeviceModel.ruleFieldName')" align="center" prop="fieldName" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.ruleFieldRule')" align="center" prop="fieldRule" />
<el-table-column :label="t('DataCollection.DeviceModel.ruleDefaultValue')" align="center" prop="defaultValue" />
<el-table-column
:label="t('DataCollection.DeviceModel.ruleCreateTime')"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable
/>
<el-table-column :label="t('DataCollection.DeviceModel.ruleOperate')" align="center" width="160px">
<template #default="scope">
<el-button link type="primary" @click="openRuleForm(scope.row)">
{{ t('DataCollection.DeviceModel.ruleEditRuleButton') }}
</el-button>
<el-button
v-if="scope.row.identifier === 'ALARM' || scope.row.identifier === 'COUNT'"
link
type="danger"
@click="handleRuleDelete(scope.row.id)"
>
{{ t('DataCollection.DeviceModel.ruleDeleteRuleButton') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="ruleTotal"
v-model:page="ruleQueryParams.pageNo"
v-model:limit="ruleQueryParams.pageSize"
@pagination="getRuleList"
/>
<el-dialog v-model="ruleDialogVisible" :title="t('DataCollection.DeviceModel.ruleDialogTitle')" width="880px" draggable>
<el-form :model="ruleForm" ref="ruleFormRef" label-width="120px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogIdentifier')">
<el-input v-model="ruleForm.identifier" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldName')">
<el-input v-model="ruleForm.fieldName" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogDefaultValue')">
<el-input v-model="ruleForm.defaultValue" disabled />
</el-form-item>
<el-form-item
v-if="(ruleForm.identifier || '').toString().toUpperCase() === 'ALARM'"
:label="t('DataCollection.DeviceModel.ruleDialogAlarmLevel')"
>
<el-select
v-model="ruleForm.alarmLevel"
:placeholder="t('DataCollection.DeviceModel.ruleDialogAlarmLevelPlaceholder')"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_ALARM_REGISTRATION)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<div class="flex flex-col w-full">
<div class="border border-gray-200 dark:border-gray-600 rounded-md py-12px">
<el-form-item
v-if="!isCountIdentifier"
:label="t('DataCollection.DeviceModel.ruleDialogFieldRule')"
>
<el-select
v-model="ruleForm.fieldRule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogFieldRulePlaceholder')"
class="!w-240px"
:disabled="!currentRuleOptions.length"
>
<el-option
v-for="item in currentRuleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="isRuleDisabled(item.value, ruleForm.fieldRule)"
/>
</el-select>
</el-form-item>
<el-form-item :label="isCountIdentifier ? '四则运算规则' : t('DataCollection.DeviceModel.ruleDialogRule')">
<div v-if="isCountIdentifier" class="w-full flex flex-col gap-8px">
<div
v-for="(item, index) in countArithmeticRules"
:key="index"
class="flex items-center gap-8px"
>
<el-select
v-model="item.ruleAttributeId"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleAttributePlaceholder')"
class="!w-240px"
clearable
>
<el-option
v-for="attr in ruleAttributeOptions"
:key="attr.id"
:label="attr.attributeName || attr.attributeCode"
:value="attr.id"
/>
</el-select>
<el-select
v-model="item.ruleOperator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-160px"
clearable
>
<el-option
v-for="operator in currentRuleOperatorOptions"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
<el-input
v-model="item.ruleValue"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleValuePlaceholder')"
class="!w-200px"
clearable
/>
<template v-if="index < countArithmeticRules.length - 1">
<el-select
v-model="countArithmeticRules[index].bigOperator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-110px"
clearable
>
<el-option
v-for="operator in countOperatorOptions"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</template>
<template v-else>
<div class="flex items-center gap-4px">
<el-button link type="primary" @click="handleAddCountArithmeticRule">
<Icon icon="ep:plus" />
</el-button>
<el-button
link
type="danger"
:disabled="countArithmeticRules.length <= 1"
@click="handleRemoveCountArithmeticRule(index)"
>
<Icon icon="ep:minus" />
</el-button>
</div>
</template>
</div>
<div class="text-12px text-[#909399] leading-20px">按“四则运算规则”添加顺序从上到下依次计算,不按乘除优先。</div>
</div>
<div v-else class="flex items-center gap-8px">
<el-select
v-model="ruleForm.ruleAttributeId"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleAttributePlaceholder')"
class="!w-240px"
clearable
>
<el-option
v-for="item in ruleAttributeOptions"
:key="item.id"
:label="item.attributeName || item.attributeCode"
:value="item.id"
/>
</el-select>
<el-select
v-model="ruleForm.ruleOperator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-160px"
clearable
>
<el-option
v-for="item in currentRuleOperatorOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-if="ruleForm.ruleOperator !== 'TRUE' && ruleForm.ruleOperator !== 'FALSE'"
v-model="ruleForm.ruleValue"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleValuePlaceholder')"
class="!w-200px"
clearable
/>
</div>
</el-form-item>
</div>
<template v-if="isRunningIdentifier">
<div
v-for="(item, index) in extraPointRules"
:key="index"
class="border border-gray-200 dark:border-gray-600 rounded-md px-16px py-12px"
>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldRule')">
<el-select
v-model="item.rule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogFieldRulePlaceholder')"
class="!w-240px"
>
<el-option
v-for="opt in currentRuleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
:disabled="isRuleDisabled(opt.value, item.rule)"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogRule')">
<div class="flex items-center gap-8px">
<el-select
v-model="item.id"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleAttributePlaceholder')"
class="!w-240px"
>
<el-option
v-for="attr in ruleAttributeOptions"
:key="attr.id"
:label="attr.attributeName || attr.attributeCode"
:value="attr.id"
/>
</el-select>
<el-select
v-model="item.operator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-160px"
>
<el-option
v-for="opt in ruleOperatorOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-if="isCountIdentifier || (item.operator !== 'TRUE' && item.operator !== 'FALSE')"
v-model="item.operatorRule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleValuePlaceholder')"
class="!w-200px"
/>
<el-button type="danger" link @click="handleRemovePointRule(index)">
{{ t('DataCollection.DeviceModel.ruleDeleteRuleButton') }}
</el-button>
</div>
</el-form-item>
</div>
</template>
<el-form-item v-if="isRunningIdentifier" label=" ">
<el-button type="primary" link @click="handleAddPointRule">
+ {{ t('DataCollection.DeviceModel.ruleCreateButton') }}
</el-button>
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="ruleDialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
<el-button type="primary" :loading="ruleFormLoading" @click="handleRuleSubmit">{{ t('DataCollection.DeviceModel.dialogOk') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="createRuleDialogVisible" :title="createRuleDialogTitle" width="520px" draggable>
<el-form :model="createRuleForm" ref="createRuleFormRef" label-width="120px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogIdentifier')">
<el-input v-model="createRuleForm.identifier" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldName')">
<el-input
v-model="createRuleForm.fieldName"
:placeholder="t('DataCollection.DeviceModel.ruleSearchFieldNamePlaceholder')"
:disabled="isCreateCountRule"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogDefaultValue')">
<el-input v-model="createRuleForm.defaultValue" disabled />
</el-form-item>
<el-form-item v-if="!isCreateCountRule" :label="t('DataCollection.DeviceModel.ruleDialogAlarmLevel')">
<el-select
v-model="createRuleForm.alarmLevel"
:placeholder="t('DataCollection.DeviceModel.ruleDialogAlarmLevelPlaceholder')"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_ALARM_REGISTRATION)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createRuleDialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
<el-button type="primary" :loading="createRuleFormLoading" @click="handleCreateRuleSubmit">
{{ t('DataCollection.DeviceModel.dialogOk') }}
</el-button>
</span>
</template>
</el-dialog>
</el-tab-pane>
</el-tabs>
</template>
<el-empty v-else :description="t('DataCollection.Device.emptyDescription')" />
</ContentWrap>-->
<Dialog :title="t('DataCollection.Device.alarmHistoryTitle')" v-model="deviceAlarmDialogVisible" width="1200px">
<el-form
class="-mb-15px"
:model="deviceAlarmQueryParams"
ref="deviceAlarmQueryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item :label="t('DataCollection.Device.alarmRuleName')" prop="ruleId">
<el-select
v-model="deviceAlarmQueryParams.ruleId"
:loading="deviceAlarmRuleLoading"
clearable
filterable
class="!w-220px"
:placeholder="t('DataCollection.DeviceModel.ruleSearchFieldNamePlaceholder')"
>
<el-option
v-for="item in deviceAlarmRuleOptions"
:key="item.id"
:label="item.fieldName || item.ruleName || item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.alarmPointName')" prop="modelId">
<el-select
v-model="deviceAlarmQueryParams.modelId"
:loading="deviceAlarmPointLoading"
clearable
filterable
class="!w-220px"
:placeholder="t('DataCollection.Device.placeholderAttributeName')"
>
<el-option
v-for="item in deviceAlarmPointOptions"
:key="item.id"
:label="item.attributeName || item.attributeCode || item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.alarmTime')" prop="createTime">
<el-date-picker
v-model="deviceAlarmQueryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:start-placeholder="t('DataCollection.HistoryData.dialogCollectionTimeStartPlaceholder')"
:end-placeholder="t('DataCollection.HistoryData.dialogCollectionTimeEndPlaceholder')"
class="!w-260px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleDeviceAlarmQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('DataCollection.Device.search') }}
</el-button>
<el-button @click="resetDeviceAlarmQuery">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('DataCollection.Device.reset') }}
</el-button>
</el-form-item>
</el-form>
<el-table
:data="deviceAlarmList"
v-loading="deviceAlarmLoading"
:stripe="true"
:show-overflow-tooltip="true"
:max-height="700"
class="mt-10px"
>
<el-table-column :label="t('DataCollection.Device.alarmRuleName')" align="center" prop="ruleName" sortable />
<el-table-column :label="t('DataCollection.Device.alarmPointName')" align="center" prop="modelName" sortable />
<el-table-column :label="t('DataCollection.Device.alarmPointValue')" align="center" prop="addressValue" />
<el-table-column :label="t('DataCollection.Device.alarmLevel')" align="center" prop="alarmLevel">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_ALARM_REGISTRATION" :value="scope.row.alarmLevel" />
</template>
</el-table-column>
<el-table-column
:label="t('DataCollection.Device.alarmTime')"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable />
</el-table>
<div class="mt-10px pb-10px flex justify-end">
<Pagination
:total="deviceAlarmTotal"
v-model:page="deviceAlarmQueryParams.pageNo"
v-model:limit="deviceAlarmQueryParams.pageSize"
@pagination="getDeviceAlarmList"
/>
</div>
</Dialog>
<!-- 表单弹窗:弹出设备属性 -->
<DetailForm ref="detailFormRef" />
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import request from '@/config/axios'
import { DeviceApi, DeviceConnectParams, DeviceVO } from '@/api/iot/device'
import DeviceForm from './DeviceForm.vue'
import DeviceAttributeList from './components/DeviceAttributeList.vue'
import {
Refresh,
Grid,
Menu,
Search,
Location
} from '@element-plus/icons-vue'
import {useRouter} from "vue-router";
import DetailForm from "@/views/iot/device/detailForm.vue";
import {ProductUnitApi, ProductUnitVO} from "@/api/erp/product/unit";
const ifShow =ref(true)
const currentView = ref('table') // 'table' 'grid'
// script setup
const noImageUrl =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iODAiIGhlaWdodD0iODAiIGZpbGw9IiNFOUVDRUYiLz48dGV4dCB4PSI0MCIgeT0iNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5OTkiIGZvbnQtc2l6ZT0iMTQiPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD48L3N2Zz4='
//
const router = useRouter()
//
const handleView = (row) => {
router.push({
path: '/equipment/detail',
query: {id: row.id}
})
}
/** 设备类型 列表 */
//defineOptions({name: 'DeviceLedger'})
const getEquipmentColor = (type) => {
const colorMap = {
'production': '#409eff',
'inspection': '#67c23a',
'packaging': '#e6a23c',
'transport': '#f56c6c',
'other': '#909399'
}
return colorMap[type] || '#409eff'
}
const getEquipmentIcon = (type) => {
const iconMap = {
'production': 'Monitor',
'inspection': 'Search',
'packaging': 'Box',
'transport': 'Truck',
'other': 'Tools'
}
return iconMap[type] || 'Tools'
}
// 工具函数
const getStatusTag = (status) => {
const tagMap = {
'运行': 'success',//running
'待机中': 'info',//standby
'故障中': 'danger',//fault
'报警中': 'warning',//maintenance
'离线': 'info'//stopped
}
return tagMap[status] || 'info'
}
const getStatusText = (status) => {
const textMap = {
'运行': 'status-running',
'待机中': 'status-standby',
'故障中': 'status-fault',
'报警中': 'status-maintenance',
'离线': 'status-stopped'
}
return textMap[status] || '未知'
}
// 切换视图
const toggleView = () => {
currentView.value = currentView.value === 'table' ? 'grid' : 'table'
ifShow.value=currentView.value === 'table' ? true : false;
// 保存视图偏好
localStorage.setItem('equipment-view', currentView.value)
}
// 切回表格
const changeTable = () => {
currentView.value = 'table'
ifShow.value=true;
// 保存视图偏好
localStorage.setItem('equipment-view', currentView.value)
}
/** 物联设备 列表 */
defineOptions({ name: 'Device' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const tableRef = ref()
const loading = ref(true) // 列表的加载中
const showDetailForm =ref(true)
const list = ref<DeviceVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceCode: undefined,
deviceName: undefined,
deviceType: undefined,
status: undefined,
readTopic: undefined,
writeTopic: undefined,
gatewayId: undefined,
deviceBrandId: undefined,
offLineDuration: undefined,
lastOnlineTime: [],
remark: undefined,
isEnable: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const getOperatingStatusLabel = (value: string | number | undefined) => {
const text = String(value ?? '').trim()
if (!text) return '离线'
return text
}
const getOperatingStatusType = (value: string | number | undefined) => {
const text = String(value ?? '').trim()
if (!text) return 'info'
if (text === '运行') return 'success'
if (text === '待机中') return 'info'
if (text === '故障中') return 'danger'
if (text === '报警中') return 'warning'
return 'info'
}
const isDeviceEnabled = (row: DeviceVO) => {
const value = (row as any)?.isEnable
if (typeof value === 'boolean') return value
const text = String(value ?? '').toLowerCase().trim()
if (!text) return false
if (text === 'true' || text === '1' || text === 'yes') return true
return false
}
const handleDeviceEnableChange = async (row: DeviceVO, value: boolean) => {
if (!row.id) return
const oldValue = (row as any).isEnable
;(row as any).isEnable = value
try {
await DeviceApi.updateDeviceEnabled(row.id, value ? 'true' : 'false')
const name = (row as any).deviceName ?? (row as any).deviceCode ?? ''
const suffix = value ? '已启用' : '已停用'
if (name) {
message.success(`${name}${suffix}`)
} else {
message.success(suffix)
}
} catch {
;(row as any).isEnable = oldValue
message.error(t('common.updateFail'))
}
}
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
/** 查询列表 */
const getList = async (showLoading = true) => {
if (showLoading) {
loading.value = true
}
try {
const data = await DeviceApi.getDevicePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
if (showLoading) {
loading.value = false
}
}
}
/** 查询列表 */
const getListOne = async (showLoading = true) => {
if (showLoading) {
loading.value = true
}
try {
const data = await DeviceApi.getDevicePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
if (showLoading) {
loading.value = false
}
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 设备属性弹出框 */
const detailFormRef = ref()
const openDetailForm = (id?: number,deviceName?:string) => {
attributeDeviceId.value = undefined
attributeDeviceName.value = ''
detailFormRef.value.open(id,deviceName)
//formRef.value.open(id,deviceName)
}
const enabledChange = (row: DeviceVO, value: boolean) => {
showDetailForm.value=false
if (!row.id) return
const oldValue = (row as any).isEnable
;(row as any).isEnable = value
try {
DeviceApi.updateDeviceEnabled(row.id, value ? 'true' : 'false')
const name = (row as any).deviceName ?? (row as any).deviceCode ?? ''
const suffix = value ? '已启用' : '已停用'
if (name) {
message.success(`${name}${suffix}`)
} else {
message.success(suffix)
}
} catch {
;(row as any).is
}
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
const idsParam = buildIdsParam(ids)
await DeviceApi.deleteDevice(idsParam)
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
if (attributeDeviceId.value && idsParam.split(',').includes(String(attributeDeviceId.value))) {
attributeDeviceId.value = undefined
attributeDeviceName.value = ''
}
// 刷新列表
await getList()
} catch { }
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error(t('common.delNoData'))
return
}
await handleDelete(selectedIds.value)
}
const handleCopy = async (id: number) => {
// 二次确认
await message.confirm('是否复制设备?')
try {
await DeviceApi.copyDevice(id)
message.success(t('common.copySuccess'))
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const params: any = {
...queryParams,
ids: selectedIds.value.length ? selectedIds.value.join(',') : undefined
}
const data = await DeviceApi.exportDevice(params)
download.excel(data, t('DataCollection.Device.exportFilename'))
} catch {
} finally {
exportLoading.value = false
}
}
interface DevicePointRuleVO {
id: number
identifier: string
fieldName: string
fieldRule: string
defaultValue: string
deviceId: number
createTime?: string | number | Date
ruleAttributeId?: number
ruleOperator?: string
ruleValue?: string | number
}
const attributeDeviceId = ref<number | undefined>(undefined)
const attributeDeviceName = ref('')
const deviceTabActive = ref('deviceAttribute')
const deviceAttributeTabLabel = computed(() => {
return t('DataCollection.Device.deviceAttributeTabLabel')
})
const deviceRuleTabLabel = computed(() => {
return t('DataCollection.Device.deviceRuleTabLabel')
})
const handleShowAttribute = (row: any) => {
attributeDeviceId.value = row?.id
attributeDeviceName.value = row?.deviceName ?? ''
router.push({
path: '/iot/pointManagement',
query: {
id: row?.id,
name: row?.deviceName
}
});
}
const ruleLoading = ref(false)
const ruleList = ref<DevicePointRuleVO[]>([])
const ruleTotal = ref(0)
const ruleQueryParams = reactive({
pageNo: 1,
pageSize: 10,
identifier: undefined as string | undefined,
fieldName: undefined as string | undefined,
fieldRule: undefined as string | undefined,
defaultValue: undefined as string | undefined,
alarmLevel: undefined as string | undefined,
deviceId: undefined as number | undefined,
})
const ruleQueryFormRef = ref()
const ruleAttributeOptions = ref<any[]>([])
const getRuleList = async () => {
if (!attributeDeviceId.value) return
ruleLoading.value = true
try {
const params = {
...ruleQueryParams,
deviceId: attributeDeviceId.value,
}
const data = await request.get({ url: '/iot/device-point-rules/page', params })
const listData = Array.isArray((data as any)?.list) ? (data as any).list : (Array.isArray(data) ? data : [])
const totalData = (data as any)?.total ?? listData.length
ruleList.value = listData as DevicePointRuleVO[]
ruleTotal.value = totalData
} finally {
ruleLoading.value = false
}
}
const handleRuleQuery = () => {
if (!attributeDeviceId.value) return
ruleQueryParams.pageNo = 1
getRuleList()
}
const resetRuleQuery = () => {
if (!attributeDeviceId.value) return
ruleQueryFormRef.value?.resetFields?.()
handleRuleQuery()
}
const loadRuleAttributeOptions = async (deviceId: number) => {
try {
const res = await request.get({ url: '/iot/device-contact-model/list', params: { id: deviceId } })
const data = Array.isArray(res) ? res : (res as any)?.list ?? []
ruleAttributeOptions.value = data as any[]
} catch {
ruleAttributeOptions.value = []
}
}
watch(
() => attributeDeviceId.value,
async (val) => {
ruleQueryParams.deviceId = val ?? undefined
if (!val) {
ruleList.value = []
ruleTotal.value = 0
ruleAttributeOptions.value = []
return
}
ruleQueryParams.pageNo = 1
await getRuleList()
}
)
const ruleDialogVisible = ref(false)
const ruleFormLoading = ref(false)
const ruleFormRef = ref()
const ruleForm = reactive<Partial<DevicePointRuleVO>>({
id: undefined,
identifier: '',
fieldName: '',
fieldRule: '',
defaultValue: '',
deviceId: undefined,
ruleAttributeId: undefined,
ruleOperator: undefined,
ruleValue: undefined,
alarmLevel: '',
})
type CountArithmeticRule = {
ruleAttributeId?: number
ruleOperator?: string
ruleValue?: string | number
bigOperator?: string
}
const createEmptyCountArithmeticRule = (): CountArithmeticRule => ({
ruleAttributeId: undefined,
ruleOperator: undefined,
ruleValue: undefined,
})
const countArithmeticRules = ref<CountArithmeticRule[]>([createEmptyCountArithmeticRule()])
const extraPointRules = ref<
Array<{
id?: number
rule?: string
operator?: string
operatorRule?: string | number
}>
>([])
const runningRuleOptions = [
{ value: '1', label: '运行' },
{ value: '2', label: '待机中(不运行、没故障)' },
{ value: '3', label: '故障中(故障且待机)' },
// { value: '4', label: '报警中(故障且运行)' },
]
const alarmRuleOptions = [{ value: '5', label: '报警' }]
const countOperatorOptions = [
{ value: '+', label: '+' },
{ value: '-', label: '-' },
{ value: '*', label: '*' },
{ value: '/', label: '/' },
]
const currentRuleOptions = computed(() => {
const id = (ruleForm.identifier || '').toString().toUpperCase()
if (id === 'RUNNING') return runningRuleOptions
if (id === 'ALARM') return alarmRuleOptions
return []
})
const isRuleDisabled = (value: string, selfRule?: string | number) => {
if (!value) return false
const v = String(value)
const self = selfRule != null ? String(selfRule) : undefined
if (self === v) return false
if (ruleForm.fieldRule && String(ruleForm.fieldRule) === v && self !== v) return true
if (
extraPointRules.value.some(
(item) => item.rule != null && String(item.rule) === v && String(item.rule) !== self
)
) {
return true
}
return false
}
const ruleOperatorOptions = computed(() => getStrDictOptions('czsb_rules_conditions'))
const isCountIdentifier = computed(() => {
return (ruleForm.identifier || '').toString().toUpperCase() === 'COUNT'
})
const currentRuleOperatorOptions = computed(() => {
if (isCountIdentifier.value) return countOperatorOptions
return ruleOperatorOptions.value
})
const isRunningIdentifier = computed(() => {
return (ruleForm.identifier || '').toString().toUpperCase() === 'RUNNING'
})
const resolveRuleAttributeIdByCode = (code: any) => {
if (code == null) return undefined
const targetCode = String(code).trim().toUpperCase()
const target = ruleAttributeOptions.value.find(
(item) => String(item?.attributeCode ?? '').trim().toUpperCase() === targetCode
)
return target?.id
}
const toCountRuleValue = (value: any) => {
const text = String(value ?? '').trim()
if (!text) return ''
if (/^-?\d+(\.\d+)?$/.test(text)) {
return Number(text)
}
return text
}
const parseAlarmRunningFieldRule = (fieldRule: any) => {
if (!fieldRule) return [] as any[]
if (Array.isArray(fieldRule)) return fieldRule
if (typeof fieldRule === 'string') {
try {
const parsed = JSON.parse(fieldRule)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const buildAlarmRunningFieldRule = () => {
const list: Array<{
code: string
operator: string
operatorRule: string | number
rule: string
}> = []
if (ruleForm.ruleAttributeId && ruleForm.ruleOperator) {
const attr = ruleAttributeOptions.value.find((item) => item.id === ruleForm.ruleAttributeId)
if (attr?.attributeCode) {
list.push({
code: attr.attributeCode,
operator: String(ruleForm.ruleOperator),
operatorRule: (ruleForm.ruleValue as any) ?? '',
rule: String(ruleForm.fieldRule ?? ''),
})
}
}
extraPointRules.value.forEach((item) => {
if (!item.id || !item.operator) return
const attr = ruleAttributeOptions.value.find((opt) => opt.id === item.id)
if (!attr?.attributeCode) return
list.push({
code: attr.attributeCode,
operator: String(item.operator),
operatorRule: (item.operatorRule as any) ?? '',
rule: String(item.rule ?? ruleForm.fieldRule ?? ''),
})
})
return JSON.stringify(list)
}
const buildCountFieldRule = (rules: CountArithmeticRule[]) => {
const validRules = rules.filter((item) => {
return item.ruleAttributeId && item.ruleOperator && String(item.ruleValue ?? '').trim() !== ''
})
if (!validRules.length) return ''
const list = validRules.map((item, index) => {
const attr = ruleAttributeOptions.value.find((opt) => opt.id === item.ruleAttributeId)
if (!attr?.attributeCode) return undefined
const row: any = {
code: attr.attributeCode,
operator: String(item.ruleOperator),
value: toCountRuleValue(item.ruleValue),
}
if (index < validRules.length - 1 && item.bigOperator) {
row.bigOperator = String(item.bigOperator)
}
return row
}).filter(Boolean)
if (!list.length) return ''
return JSON.stringify(list)
}
const parseCountFieldRule = (fieldRule: any): CountArithmeticRule[] => {
if (!fieldRule) return []
const parsed = (() => {
if (typeof fieldRule !== 'string') return fieldRule
try {
return JSON.parse(fieldRule)
} catch {
return undefined
}
})() as any
if (Array.isArray(parsed)) {
return parsed
.map((item: any) => {
const pointId = resolveRuleAttributeIdByCode(item?.code) ?? item?.pointId
return {
ruleAttributeId: pointId != null ? Number(pointId) : undefined,
ruleOperator: item?.operator,
ruleValue: item?.value ?? item?.operatorRule,
bigOperator: item?.bigOperator,
} as CountArithmeticRule
})
.filter((item) => item.ruleAttributeId || item.ruleOperator || item.ruleValue != null || item.bigOperator)
}
const root = parsed?.expr ?? parsed
if (!root || typeof root !== 'object') return []
const rows: CountArithmeticRule[] = []
const walk = (node: any) => {
if (!node || typeof node !== 'object' || !node.op) return
if (node.left && typeof node.left === 'object' && node.left.op) {
walk(node.left)
}
const attrId = resolveRuleAttributeIdByCode(node?.left?.field)
const rightValue =
node?.right?.field != null
? String(node.right.field)
: node?.right?.value != null
? node.right.value
: undefined
rows.push({
ruleAttributeId: attrId,
ruleOperator: node.op,
ruleValue: rightValue,
})
}
walk(root)
return rows.filter((item) => item.ruleOperator || item.ruleAttributeId || item.ruleValue != null)
}
const handleAddCountArithmeticRule = () => {
countArithmeticRules.value.push(createEmptyCountArithmeticRule())
}
const handleRemoveCountArithmeticRule = (index: number) => {
if (countArithmeticRules.value.length <= 1) return
if (index < 0 || index >= countArithmeticRules.value.length) return
countArithmeticRules.value.splice(index, 1)
const list = countArithmeticRules.value
if (list.length > 0) {
list[list.length - 1].bigOperator = undefined
}
}
const openRuleForm = async (row: DevicePointRuleVO & { pointRulesVOList?: any[] }) => {
const deviceId = row.deviceId || attributeDeviceId.value
if (!deviceId) {
message.error(t('DataCollection.Device.messageDeviceInfoMissingForRules'))
return
}
await loadRuleAttributeOptions(deviceId)
ruleForm.id = row.id
ruleForm.identifier = row.identifier
ruleForm.fieldName = row.fieldName
ruleForm.defaultValue = row.defaultValue
ruleForm.deviceId = row.deviceId
ruleForm.alarmLevel = (row as any).alarmLevel
countArithmeticRules.value = [createEmptyCountArithmeticRule()]
extraPointRules.value = []
const list = Array.isArray(row.pointRulesVOList) ? row.pointRulesVOList : []
const parsedAlarmRunningList = parseAlarmRunningFieldRule(row.fieldRule)
const identifier = (row.identifier || '').toString().toUpperCase()
if (identifier !== 'COUNT' && parsedAlarmRunningList.length) {
const first = parsedAlarmRunningList[0] as any
ruleForm.fieldRule = first?.rule ?? ''
ruleForm.ruleAttributeId = resolveRuleAttributeIdByCode(first?.code) as any
ruleForm.ruleOperator = first?.operator as any
ruleForm.ruleValue = first?.operatorRule as any
extraPointRules.value = parsedAlarmRunningList.slice(1).map((item: any) => ({
id: resolveRuleAttributeIdByCode(item?.code),
rule: item?.rule,
operator: item?.operator,
operatorRule: item?.operatorRule,
}))
} else if (list.length) {
const first = list[0] as any
const firstRule = first.rule ?? row.fieldRule
const firstAttrId = (() => {
if (first && first.code != null) {
const target = ruleAttributeOptions.value.find(
(item) => item.attributeCode === first.code
)
return target?.id
}
if (first && first.id != null) return first.id
return undefined
})()
ruleForm.fieldRule = firstRule as any
ruleForm.ruleAttributeId = firstAttrId as any
ruleForm.ruleOperator = first.operator as any
ruleForm.ruleValue = first.operatorRule as any
extraPointRules.value = list.slice(1).map((item: any) => ({
id: (() => {
if (item && item.code != null) {
const target = ruleAttributeOptions.value.find(
(opt) => opt.attributeCode === item.code
)
return target?.id
}
if (item && item.id != null) return item.id
return undefined
})(),
rule: item.rule,
operator: item.operator,
operatorRule: item.operatorRule,
}))
if (identifier === 'COUNT') {
const parsedFieldRule = parseCountFieldRule(row.fieldRule)
if (parsedFieldRule.length) {
countArithmeticRules.value = parsedFieldRule
} else {
const parsedList = list.map((item: any) => ({
ruleAttributeId: resolveRuleAttributeIdByCode(item?.code),
ruleOperator: item?.operator,
ruleValue: item?.operatorRule,
}))
const filteredParsedList = parsedList.filter((item) => {
return item.ruleAttributeId && item.ruleOperator && String(item.ruleValue ?? '').trim() !== ''
})
countArithmeticRules.value = filteredParsedList.length
? filteredParsedList
: [createEmptyCountArithmeticRule()]
}
}
} else {
ruleForm.fieldRule = row.fieldRule
ruleForm.ruleAttributeId = row.ruleAttributeId
ruleForm.ruleOperator = row.ruleOperator
ruleForm.ruleValue = row.ruleValue as any
if (identifier === 'COUNT') {
const parsedFieldRule = parseCountFieldRule(row.fieldRule)
countArithmeticRules.value = parsedFieldRule.length
? parsedFieldRule
: [
{
ruleAttributeId: row.ruleAttributeId,
ruleOperator: row.ruleOperator,
ruleValue: row.ruleValue as any,
},
]
}
}
const options = currentRuleOptions.value
if (options.length && !options.some((item) => item.value === ruleForm.fieldRule)) {
ruleForm.fieldRule = options[0].value
}
ruleDialogVisible.value = true
}
const handleRuleSubmit = async () => {
if (!ruleForm.id) return
try {
ruleFormLoading.value = true
if (isCountIdentifier.value) {
const hasIncompleteRule = countArithmeticRules.value.some((item) => {
return !item.ruleAttributeId || !item.ruleOperator || String(item.ruleValue ?? '').trim() === ''
})
if (hasIncompleteRule) {
message.warning('请完整填写每一组四则运算规则')
return
}
const normalizedRules = countArithmeticRules.value.filter((item) => {
return item.ruleAttributeId && item.ruleOperator && String(item.ruleValue ?? '').trim() !== ''
})
if (!normalizedRules.length) {
message.warning('请至少填写一组四则运算规则')
return
}
for (let i = 0; i < normalizedRules.length - 1; i++) {
if (!normalizedRules[i].bigOperator) {
message.warning(`请为第${i + 1}组选择连接运算符`)
return
}
}
for (let i = 0; i < normalizedRules.length; i++) {
const item = normalizedRules[i]
if (item.ruleOperator !== '/') continue
const valueText = String(item.ruleValue ?? '').trim()
if (valueText !== '' && !Number.isNaN(Number(valueText)) && Number(valueText) === 0) {
message.warning(`${i + 1}组为除法时数值不能为0`)
return
}
}
const fieldRule = buildCountFieldRule(normalizedRules)
if (!fieldRule) {
message.warning('四则运算规则格式不正确')
return
}
const payload = {
id: ruleForm.id,
identifier: ruleForm.identifier,
fieldName: ruleForm.fieldName,
fieldRule,
defaultValue: ruleForm.defaultValue,
deviceId: ruleForm.deviceId ?? attributeDeviceId.value,
}
await request.put({ url: '/iot/device-point-rules/update', data: payload })
message.success(t('common.updateSuccess'))
ruleDialogVisible.value = false
await getRuleList()
return
}
const alarmRunningFieldRule = buildAlarmRunningFieldRule()
if (!alarmRunningFieldRule || alarmRunningFieldRule === '[]') {
message.warning('请至少填写一组规则')
return
}
const payload = {
id: ruleForm.id,
identifier: ruleForm.identifier,
fieldName: ruleForm.fieldName,
fieldRule: alarmRunningFieldRule,
defaultValue: ruleForm.defaultValue,
deviceId: ruleForm.deviceId ?? attributeDeviceId.value,
} as any
if (!isCountIdentifier.value) {
payload.alarmLevel = ruleForm.alarmLevel
}
await request.put({ url: '/iot/device-point-rules/update', data: payload })
message.success(t('common.updateSuccess'))
ruleDialogVisible.value = false
await getRuleList()
} finally {
ruleFormLoading.value = false
}
}
const handleRuleDelete = async (id: number) => {
if (!id) return
try {
await message.delConfirm()
await request.delete({ url: '/iot/device-point-rules/delete?id=' + id })
message.success(t('common.delSuccess'))
await getRuleList()
} catch { }
}
const handleAddPointRule = () => {
if (!isRunningIdentifier.value) return
const allOptions = currentRuleOptions.value || []
const used = new Set<string>()
if (ruleForm.fieldRule) used.add(String(ruleForm.fieldRule))
extraPointRules.value.forEach((item) => {
if (item.rule != null) used.add(String(item.rule))
})
const next = allOptions.find((opt) => !used.has(String(opt.value)))
if (!next) {
message.warning('已没有可用的点位规则可选')
return
}
extraPointRules.value.push({
rule: next.value as any,
id: undefined,
operator: undefined,
operatorRule: undefined,
})
}
const handleRemovePointRule = (index: number) => {
if (index < 0 || index >= extraPointRules.value.length) return
extraPointRules.value.splice(index, 1)
}
const createRuleDialogVisible = ref(false)
const createRuleFormLoading = ref(false)
const createRuleFormRef = ref()
const createRuleForm = reactive({
identifier: 'ALARM',
fieldName: '',
defaultValue: '报警',
alarmLevel: '',
})
const isCreateCountRule = computed(() => {
return createRuleForm.identifier.toString().toUpperCase() === 'COUNT'
})
const createRuleDialogTitle = computed(() => {
if (isCreateCountRule.value) return '新增产量'
return t('DataCollection.DeviceModel.ruleCreateButton')
})
const openCreateRuleForm = () => {
if (!attributeDeviceId.value) {
message.error('请先选择设备')
return
}
createRuleForm.identifier = 'ALARM'
createRuleForm.fieldName = '报警'
createRuleForm.defaultValue = '报警'
createRuleForm.alarmLevel = ''
createRuleDialogVisible.value = true
}
const openCreateCountRuleForm = () => {
if (!attributeDeviceId.value) {
message.error('请先选择设备')
return
}
createRuleForm.identifier = 'COUNT'
createRuleForm.fieldName = '产能'
createRuleForm.defaultValue = '产能'
createRuleForm.alarmLevel = ''
createRuleDialogVisible.value = true
}
const handleCreateRuleSubmit = async () => {
if (!attributeDeviceId.value) return
if (!createRuleForm.fieldName) {
message.warning('请输入名称')
return
}
try {
createRuleFormLoading.value = true
const payload = {
identifier: createRuleForm.identifier,
fieldName: createRuleForm.fieldName,
defaultValue: createRuleForm.defaultValue,
deviceId: attributeDeviceId.value,
} as any
if (!isCreateCountRule.value) {
payload.alarmLevel = createRuleForm.alarmLevel
}
await request.post({ url: '/iot/device-point-rules/create', data: payload })
message.success('新增成功')
createRuleDialogVisible.value = false
await getRuleList()
} finally {
createRuleFormLoading.value = false
}
}
const connectLoadingMap = reactive<Record<number, boolean>>({})
const getRowConnectValue = (row: any) => {
return row?.isConnect ?? row?.status
}
const isRowConnected = (row: any) => {
return String(getRowConnectValue(row)) === '1'
}
const handleEdit = async (row: DeviceVO) => {
if (isRowConnected(row)) {
await message.alertWarning('设备在线状态,无法编辑,请先断开连接')
return
}
openForm('update', row.id)
}
const handleToggleConnect = async (row: DeviceVO) => {
const id = row?.id
if (!id) {
message.error('设备信息不完整')
return
}
const nextIsConnect = isRowConnected(row) ? '2' : '1'
const actionText = nextIsConnect === '1' ? '连接' : '断开连接'
try {
await message.confirm(`确认${actionText}设备“${row.deviceName ?? ''}”吗?`)
connectLoadingMap[id] = true
const params: DeviceConnectParams = { id: String(id), isConnect: nextIsConnect }
await DeviceApi.connectDevice(params)
message.success(`${actionText}成功`)
await getList()
} catch {
} finally {
connectLoadingMap[id] = false
}
}
let deviceTimer: number | undefined
const startDeviceTimer = () => {
if (deviceTimer) return
deviceTimer = window.setInterval(() => {
getList(false)
}, 5000)
}
const stopDeviceTimer = () => {
if (!deviceTimer) return
window.clearInterval(deviceTimer)
deviceTimer = undefined
}
onMounted(() => {
getList()
startDeviceTimer()
})
onActivated(() => {
startDeviceTimer()
})
onDeactivated(() => {
stopDeviceTimer()
})
onBeforeUnmount(() => {
stopDeviceTimer()
})
const deviceAlarmDialogVisible = ref(false)
const deviceAlarmLoading = ref(false)
const deviceAlarmList = ref<any[]>([])
const deviceAlarmTotal = ref(0)
const deviceAlarmQueryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceId: undefined as number | undefined,
modelId: undefined as number | undefined,
rule: undefined as string | undefined,
alarmLevel: undefined as string | undefined,
addressValue: undefined as string | undefined,
createTime: [] as string[],
ruleId: undefined as number | undefined,
})
const deviceAlarmQueryFormRef = ref()
const deviceAlarmRuleOptions = ref<any[]>([])
const deviceAlarmPointOptions = ref<any[]>([])
const deviceAlarmRuleLoading = ref(false)
const deviceAlarmPointLoading = ref(false)
const loadDeviceAlarmRuleOptions = async () => {
if (!attributeDeviceId.value) return
deviceAlarmRuleLoading.value = true
try {
const res = await request.get({
url: '/iot/device-point-rules/getList',
params: { id: attributeDeviceId.value },
})
const data = (res as any)?.data ?? res
const listData = Array.isArray((data as any)?.list)
? (data as any).list
: Array.isArray(data)
? data
: []
deviceAlarmRuleOptions.value = listData as any[]
} finally {
deviceAlarmRuleLoading.value = false
}
}
const loadDeviceAlarmPointOptions = async () => {
if (!attributeDeviceId.value) return
deviceAlarmPointLoading.value = true
try {
const res = await request.get({
url: '/iot/device/device-attribute/list',
params: { deviceId: attributeDeviceId.value },
})
const data = (res as any)?.data ?? res
const listData = Array.isArray((data as any)?.list)
? (data as any).list
: Array.isArray(data)
? data
: []
deviceAlarmPointOptions.value = listData as any[]
} finally {
deviceAlarmPointLoading.value = false
}
}
const getDeviceAlarmList = async () => {
if (!attributeDeviceId.value) return
deviceAlarmLoading.value = true
try {
deviceAlarmQueryParams.deviceId = attributeDeviceId.value
const params = {
pageNo: deviceAlarmQueryParams.pageNo,
pageSize: deviceAlarmQueryParams.pageSize,
deviceId: deviceAlarmQueryParams.deviceId,
modelId: deviceAlarmQueryParams.modelId,
rule: deviceAlarmQueryParams.rule,
alarmLevel: deviceAlarmQueryParams.alarmLevel,
addressValue: deviceAlarmQueryParams.addressValue,
createTime: deviceAlarmQueryParams.createTime,
ruleId: deviceAlarmQueryParams.ruleId,
}
const data = await request.get({
url: '/iot/device-warinning-record/page',
params,
})
const listData = Array.isArray((data as any)?.list)
? (data as any).list
: Array.isArray(data)
? data
: []
const totalData = (data as any)?.total ?? listData.length
deviceAlarmList.value = listData as any[]
deviceAlarmTotal.value = totalData
} finally {
deviceAlarmLoading.value = false
}
}
const handleDeviceAlarmQuery = () => {
if (!attributeDeviceId.value) return
deviceAlarmQueryParams.pageNo = 1
getDeviceAlarmList()
}
const resetDeviceAlarmQuery = () => {
if (!attributeDeviceId.value) return
deviceAlarmQueryFormRef.value?.resetFields?.()
deviceAlarmQueryParams.pageNo = 1
getDeviceAlarmList()
}
const handleShowDeviceAlarmHistory = async () => {
if (!attributeDeviceId.value) {
message.error(t('DataCollection.Device.messageSelectDeviceRequired'))
return
}
deviceAlarmQueryParams.pageNo = 1
deviceAlarmQueryParams.pageSize = 10
deviceAlarmQueryParams.modelId = undefined
deviceAlarmQueryParams.rule = undefined
deviceAlarmQueryParams.alarmLevel = undefined
deviceAlarmQueryParams.addressValue = undefined
deviceAlarmQueryParams.createTime = []
deviceAlarmQueryParams.ruleId = undefined
deviceAlarmDialogVisible.value = true
await Promise.all([loadDeviceAlarmRuleOptions(), loadDeviceAlarmPointOptions()])
await getDeviceAlarmList()
}
</script>
<style lang="scss" scoped>
.simple-grid-view {
background-color: #fff;
border-radius: 4px;
padding: 16px;
/* max-height: calc(100vh - 546px);*/
overflow-y: auto;
.empty-grid { // 注意:这里缩进要与上面一致
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
padding: 8px;
.grid-card {
position: relative;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fafafa;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 16px;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
transform: translateY(-2px);
background-color: #fff;
}
.status-indicator {
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
border-radius: 8px 0 0 8px;
&.status-running {
background-color: #67c23a;
}
&.status-standby {
background-color: #909399;
}
&.status-fault {
background-color: #f56c6c;
}
&.status-maintenance {
background-color: #e6a23c;
}
&.status-stopped {
background-color: #909399;
}
}
.card-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background-color: #f5f7fa;
border-radius: 8px;
}
}
}
.simple-pagination {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
text-align: center;
}
}
// 响应式
@media (max-width: 768px) {
.equipment-simple {
padding: 12px;
.simple-toolbar {
flex-direction: column;
gap: 12px;
.toolbar-left {
width: 100%;
.simple-search {
flex: 1;
width: 100%;
}
.status-filter,
.type-filter {
width: 100px;
}
}
.toolbar-right {
width: 100%;
justify-content: flex-end;
}
}
.simple-grid-view {
.grid-container {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
}
}
@media (max-width: 480px) {
.equipment-simple {
.simple-grid-view {
.grid-container {
grid-template-columns: 1fr;
}
}
.simple-toolbar {
.toolbar-left {
flex-wrap: wrap;
.simple-search {
width: 100%;
}
.status-filter,
.type-filter {
flex: 1;
min-width: 120px;
}
}
}
}
}
.device-card {
width: 100%;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.device-icon {
/*width: 40px;
height: 40px;
background-color: #f0f7ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;*/
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background-color: #f0f7ff;
border-radius: 8px;
overflow: hidden;
.el-image {
cursor: pointer;
}
}
.device-info {
margin-left: 6px;
flex: 1;
}
.device-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.device-id {
font-size: 12px;
color: #999;
}
.content {
padding: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-label {
display: flex;
align-items: center;
color: #666;
font-size: 14px;
}
.info-label i {
margin-right: 6px;
color: #1890ff;
}
.info-value {
color: #333;
font-size: 14px;
}
.toggle-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
}
.toggle-label {
color: #666;
font-size: 14px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #1890ff;
}
input:checked + .slider:before {
transform: translateX(20px);
}
.footer {
display: flex;
justify-content: space-between;
padding: 16px 20px;
border-top: 1px solid #eee;
}
.action-btn {
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 6px;
font-size: 13px;
//cursor: pointer;
border: none;
background-color: transparent;
}
.btn-primary {
color: #1890ff;
border: 1px solid #1890ff;
}
.btn-success {
color: #28a745;
border: 1px solid #28a745;
}
.action-btn i {
margin-right: 6px;
font-size: 16px;
}
.action-icons {
display: flex;
}
.icon-btn {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: #666;
cursor: pointer;
border: none;
}
/* 基础样式 */
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
display: inline-block;
}
/* 在线状态 */
.status-online {
background-color: #e6f7e6;
color: #67c23a;
border: 1px solid rgba(40, 167, 69, 0.2);
}
.dot-online {
background-color: #28a745;
box-shadow: 0 0 4px rgba(40, 167, 69, 0.5);
}
/* 离线状态 */
.status-offline {
background-color: #f5f5f5;
color: #909399;
border: 1px solid rgba(108, 117, 125, 0.2);
}
.dot-offline {
background-color: #6c757d;
box-shadow: 0 0 4px rgba(108, 117, 125, 0.3);
}
/* 运行中 */
.status-running {
background-color: #67c23a;
color: white;
border: 1px solid rgba(24, 144, 255, 0.2);
}
.dot-running {
background-color: #67c23a;
animation: pulse 2s infinite;
box-shadow: 0 0 4px rgba(24, 144, 255, 0.5);
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 已停止 */
.status-stopped {
background-color: #f5f5f5;
color: #999;
border: 1px solid rgba(153, 153, 153, 0.2);
}
.dot-stopped {
background-color: #999;
}
/* 待机中 */
.status-standby {
background-color: #f5f5f5;
color: #999;
border: 1px solid rgba(153, 153, 153, 0.2);
}
.dot-stopped {
background-color: #999;
}
/* 故障 */
.status-fault {
background-color: #fde8e8;
color: #f5222d;
border: 1px solid rgba(245, 34, 45, 0.2);
}
.dot-fault {
background-color: #f5222d;
box-shadow: 0 0 4px rgba(245, 34, 45, 0.5);
}
/* 报警 */
.status-alarm {
background-color: #fff7e6;
color: #fa8c16;
border: 1px solid rgba(250, 140, 22, 0.2);
}
.dot-alarm {
background-color: #fa8c16;
animation: blink 1s infinite;
box-shadow: 0 0 4px rgba(250, 140, 22, 0.5);
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* 维护中 */
.status-maintenance {
background-color: #f0f7ff;
color: #1890ff;
border: 1px solid rgba(24, 144, 255, 0.2);
}
.dot-maintenance {
background-color: #1890ff;
animation: maintenance 3s infinite;
}
@keyframes maintenance {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* 未启用 */
.status-disabled {
background-color: #fafafa;
color: #bfbfbf;
border: 1px solid rgba(191, 191, 191, 0.2);
text-decoration: line-through;
}
.dot-disabled {
background-color: #bfbfbf;
}
/* 默认状态 */
.status-default {
background-color: #f5f5f5;
color: #666;
border: 1px solid rgba(102, 102, 102, 0.2);
}
.dot-default {
background-color: #666;
}
</style>