feat(芯导-环保门禁): 设备远程监控、车辆信息采集手机端,修复车辆列表分页

- 新增设备远程监控页面:设备台账/远程控制(启停指令)/异常报警(60秒轮询弹窗推送)
- 新增车辆信息采集手机端 H5 页面(12 项字段,提交后进审核)
- 新增车辆信息采集审核容器页(采集审核 + 手机端采集 两个 tab)
- 新增 deviceCommand 接口封装
- 修复车辆信息审核列表分页参数错误(pageNum/pageSize → current/size)导致只显示前 10 条
已修改1个文件
已删除5个文件
已添加4个文件
602 ■■■■■ 文件已修改
src/api/environmentAccess/accessVehicle.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/deviceCommand.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/inspection.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/reportCenter.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/unattendedWeighbridgeSystem.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/vehicleInfo.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/remoteMonitor/index.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/vehicleInfoAudit/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/vehicleInfoCenter/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/vehicleInfoCollect/index.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/accessVehicle.js
ÎļþÒÑɾ³ý
src/api/environmentAccess/deviceCommand.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
import request from "@/utils/request";
// è®¾å¤‡åˆ—表(远程控制对象)
export function getDeviceList() {
  return request({ url: "/environmentAccess/deviceCommand/deviceList", method: "get" });
}
// å¼‚常报警列表
export function getAlarmList() {
  return request({ url: "/environmentAccess/deviceCommand/alarmList", method: "get" });
}
// æŒ‡ä»¤è®°å½•分页
export function getCommandPage(params) {
  return request({ url: "/environmentAccess/deviceCommand/listPage", method: "get", params });
}
// ä¸‹å‘启停指令
export function sendCommand(data) {
  return request({ url: "/environmentAccess/deviceCommand/send", method: "post", data });
}
src/api/environmentAccess/inspection.js
ÎļþÒÑɾ³ý
src/api/environmentAccess/reportCenter.js
ÎļþÒÑɾ³ý
src/api/environmentAccess/unattendedWeighbridgeSystem.js
ÎļþÒÑɾ³ý
src/api/environmentAccess/vehicleInfo.js
ÎļþÒÑɾ³ý
src/views/environmentAccess/remoteMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab">
      <el-tab-pane label="设备台账" name="ledger" />
      <el-tab-pane label="远程控制" name="control" />
      <el-tab-pane label="异常报警" name="alarm">
        <template #label>
          <span>异常报警<el-badge v-if="unhandledCount > 0" :value="unhandledCount" style="margin-left: 6px" /></span>
        </template>
      </el-tab-pane>
    </el-tabs>
    <!-- è¿œç¨‹æŽ§åˆ¶ -->
    <div v-if="activeTab === 'ledger'">
      <LedgerPage />
    </div>
    <div v-else-if="activeTab === 'control'">
      <div class="section-title">设备列表(远程启停)</div>
      <el-table :data="deviceList" border v-loading="deviceLoading" height="300">
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="设备名称" prop="deviceName" show-overflow-tooltip />
        <el-table-column label="规格型号" prop="deviceModel" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" width="80" />
        <el-table-column fixed="right" label="远程控制" width="180" align="center">
          <template #default="scope">
            <el-button link type="success" size="small" @click="handleSend(scope.row, 'start')">启动</el-button>
            <el-button link type="danger" size="small" @click="handleSend(scope.row, 'stop')">停止</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="section-title" style="margin-top: 16px">指令记录</div>
      <el-table :data="commandList" border v-loading="commandLoading" height="300">
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="设备名称" prop="deviceName" show-overflow-tooltip />
        <el-table-column label="指令" prop="commandType" width="90" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.commandType === 'start' ? 'success' : 'danger'">
              {{ scope.row.commandType === 'start' ? '启动' : '停止' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" prop="commandStatus" width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.commandStatus === 'success' ? 'success' : (scope.row.commandStatus === 'failed' ? 'danger' : 'info')">
              {{ scope.row.commandStatus === 'success' ? '成功' : (scope.row.commandStatus === 'failed' ? '失败' : '下发中') }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作人" prop="operator" width="110" />
        <el-table-column label="下发时间" prop="commandTime" width="170" />
        <el-table-column label="执行结果" prop="resultMsg" show-overflow-tooltip />
      </el-table>
      <pagination v-show="commandPage.total > 0" :total="commandPage.total" layout="total, prev, pager, next"
        :page="commandPage.current" :limit="commandPage.size" @pagination="commandPageChange" />
    </div>
    <!-- å¼‚常报警 -->
    <div v-else>
      <el-alert type="info" :closable="false" show-icon style="margin-bottom: 10px"
        title="系统每 60 ç§’自动检查一次新报警,发现未处理报警会弹窗提醒(站内推送)" />
      <el-table :data="alarmList" border v-loading="alarmLoading" height="calc(100vh - 16em)">
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="设备名称" prop="deviceName" show-overflow-tooltip width="220">
          <template #default="scope">{{ scope.row.deviceName || '-' }}</template>
        </el-table-column>
        <el-table-column label="故障描述" prop="defectDescription" show-overflow-tooltip />
        <el-table-column label="设备状态" prop="status" width="120" />
        <el-table-column label="是否处理" width="110" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.handled ? 'success' : 'danger'">{{ scope.row.handled ? '已消除' : '未处理' }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="发生时间" prop="createTime" width="170" />
        <el-table-column label="消除时间" prop="eliminateTime" width="170" />
      </el-table>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from "vue";
import { ElNotification, ElMessageBox, ElMessage } from "element-plus";
import pagination from "@/components/PIMTable/Pagination.vue";
import LedgerPage from "@/views/equipmentManagement/ledger/index.vue";
import { getDeviceList, getAlarmList, getCommandPage, sendCommand } from "@/api/environmentAccess/deviceCommand.js";
defineOptions({ name: "设备远程监控" });
const activeTab = ref("ledger");
const deviceList = ref([]);
const deviceLoading = ref(false);
const commandList = ref([]);
const commandLoading = ref(false);
const commandPage = ref({ current: 1, size: 10, total: 0 });
const alarmList = ref([]);
const alarmLoading = ref(false);
const unhandledCount = computed(() => alarmList.value.filter((a) => !a.handled).length);
/* ---------- è¿œç¨‹æŽ§åˆ¶ ---------- */
const loadDevices = () => {
  deviceLoading.value = true;
  getDeviceList().then((res) => {
    deviceList.value = res.data || [];
  }).finally(() => { deviceLoading.value = false; });
};
const loadCommands = () => {
  commandLoading.value = true;
  getCommandPage({ current: commandPage.value.current, size: commandPage.value.size }).then((res) => {
    commandList.value = (res.data && res.data.records) || [];
    commandPage.value.total = (res.data && res.data.total) || 0;
  }).finally(() => { commandLoading.value = false; });
};
const commandPageChange = (obj) => {
  commandPage.value.current = obj.page;
  commandPage.value.size = obj.limit;
  loadCommands();
};
const handleSend = (row, type) => {
  const label = type === "start" ? "启动" : "停止";
  ElMessageBox.confirm(`确认对「${row.deviceName || '该设备'}」下发${label}指令?`, "远程控制", { type: "warning" })
    .then(() => {
      sendCommand({ deviceLedgerId: row.id, deviceName: row.deviceName, commandType: type }).then((res) => {
        ElMessage.success(res.msg || `${label}指令已下发`);
        commandPage.value.current = 1;
        loadCommands();
      });
    })
    .catch(() => {});
};
/* ---------- å¼‚常报警(含站内弹窗提醒) ---------- */
let alarmTimer = null;
let lastMaxAlarmId = null;
const loadAlarms = (notify) => {
  alarmLoading.value = true;
  return getAlarmList().then((res) => {
    const list = res.data || [];
    alarmList.value = list;
    const maxId = list.reduce((m, a) => Math.max(m, Number(a.id) || 0), 0);
    if (notify && lastMaxAlarmId !== null && maxId > lastMaxAlarmId) {
      const fresh = list.filter((a) => Number(a.id) > lastMaxAlarmId && !a.handled);
      fresh.slice(0, 3).forEach((a) => {
        ElNotification({
          title: "设备异常报警",
          message: `【${a.deviceName || '未知设备'}】${a.defectDescription || '发生设备故障'},请及时处理`,
          type: "error",
          duration: 15000,
          position: "bottom-right",
        });
      });
    }
    lastMaxAlarmId = maxId;
  }).finally(() => { alarmLoading.value = false; });
};
const startAlarmPolling = () => {
  loadAlarms(false);
  alarmTimer = setInterval(() => loadAlarms(true), 60000);
};
onMounted(() => {
  loadDevices();
  loadCommands();
  startAlarmPolling();
});
onUnmounted(() => {
  if (alarmTimer) {
    clearInterval(alarmTimer);
    alarmTimer = null;
  }
});
</script>
<style scoped lang="scss">
.section-title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 8px;
}
</style>
src/views/environmentAccess/vehicleInfoAudit/index.vue
@@ -367,8 +367,8 @@
function loadTableData() {
  tableLoading.value = true;
  listVehicleInfo({
    pageNum: page.current,
    pageSize: page.size,
    current: page.current,
    size: page.size,
    ...searchForm,
  })
    .then((res) => {
@@ -419,6 +419,7 @@
      .then(() => {
        ElMessage.success(operationType.value === "add" ? "新增成功" : "编辑成功");
        dialogVisible.value = false;
        if (operationType.value === "add") page.current = 1;
        loadTableData();
      })
      .catch(() => {});
src/views/environmentAccess/vehicleInfoCenter/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
<template>
  <div class="vehicle-center">
    <el-tabs v-model="activeTab" class="center-tabs">
      <el-tab-pane label="采集审核" name="audit" />
      <el-tab-pane label="手机端采集" name="collect" />
    </el-tabs>
    <div class="center-body">
      <keep-alive>
        <component :is="currentComponent" />
      </keep-alive>
    </div>
  </div>
</template>
<script setup>
import { ref, computed } from "vue";
import AuditPage from "../vehicleInfoAudit/index.vue";
import CollectPage from "../vehicleInfoCollect/index.vue";
defineOptions({ name: "车辆信息采集审核" });
const activeTab = ref("audit");
const tabMap = {
  audit: AuditPage,
  collect: CollectPage,
};
const currentComponent = computed(() => tabMap[activeTab.value] || AuditPage);
</script>
<style scoped lang="scss">
.vehicle-center {
  padding: 10px 12px;
}
.center-tabs {
  margin-bottom: 6px;
}
/* å­é¡µé¢è‡ªå¸¦çš„ app-container ä¼šå¤šä¸€å±‚内边距 */
.center-body :deep(.app-container) {
  padding: 0;
}
</style>
src/views/environmentAccess/vehicleInfoCollect/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
<template>
  <div class="collect-page">
    <div class="collect-header">
      <h2>车辆信息采集</h2>
      <p>请如实填写车辆信息,提交后由企业审核</p>
    </div>
    <el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="collect-form">
      <el-form-item label="车牌号码" prop="plateNo">
        <el-input v-model="form.plateNo" size="large" placeholder="如:苏A12345" clearable />
      </el-form-item>
      <el-form-item label="号牌颜色" prop="plateColor">
        <el-select v-model="form.plateColor" size="large" placeholder="请选择" style="width: 100%">
          <el-option v-for="i in plateColorOptions" :key="i" :label="i" :value="i" />
        </el-select>
      </el-form-item>
      <el-form-item label="车辆类型" prop="carType">
        <el-select v-model="form.carType" size="large" placeholder="请选择" style="width: 100%">
          <el-option v-for="i in carTypeOptions" :key="i" :label="i" :value="i" />
        </el-select>
      </el-form-item>
      <el-form-item label="车辆识别代码(VIN)" prop="carVin">
        <el-input v-model="form.carVin" size="large" placeholder="17 ä½ VIN ç " clearable />
      </el-form-item>
      <el-form-item label="车辆型号" prop="carModel">
        <el-input v-model="form.carModel" size="large" placeholder="请输入车辆型号" clearable />
      </el-form-item>
      <el-form-item label="发动机型号" prop="engineModel">
        <el-input v-model="form.engineModel" size="large" placeholder="请输入发动机型号" clearable />
      </el-form-item>
      <el-form-item label="发动机生产厂" prop="engineProductFactory">
        <el-input v-model="form.engineProductFactory" size="large" placeholder="请输入发动机生产厂" clearable />
      </el-form-item>
      <el-form-item label="发动机编号" prop="engineNo">
        <el-input v-model="form.engineNo" size="large" placeholder="请输入发动机编号" clearable />
      </el-form-item>
      <el-form-item label="排放标准" prop="emissionStandard">
        <el-select v-model="form.emissionStandard" size="large" placeholder="请选择" style="width: 100%">
          <el-option v-for="i in emissionOptions" :key="i" :label="i" :value="i" />
        </el-select>
      </el-form-item>
      <el-form-item label="注册登记日期" prop="registeDate">
        <el-date-picker
          v-model="form.registeDate"
          size="large"
          type="date"
          value-format="YYYY-MM-DD"
          placeholder="请选择注册登记日期"
          style="width: 100%"
        />
      </el-form-item>
      <el-form-item label="使用性质" prop="natureOfUse">
        <el-select v-model="form.natureOfUse" size="large" placeholder="请选择" style="width: 100%">
          <el-option v-for="i in natureOptions" :key="i" :label="i" :value="i" />
        </el-select>
      </el-form-item>
      <el-form-item label="燃料类型" prop="fuelType">
        <el-select v-model="form.fuelType" size="large" placeholder="请选择" style="width: 100%">
          <el-option v-for="i in fuelOptions" :key="i" :label="i" :value="i" />
        </el-select>
      </el-form-item>
      <div class="collect-actions">
        <el-button type="primary" size="large" :loading="submitting" style="width: 100%" @click="submit">
          æäº¤é‡‡é›†
        </el-button>
        <el-button size="large" style="width: 100%; margin: 10px 0 0 0" @click="reset">重新填写</el-button>
      </div>
    </el-form>
  </div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { addVehicleInfo } from "@/api/environmentAccess/vehicleInfo.js";
defineOptions({ name: "车辆信息采集" });
const formRef = ref();
const submitting = ref(false);
const plateColorOptions = ["蓝色", "黄色", "绿色", "白色", "黑色", "其他"];
const carTypeOptions = ["小型汽车", "大型汽车", "新能源汽车", "挂车", "摩托车", "其他"];
const emissionOptions = ["国三", "国四", "国五", "国六", "纯电动", "其他"];
const natureOptions = ["营运", "非营运", "货运", "客运", "其他"];
const fuelOptions = ["汽油", "柴油", "天然气", "电", "混合动力", "其他"];
const emptyForm = () => ({
  plateNo: "",
  plateColor: "",
  carType: "",
  carVin: "",
  carModel: "",
  engineModel: "",
  engineProductFactory: "",
  engineNo: "",
  emissionStandard: "",
  registeDate: "",
  natureOfUse: "",
  fuelType: "",
});
const form = reactive(emptyForm());
const rules = {
  plateNo: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  plateColor: [{ required: true, message: "请选择号牌颜色", trigger: "change" }],
  carType: [{ required: true, message: "请选择车辆类型", trigger: "change" }],
};
const submit = () => {
  formRef.value?.validate((valid) => {
    if (!valid) return;
    submitting.value = true;
    addVehicleInfo({ ...form, reviewStatus: "pending" })
      .then(() => {
        ElMessage.success("提交成功,等待企业审核");
        Object.assign(form, emptyForm());
        formRef.value?.clearValidate();
      })
      .catch(() => ElMessage.error("提交失败,请稍后重试"))
      .finally(() => {
        submitting.value = false;
      });
  });
};
const reset = () => {
  Object.assign(form, emptyForm());
  formRef.value?.clearValidate();
};
</script>
<style scoped lang="scss">
.collect-page {
  max-width: 640px;
  margin: 0 auto;
  padding: 16px 14px 40px;
  background: #fff;
  min-height: 100vh;
  box-sizing: border-box;
}
.collect-header {
  padding: 8px 0 16px;
  border-bottom: 1px solid #ebeef5;
  margin-bottom: 18px;
  h2 {
    margin: 0 0 6px;
    font-size: 20px;
    color: #303133;
  }
  p {
    margin: 0;
    font-size: 13px;
    color: #909399;
  }
}
.collect-form {
  :deep(.el-form-item__label) {
    font-size: 15px;
    padding-bottom: 4px;
  }
  :deep(.el-input__inner),
  :deep(.el-select__placeholder) {
    font-size: 15px;
  }
}
.collect-actions {
  margin-top: 22px;
}
</style>