hekang 2 месяцев назад
Родитель
Сommit
787032b9f3

+ 377 - 0
forecast-backend-vue/src/views/ufo/electricity/fault/FaultProcess.vue

@@ -0,0 +1,377 @@
+<template>
+  <div class="fault-page">
+    <a-card :bordered="false">
+      <a-form :model="queryParams" layout="inline">
+        <a-form-item label="故障标题">
+          <a-input v-model:value="queryParams.faultTitle" allow-clear placeholder="请输入故障标题" />
+        </a-form-item>
+        <a-form-item label="场站">
+          <a-select
+            v-model:value="queryParams.stationId"
+            allow-clear
+            show-search
+            class="query-select"
+            option-filter-prop="label"
+            :options="stationOptions"
+          />
+        </a-form-item>
+        <a-form-item label="状态">
+          <a-select v-model:value="queryParams.status" allow-clear class="query-select" :options="statusOptions" />
+        </a-form-item>
+        <a-form-item>
+          <a-space>
+            <a-button type="primary" :loading="loading" @click="handleQuery">查询</a-button>
+            <a-button @click="resetQuery">重置</a-button>
+            <a-button v-hasPermi="['alarm:faultProcess:add']" type="primary" @click="openForm()">新增</a-button>
+          </a-space>
+        </a-form-item>
+      </a-form>
+    </a-card>
+
+    <a-card :bordered="false" class="section">
+      <a-table
+        :columns="columns"
+        :data-source="rows"
+        :loading="loading"
+        :pagination="pagination"
+        row-key="id"
+        @change="handleTableChange"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'stationId'">
+            {{ stationText(record.stationId) }}
+          </template>
+          <template v-else-if="column.key === 'faultType'">
+            {{ dictText(faultTypeOptions, record.faultType) }}
+          </template>
+          <template v-else-if="column.key === 'faultLevel'">
+            {{ dictText(faultLevelOptions, record.faultLevel) }}
+          </template>
+          <template v-else-if="column.key === 'status'">
+            <a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
+          </template>
+          <template v-else-if="column.key === 'operation'">
+            <a-space>
+              <a v-hasPermi="['alarm:faultProcess:edit']" @click="openForm(record)">编辑</a>
+              <a @click="openLogs(record)">日志</a>
+              <a-dropdown v-hasPermi="['alarm:faultProcess:edit']">
+                <a>处理</a>
+                <template #overlay>
+                  <a-menu @click="({ key }) => openAction(record, key)">
+                    <a-menu-item key="assign">认领/派单</a-menu-item>
+                    <a-menu-item key="handle">处理记录</a-menu-item>
+                    <a-menu-item key="resolve">解决</a-menu-item>
+                    <a-menu-item key="close">关闭</a-menu-item>
+                    <a-menu-item key="falseAlarm">误报</a-menu-item>
+                  </a-menu>
+                </template>
+              </a-dropdown>
+              <a-popconfirm title="确认删除该故障处理记录?" @confirm="removeRecord(record)">
+                <a v-hasPermi="['alarm:faultProcess:remove']" class="danger-link">删除</a>
+              </a-popconfirm>
+            </a-space>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+
+    <a-modal v-model:open="formOpen" :title="form.id ? '编辑故障处理' : '新增故障处理'" width="720px" @ok="submitForm">
+      <a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
+        <a-form-item label="场站" name="stationId">
+          <a-select
+            v-model:value="form.stationId"
+            allow-clear
+            show-search
+            option-filter-prop="label"
+            :options="stationOptions"
+          />
+        </a-form-item>
+        <a-form-item label="故障标题" name="faultTitle">
+          <a-input v-model:value="form.faultTitle" allow-clear />
+        </a-form-item>
+        <a-form-item label="故障类型" name="faultType">
+          <a-select v-model:value="form.faultType" allow-clear :options="faultTypeOptions" />
+        </a-form-item>
+        <a-form-item label="故障等级" name="faultLevel">
+          <a-select v-model:value="form.faultLevel" allow-clear :options="faultLevelOptions" />
+        </a-form-item>
+        <a-form-item label="故障原因" name="faultReason">
+          <a-textarea v-model:value="form.faultReason" :rows="3" allow-clear />
+        </a-form-item>
+        <a-form-item label="处理结果" name="processResult">
+          <a-textarea v-model:value="form.processResult" :rows="3" allow-clear />
+        </a-form-item>
+        <a-form-item label="备注" name="remark">
+          <a-textarea v-model:value="form.remark" :rows="2" allow-clear />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <a-modal v-model:open="actionOpen" :title="actionTitle" width="640px" @ok="submitAction">
+      <a-form :model="actionForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
+        <a-form-item v-if="['resolve', 'close', 'falseAlarm'].includes(actionType)" label="处理结果">
+          <a-textarea v-model:value="actionForm.processResult" :rows="3" allow-clear />
+        </a-form-item>
+        <a-form-item v-if="actionType === 'handle'" label="处理内容">
+          <a-textarea v-model:value="actionForm.content" :rows="4" allow-clear />
+        </a-form-item>
+        <a-form-item label="备注">
+          <a-textarea v-model:value="actionForm.remark" :rows="2" allow-clear />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+
+    <a-modal v-model:open="logOpen" title="处理日志" width="860px" :footer="null">
+      <a-table :columns="logColumns" :data-source="logRows" :pagination="false" row-key="id" size="small">
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'actionType'">
+            {{ actionText(record.actionType) }}
+          </template>
+          <template v-else-if="column.key === 'beforeStatus'">
+            {{ statusText(record.beforeStatus) }}
+          </template>
+          <template v-else-if="column.key === 'afterStatus'">
+            {{ statusText(record.afterStatus) }}
+          </template>
+        </template>
+      </a-table>
+    </a-modal>
+  </div>
+</template>
+
+<script setup name="ElectricityFaultProcess">
+import { onMounted, reactive, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import { listStation } from '@/api/config/station';
+import { getDicts } from '@/api/system/dict/data';
+import {
+  addFaultProcess,
+  assignFaultProcess,
+  closeFaultProcess,
+  delFaultProcess,
+  falseAlarmFaultProcess,
+  getFaultProcessLogs,
+  handleFaultProcess,
+  listFaultProcess,
+  resolveFaultProcess,
+  updateFaultProcess
+} from '@/api/alarm/faultProcess';
+import { buildStationNameMap, toStationOptions } from '../components/stationAdapter';
+
+const loading = ref(false);
+const rows = ref([]);
+const stationOptions = ref([]);
+const stationNameMap = ref(new Map());
+const faultTypeOptions = ref([]);
+const faultLevelOptions = ref([]);
+const statusOptions = ref([]);
+const actionOptions = ref([]);
+const formOpen = ref(false);
+const actionOpen = ref(false);
+const logOpen = ref(false);
+const logRows = ref([]);
+const formRef = ref();
+const actionType = ref('');
+const actionTitle = ref('');
+const currentRecord = ref();
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  faultTitle: undefined,
+  stationId: undefined,
+  status: undefined
+});
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true
+});
+
+const form = reactive({});
+const actionForm = reactive({});
+
+
+const rules = {
+  stationId: [{ required: true, message: '请选择场站', trigger: 'change' }],
+  faultTitle: [{ required: true, message: '请输入故障标题', trigger: 'blur' }],
+  faultType: [{ required: true, message: '请选择故障类型', trigger: 'change' }],
+  faultLevel: [{ required: true, message: '请选择故障等级', trigger: 'change' }]
+};
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', width: 80 },
+  { title: '故障标题', dataIndex: 'faultTitle' },
+  { title: '场站', dataIndex: 'stationId', key: 'stationId', width: 160 },
+  { title: '故障类型', dataIndex: 'faultType', key: 'faultType', width: 120 },
+  { title: '等级', dataIndex: 'faultLevel', key: 'faultLevel', width: 100 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
+  { title: '处理人', dataIndex: 'handlerName', width: 120 },
+  { title: '创建时间', dataIndex: 'createTime', width: 180 },
+  { title: '操作', key: 'operation', width: 220, fixed: 'right' }
+];
+
+const logColumns = [
+  { title: '动作', dataIndex: 'actionType', key: 'actionType', width: 120 },
+  { title: '处理前', dataIndex: 'beforeStatus', key: 'beforeStatus', width: 100 },
+  { title: '处理后', dataIndex: 'afterStatus', key: 'afterStatus', width: 100 },
+  { title: '操作人', dataIndex: 'operatorName', width: 120 },
+  { title: '操作时间', dataIndex: 'actionTime', width: 160 },
+  { title: '内容', dataIndex: 'content' }
+];
+
+const normalizeRows = (res) => res?.rows || res?.data || [];
+const normalizeDict = (res, numberValue = false) => (res?.data || []).map((item) => ({
+  label: item.dictLabel,
+  value: numberValue ? Number(item.dictValue) : item.dictValue
+}));
+
+const stationText = (stationId) => stationNameMap.value.get(stationId) || stationId || '-';
+const dictText = (options, value) => options.find((item) => String(item.value) === String(value))?.label || value || '-';
+const actionText = (action) => dictText(actionOptions.value, action);
+const statusText = (status) => dictText(statusOptions.value, status);
+const statusColor = (status) => {
+  const colorMap = {
+    pending: 'default',
+    processing: 'processing',
+    resolved: 'success',
+    closed: 'blue',
+    false_alarm: 'warning'
+  };
+  return colorMap[status] || 'default';
+};
+
+const resetObject = (target, source = {}) => {
+  Object.keys(target).forEach((key) => delete target[key]);
+  Object.assign(target, source);
+};
+
+const loadOptions = async () => {
+  const [stationRes, typeRes, levelRes, statusRes, actionRes] = await Promise.all([
+    listStation({ pageNum: 1, pageSize: 500 }),
+    getDicts('fault_type'),
+    getDicts('fault_level'),
+    getDicts('fault_process_status'),
+    getDicts('fault_process_action')
+  ]);
+  const stationRows = normalizeRows(stationRes);
+  stationOptions.value = toStationOptions(stationRows);
+  stationNameMap.value = buildStationNameMap(stationRows);
+  faultTypeOptions.value = normalizeDict(typeRes);
+  faultLevelOptions.value = normalizeDict(levelRes, true);
+  statusOptions.value = normalizeDict(statusRes);
+  actionOptions.value = normalizeDict(actionRes);
+};
+
+const handleQuery = async () => {
+  loading.value = true;
+  try {
+    const res = await listFaultProcess(queryParams);
+    rows.value = normalizeRows(res);
+    pagination.total = res?.total || rows.value.length;
+    pagination.current = queryParams.pageNum;
+    pagination.pageSize = queryParams.pageSize;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const resetQuery = () => {
+  queryParams.pageNum = 1;
+  queryParams.faultTitle = undefined;
+  queryParams.stationId = undefined;
+  queryParams.status = undefined;
+  handleQuery();
+};
+
+const handleTableChange = (page) => {
+  queryParams.pageNum = page.current;
+  queryParams.pageSize = page.pageSize;
+  handleQuery();
+};
+
+const openForm = (record) => {
+  const source = record ? { ...record } : { sourceType: 'manual', status: 'pending', delFlag: '0' };
+  if (source.faultLevel !== undefined && source.faultLevel !== null) {
+    source.faultLevel = Number(source.faultLevel);
+  }
+  if (!record) {
+    delete source.alarmEventId;
+  }
+  delete source.handlerId;
+  delete source.handlerName;
+  resetObject(form, source);
+  formOpen.value = true;
+};
+
+const submitForm = async () => {
+  await formRef.value?.validate();
+  if (form.id) {
+    await updateFaultProcess(form);
+  } else {
+    await addFaultProcess(form);
+  }
+  message.success('保存成功');
+  formOpen.value = false;
+  handleQuery();
+};
+
+const removeRecord = async (record) => {
+  await delFaultProcess(record.id);
+  message.success('删除成功');
+  handleQuery();
+};
+
+const openAction = (record, type) => {
+  currentRecord.value = record;
+  actionType.value = type;
+  const titleMap = {
+    assign: '认领/派单',
+    handle: '处理记录',
+    resolve: '解决故障',
+    close: '关闭故障',
+    falseAlarm: '标记误报'
+  };
+  actionTitle.value = titleMap[type];
+  resetObject(actionForm, {
+    processResult: record.processResult,
+    remark: undefined,
+    content: undefined
+  });
+  actionOpen.value = true;
+};
+
+const submitAction = async () => {
+  const id = currentRecord.value.id;
+  const actionMap = {
+    assign: () => assignFaultProcess(id, actionForm),
+    handle: () => handleFaultProcess(id, actionForm),
+    resolve: () => resolveFaultProcess(id, actionForm),
+    close: () => closeFaultProcess(id, actionForm),
+    falseAlarm: () => falseAlarmFaultProcess(id, actionForm)
+  };
+  await actionMap[actionType.value]();
+  message.success('操作成功');
+  actionOpen.value = false;
+  handleQuery();
+};
+
+const openLogs = async (record) => {
+  const res = await getFaultProcessLogs(record.id);
+  logRows.value = res?.data || [];
+  logOpen.value = true;
+};
+
+onMounted(async () => {
+  await loadOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.fault-page { padding: 16px; }
+.section { margin-top: 16px; }
+.query-select { width: 180px; }
+</style>

+ 179 - 0
forecast-backend-vue/src/views/ufo/electricity/fault/FaultProcessLog.vue

@@ -0,0 +1,179 @@
+<template>
+  <div class="fault-page">
+    <a-card :bordered="false">
+      <a-form :model="queryParams" layout="inline">
+        <a-form-item label="处理单ID">
+          <a-input-number v-model:value="queryParams.processId" class="query-number" :min="1" />
+        </a-form-item>
+        <a-form-item label="告警事件ID">
+          <a-input v-model:value="queryParams.alarmEventId" allow-clear placeholder="请输入告警事件ID" />
+        </a-form-item>
+        <a-form-item label="动作">
+          <a-select v-model:value="queryParams.actionType" allow-clear class="query-select" :options="actionOptions" />
+        </a-form-item>
+        <a-form-item>
+          <a-space>
+            <a-button type="primary" :loading="loading" @click="handleQuery">查询</a-button>
+            <a-button @click="resetQuery">重置</a-button>
+          </a-space>
+        </a-form-item>
+      </a-form>
+    </a-card>
+
+    <a-card :bordered="false" class="section">
+      <a-table
+        :columns="columns"
+        :data-source="rows"
+        :loading="loading"
+        :pagination="pagination"
+        row-key="id"
+        @change="handleTableChange"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'statusChange'">
+            {{ statusText(record.beforeStatus) }} -> {{ statusText(record.afterStatus) }}
+          </template>
+          <template v-else-if="column.key === 'operation'">
+            <a-space>
+              <a @click="openDetail(record)">详情</a>
+              <a-popconfirm title="确认删除该日志?" @confirm="removeRecord(record)">
+                <a v-hasPermi="['alarm:faultProcessLog:remove']" class="danger-link">删除</a>
+              </a-popconfirm>
+            </a-space>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+
+    <a-modal v-model:open="detailOpen" title="故障处理日志详情" width="720px" :footer="null">
+      <a-descriptions bordered :column="2" size="small">
+        <a-descriptions-item label="ID">{{ detail.id }}</a-descriptions-item>
+        <a-descriptions-item label="处理单ID">{{ detail.processId }}</a-descriptions-item>
+        <a-descriptions-item label="告警事件ID">{{ detail.alarmEventId }}</a-descriptions-item>
+        <a-descriptions-item label="动作">{{ actionText(detail.actionType) }}</a-descriptions-item>
+        <a-descriptions-item label="处理前">{{ statusText(detail.beforeStatus) }}</a-descriptions-item>
+        <a-descriptions-item label="处理后">{{ statusText(detail.afterStatus) }}</a-descriptions-item>
+        <a-descriptions-item label="操作人">{{ detail.operatorName }}</a-descriptions-item>
+        <a-descriptions-item label="操作时间">{{ detail.actionTime }}</a-descriptions-item>
+        <a-descriptions-item label="内容" :span="2">{{ detail.content }}</a-descriptions-item>
+        <a-descriptions-item label="附件" :span="2">{{ detail.attachments }}</a-descriptions-item>
+      </a-descriptions>
+    </a-modal>
+  </div>
+</template>
+
+<script setup name="ElectricityFaultProcessLog">
+import { onMounted, reactive, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import { getDicts } from '@/api/system/dict/data';
+import { delFaultProcessLog, listFaultProcessLog } from '@/api/alarm/faultProcessLog';
+
+const loading = ref(false);
+const rows = ref([]);
+const detailOpen = ref(false);
+const detail = reactive({});
+const actionOptions = ref([]);
+const statusOptions = ref([]);
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  processId: undefined,
+  alarmEventId: undefined,
+  actionType: undefined
+});
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true
+});
+
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', width: 80 },
+  { title: '处理单ID', dataIndex: 'processId', width: 110 },
+  { title: '告警事件ID', dataIndex: 'alarmEventId', width: 160 },
+  { title: '动作', dataIndex: 'actionType', customRender: ({ text }) => actionText(text), width: 120 },
+  { title: '状态变化', key: 'statusChange', width: 180 },
+  { title: '操作人', dataIndex: 'operatorName', width: 120 },
+  { title: '操作时间', dataIndex: 'actionTime', width: 180 },
+  { title: '内容', dataIndex: 'content' },
+  { title: '操作', key: 'operation', width: 120, fixed: 'right' }
+];
+
+const normalizeRows = (res) => res?.rows || res?.data || [];
+const normalizeDict = (res) => (res?.data || []).map((item) => ({
+  label: item.dictLabel,
+  value: item.dictValue
+}));
+const dictText = (options, value) => options.find((item) => String(item.value) === String(value))?.label || value || '-';
+const actionText = (value) => dictText(actionOptions.value, value);
+const statusText = (value) => dictText(statusOptions.value, value);
+
+const resetObject = (target, source = {}) => {
+  Object.keys(target).forEach((key) => delete target[key]);
+  Object.assign(target, source);
+};
+
+const handleQuery = async () => {
+  loading.value = true;
+  try {
+    const res = await listFaultProcessLog(queryParams);
+    rows.value = normalizeRows(res);
+    pagination.total = res?.total || rows.value.length;
+    pagination.current = queryParams.pageNum;
+    pagination.pageSize = queryParams.pageSize;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const resetQuery = () => {
+  queryParams.pageNum = 1;
+  queryParams.processId = undefined;
+  queryParams.alarmEventId = undefined;
+  queryParams.actionType = undefined;
+  handleQuery();
+};
+
+const handleTableChange = (page) => {
+  queryParams.pageNum = page.current;
+  queryParams.pageSize = page.pageSize;
+  handleQuery();
+};
+
+const openDetail = (record) => {
+  resetObject(detail, record);
+  detailOpen.value = true;
+};
+
+const removeRecord = async (record) => {
+  await delFaultProcessLog(record.id);
+  message.success('删除成功');
+  handleQuery();
+};
+
+const loadOptions = async () => {
+  const [statusRes, actionRes] = await Promise.all([
+    getDicts('fault_process_status'),
+    getDicts('fault_process_action')
+  ]);
+  statusOptions.value = normalizeDict(statusRes);
+  actionOptions.value = normalizeDict(actionRes);
+};
+
+onMounted(async () => {
+  await loadOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.fault-page { padding: 16px; }
+.section { margin-top: 16px; }
+.query-select { width: 160px; }
+.query-number { width: 160px; }
+.danger-link { color: #ff4d4f; }
+</style>