Przeglądaj źródła

[Feat 0000] 添加问题反馈系列页面

houzekong 3 miesięcy temu
rodzic
commit
fdef719c6a

+ 536 - 0
src/views/dashboard/basicInfo/problemReport/components/ProblemReportModal.vue

@@ -0,0 +1,536 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="registerModal"
+    @ok="handleSubmit"
+    :title="getTitle"
+    width="900px"
+    :min-height="600"
+    :max-height="1000"
+    scrollable
+    centered
+    destroyOnClose
+    :bodyStyle="{ padding: '20px' }"
+  >
+    <a-form ref="formRef" :model="formData" :rules="formRules" layout="horizontal" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+      <!-- 查看模式 -->
+      <div class="que-container" v-if="mode === 'view'">
+        <div class="que-status">
+          <div>矿井状态:</div>
+          <div>
+            <span :class="getStatusClass(currentRecord?.mineLinkStatus)" class="status-dot">
+              {{ getStatusText(currentRecord?.mineLinkStatus) }}
+            </span>
+          </div>
+        </div>
+        <div class="que-item" v-for="(item, index) in queList" :key="index">
+          <div class="que-details">
+            <div class="que-field">
+              <span class="que-value que-goafName">{{ item.goafName || '-' }}</span>
+            </div>
+            <div class="que-field time-field">
+              <span class="que-label">时间:</span>
+              <span class="que-value">{{ formatDate(item.startTime) || '-' }}</span>
+              <span class="time-separator">-----</span>
+              <span class="que-value">{{ formatDate(item.endTime) || '-' }}</span>
+            </div>
+            <div class="que-field">
+              <span class="que-label">问题描述:</span>
+              <span class="que-value">{{ item.queCon || '-' }}</span>
+            </div>
+            <!-- <div class="que-field">
+              <span class="que-label">参数:</span>
+              <span class="que-value">{{ (item.param || '-').replace(/,/g, ' ') }}</span>
+            </div> -->
+          </div>
+        </div>
+      </div>
+
+      <!-- 编辑/新增模式 -->
+      <div v-else class="edit-container">
+        <!-- 动态渲染topFormSchema字段(编辑/新增模式) -->
+        <div class="mine-base-info" v-if="mode === 'add'">
+          <a-form-item v-for="schema in topFormSchema" :key="schema.field" :name="schema.field" :label="schema.label" :rules="schema.rules">
+            <component
+              :is="getComponent(schema.component)"
+              v-model:value="formData[schema.field]"
+              v-bind="schema.componentProps"
+              :placeholder="`请输入${schema.label}`"
+              style="width: 100%"
+            />
+          </a-form-item>
+        </div>
+
+        <!-- 问题项编辑区 -->
+        <div class="que-item" v-for="(item, index) in queList" :key="item.orderNum || index">
+          <div class="que-index">问题 {{ index + 1 }}</div>
+          <div class="edit-fields">
+            <a-form-item
+              v-for="schema in formSchema"
+              :key="`${schema.field}-${index}`"
+              :name="[`queList`, index, schema.field]"
+              :label="schema.label"
+              :rules="schema.rules"
+            >
+              <component
+                :is="getComponent(schema.component)"
+                v-model:value="item[schema.field]"
+                v-bind="schema.componentProps"
+                :placeholder="`请输入${schema.label}`"
+                style="width: 100%"
+              />
+            </a-form-item>
+            <div class="form-actions">
+              <a-button type="text" danger @click="removeItem(index)" :disabled="queList.length <= 1"> 删除 </a-button>
+            </div>
+          </div>
+        </div>
+
+        <a-button type="dashed" class="add-btn" @click="addNewItem"> <plus-outlined /> 新增问题 </a-button>
+      </div>
+    </a-form>
+  </BasicModal>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, reactive, watch } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { formSchema, topFormSchema } from '../problemReport.data';
+  import { Select, Input, DatePicker, message } from 'ant-design-vue';
+  import { PlusOutlined } from '@ant-design/icons-vue';
+  import dayjs, { Dayjs } from 'dayjs';
+  import MineCascader from '/@/components/Form/src/jeecg/components/MineCascader/MineCascader.vue';
+  import type { FormInstance, RuleObject } from 'ant-design-vue/es/form';
+  import { JEditor } from '/@/components/Form';
+
+  // 组件映射表
+  const componentMap = {
+    Input,
+    DatePicker,
+    Select,
+    InputTextArea: Input.TextArea,
+    MineCascader,
+    JEditor,
+  };
+
+  // 定义事件发射
+  const emit = defineEmits(['success']);
+
+  // 模态框模式:查看/编辑/新增
+  const mode = ref<'view' | 'edit' | 'add'>('view');
+  // 当前记录数据
+  const currentRecord = ref<any>({
+    mineCode: '',
+    mineLinkStatus: '0',
+    queJson: [],
+    isOk: false,
+    updateTime: '',
+    createTime: '',
+  });
+  // 问题列表
+  const queList = ref<any[]>([]);
+
+  // 表单实例(用于校验)
+  const formRef = ref<FormInstance>();
+  // 表单数据聚合(适配Form组件的model)
+  const formData = reactive({
+    mineCode: '',
+    queList: queList.value,
+  });
+
+  // 合并表单规则(从schema中提取)
+  const formRules = reactive({
+    mineCode: topFormSchema.find((item) => item.field === 'mineCode')?.rules || [],
+    queList: {
+      type: 'array',
+      required: true,
+      validator: (rule: RuleObject, value: any[], callback: Function) => {
+        // 校验每个问题项
+        if (value.length === 0) {
+          callback(new Error('至少需要填写一条问题信息'));
+          return;
+        }
+        // 逐个校验问题项的必填字段
+        for (let i = 0; i < value.length; i++) {
+          const item = value[i];
+          // 校验工作面名称
+          if (!item.goafName) {
+            callback(new Error(`第${i + 1}条问题:工作面名称不能为空`));
+            return;
+          }
+          // 校验问题描述
+          if (!item.queCon) {
+            callback(new Error(`第${i + 1}条问题:问题描述不能为空`));
+            return;
+          }
+          // 校验开始时间
+          if (!item.startTime) {
+            callback(new Error(`第${i + 1}条问题:开始时间不能为空`));
+            return;
+          }
+          // 校验结束时间
+          if (!item.endTime) {
+            callback(new Error(`第${i + 1}条问题:结束时间不能为空`));
+            return;
+          }
+          // 校验结束时间不能早于开始时间
+          if (dayjs(item.endTime).isBefore(dayjs(item.startTime))) {
+            callback(new Error(`第${i + 1}条问题:结束时间不能早于开始时间`));
+            return;
+          }
+          // 校验参数
+          if (!item.param) {
+            callback(new Error(`第${i + 1}条问题:参数不能为空`));
+            return;
+          }
+        }
+        callback();
+      },
+    },
+  });
+
+  // 监听queList变化,同步到formData
+  watch(
+    queList,
+    (newVal) => {
+      formData.queList = JSON.parse(JSON.stringify(newVal));
+    },
+    { deep: true, immediate: true }
+  );
+
+  // 监听currentRecord变化,同步mineCode到formData
+  watch(
+    () => currentRecord.value.mineCode,
+    (newVal) => {
+      formData.mineCode = newVal;
+    },
+    { immediate: true }
+  );
+
+  // 根据组件名获取对应组件
+  const getComponent = (componentName: string) => {
+    return componentMap[componentName as keyof typeof componentMap];
+  };
+
+  // 日期格式化方法
+  const formatDate = (date: string | Dayjs | null) => {
+    if (!date) return '';
+    return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
+  };
+
+  // 根据状态值获取显示文本(在线/离线)
+  const getStatusText = (status: string | number) => {
+    return status === '1' || status === 1 ? '在线' : '离线';
+  };
+
+  // 根据状态值获取样式类
+  const getStatusClass = (status: string | number) => {
+    return status === '1' || status === 1 ? 'status-online' : 'status-offline';
+  };
+
+  // 注册模态框并初始化数据
+  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+    setModalProps({ confirmLoading: false });
+
+    // 接收模式参数
+    mode.value = data?.mode || 'view';
+    // 初始化当前记录
+    currentRecord.value = data?.record
+      ? JSON.parse(JSON.stringify(data.record))
+      : {
+          mineCode: '',
+          mineLinkStatus: '0',
+          queJson: [],
+          isOk: false,
+          updateTime: '',
+          createTime: '',
+        };
+    // 同步mineCode到formData
+    formData.mineCode = currentRecord.value.mineCode;
+
+    // 初始化问题列表
+    if (mode.value === 'add') {
+      // 新增模式:初始化一条空数据
+      queList.value = [
+        {
+          orderNum: '1',
+          goafName: '',
+          queCon: '',
+          startTime: null,
+          endTime: null,
+          param: '',
+        },
+      ];
+      formData.queList = JSON.parse(JSON.stringify(queList.value));
+      if (formRef.value) {
+        formRef.value.resetFields();
+      }
+    } else {
+      // 编辑/查看模式:解析已有问题数据
+      try {
+        const rawData = currentRecord.value?.queJson
+          ? typeof currentRecord.value.queJson === 'string'
+            ? currentRecord.value.queJson.trim()
+              ? JSON.parse(currentRecord.value.queJson)
+              : [] // 兼容空字符串
+            : currentRecord.value.queJson
+          : [];
+        // 兼容日期格式,避免转换失败
+        queList.value = rawData.map((item: any) => ({
+          ...item,
+          startTime: item.startTime ? dayjs(item.startTime, ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD']) : null,
+          endTime: item.endTime ? dayjs(item.endTime, ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD']) : null,
+        }));
+        // 深拷贝同步到formData,切断引用
+        formData.queList = JSON.parse(JSON.stringify(queList.value));
+      } catch (error) {
+        console.error('解析问题数据失败:', error);
+        message.warning('问题数据格式异常,将显示原始数据');
+        // 保留原始queJson,而非重置为空数据
+        queList.value = currentRecord.value?.queJson ? (typeof currentRecord.value.queJson === 'string' ? [] : currentRecord.value.queJson) : [];
+        formData.queList = JSON.parse(JSON.stringify(queList.value));
+      }
+    }
+    // 重置表单校验状态
+    if (formRef.value) {
+      formRef.value.resetFields();
+    }
+  });
+
+  // 模态框标题计算属性
+  const getTitle = computed(() => {
+    const titleMap = {
+      view: '质量问题详情',
+      edit: '编辑质量问题',
+      add: '新增质量问题',
+    };
+    return titleMap[mode.value];
+  });
+
+  // 新增问题项
+  const addNewItem = () => {
+    const newItem = {
+      orderNum: (queList.value.length + 1).toString(),
+      goafName: '',
+      queCon: '',
+      startTime: null,
+      endTime: null,
+      param: '',
+    };
+    queList.value.push(newItem);
+  };
+
+  // 删除问题项
+  const removeItem = (index: number) => {
+    queList.value.splice(index, 1);
+    queList.value.forEach((item, i) => {
+      item.orderNum = (i + 1).toString();
+    });
+  };
+
+  // 提交表单处理
+  async function handleSubmit() {
+    try {
+      // 查看模式直接提交
+      if (mode.value === 'view') {
+        closeModal();
+        return;
+      }
+
+      // 1. 表单校验
+      if (!formRef.value) return;
+      const validateResult = await formRef.value.validate();
+      if (!validateResult) return;
+
+      setModalProps({ confirmLoading: true });
+      // 2. 处理问题列表数据(空日期字段置空,避免空字符串)
+      const submitQueList = queList.value.map((item) => ({
+        ...item,
+        startTime: item.startTime ? item.startTime.format('YYYY-MM-DD HH:mm:ss') : null,
+        endTime: item.endTime ? item.endTime.format('YYYY-MM-DD HH:mm:ss') : null,
+      }));
+      // 3. 构造完整提交数据
+      const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
+      const result = {
+        ...currentRecord.value,
+        mineCode: formData.mineCode,
+        queJson: JSON.stringify(submitQueList),
+        createTime: mode.value === 'add' ? now : currentRecord.value.createTime || now,
+        updateTime: now,
+        isOk: mode.value === 'add' ? false : currentRecord.value.isOk,
+      };
+      console.log('最终提交数据:', result);
+      emit('success', result);
+      closeModal();
+    } catch (error: any) {
+      console.error('提交失败:', error);
+      // 显示校验错误提示
+      message.error(error.message || '表单校验失败,请检查必填项');
+    } finally {
+      setModalProps({ confirmLoading: false });
+    }
+  }
+</script>
+
+<style scoped>
+  .que-container {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .que-status {
+    display: flex;
+    width: 100%;
+    background-color: #f8f9fc;
+    padding: 8px 16px;
+    align-items: center;
+    border: 1px solid #cad2e0;
+    border-radius: 5px;
+  }
+
+  .mine-info {
+    padding: 12px 16px;
+    border: 1px solid #cad2e0;
+    border-radius: 5px;
+    background-color: #f8f9fc;
+  }
+
+  .mine-field {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 8px;
+  }
+
+  .status-dot {
+    position: relative;
+    padding-left: 12px;
+    font-weight: 500;
+  }
+
+  .status-dot::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background-color: inherit;
+  }
+
+  .status-online {
+    color: #10952c;
+    font-weight: 500;
+  }
+
+  .status-offline {
+    color: #f5222d;
+    font-weight: 500;
+  }
+
+  .status-online.status-dot::before {
+    background-color: #10952c;
+  }
+
+  .status-offline.status-dot::before {
+    background-color: #f5222d;
+  }
+
+  .que-item {
+    padding: 8px 16px;
+    border: 1px solid #cad2e0;
+    border-radius: 8px;
+    background-color: #f8f9fc;
+  }
+
+  .que-index {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1890ff;
+    margin-bottom: 12px;
+    padding-bottom: 8px;
+    border-bottom: 1px dashed #e8e8e8;
+  }
+
+  .que-details {
+    width: 100%;
+  }
+
+  .que-field {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    min-width: 200px;
+    margin-bottom: 10px;
+  }
+
+  .que-label {
+    font-size: 16px;
+    color: #868789;
+    white-space: nowrap;
+  }
+
+  .que-value {
+    font-size: 16px;
+    color: #838486;
+    word-break: break-all;
+  }
+
+  .que-goafName {
+    color: #4c4c4e;
+    font-size: 20px;
+  }
+
+  .time-field {
+    flex: 1;
+    min-width: 420px;
+  }
+
+  .time-separator {
+    color: #999;
+    margin: 0 8px;
+  }
+
+  .edit-container {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .mine-base-info {
+    padding: 12px 16px;
+    border: 1px solid #cad2e0;
+    border-radius: 5px;
+    background-color: #f8f9fc;
+  }
+
+  .edit-fields {
+    width: 100%;
+  }
+
+  .form-item {
+    display: flex;
+    margin-bottom: 10px;
+    align-items: center;
+  }
+
+  .form-label {
+    width: 20%;
+    color: #666;
+    font-size: 14px;
+  }
+
+  .add-btn {
+    margin-top: 8px;
+    width: 100%;
+  }
+
+  .form-actions {
+    display: flex;
+    justify-content: end;
+    margin-bottom: 8px;
+  }
+</style>

+ 564 - 0
src/views/dashboard/basicInfo/problemReport/index.vue

@@ -0,0 +1,564 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <!-- Tab标签页 -->
+  <Tabs v-model:activeKey="activeKey" class="common-page-tabs" type="line" @change="handleTabChange">
+    <TabPane key="unresolved" tab="未解决">
+      <BasicTable style="padding: 0" @register="registerUnresolvedTable">
+        <template #resetBefore>
+          <a-button type="primary" class="ml-8px" preIcon="mdi:page-next-outline" @click="handleOpenModal({}, 'add')"> 新增问题 </a-button>
+          <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="handleOpenExportModal('unresolved')"> 导出 </a-button>
+        </template>
+        <template #queJson="{ record }">
+          <div style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: space-between">
+            <span style="white-space: pre-line; word-wrap: break-word; line-height: 1.6; display: block; text-align: left; padding: 2px 4px">
+              {{ record?.queJson ? formatQueJson(record.queJson) : '' }}
+            </span>
+            <button @click="record && handleOpenModal(record, 'view')" class="action-btn" title="查看问题详情">
+              <SvgIcon name="view" />
+            </button>
+          </div>
+        </template>
+        <template #action="{ record }">
+          <button @click="handleOpenModal(record, 'edit')" class="action-btn" title="编辑该问题">
+            <SvgIcon name="edit" />
+          </button>
+          <!-- 删除按钮 -->
+          <Popconfirm
+            title="删除确认"
+            description="是否确认删除?"
+            okText="确认"
+            cancelText="取消"
+            @confirm="handleDeleteRecord(record)"
+            @cancel="handleCancel"
+            placement="top"
+          >
+            <button class="action-btn" title="删除该问题">
+              <SvgIcon name="delete" />
+            </button>
+          </Popconfirm>
+          <Popconfirm
+            title="标记已解决确认"
+            :description="getResolveDesc(record)"
+            okText="确认"
+            cancelText="取消"
+            @confirm="handleOKRecord(record)"
+            @cancel="handleCancel"
+            placement="top"
+          >
+            <button class="action-btn" title="标记该问题为已解决">
+              <SvgIcon name="resolved" />
+            </button>
+          </Popconfirm>
+          <button @click="handleGoToPage(record)" class="action-btn" title="监测详情">
+            <SvgIcon name="details" />
+          </button>
+        </template>
+      </BasicTable>
+    </TabPane>
+    <TabPane key="resolved" tab="已解决">
+      <BasicTable style="padding: 0" @register="registerResolvedTable">
+        <template #resetBefore>
+          <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="handleOpenExportModal('resolved')"> 导出 </a-button>
+        </template>
+        <template #queJson="{ record }">
+          <div style="display: flex; align-items: center; gap: 8px; width: 100%">
+            <span style="flex: 1; text-align: center">
+              {{ record?.queJson ? formatQueJson(record.queJson) : '' }}
+            </span>
+            <button @click="record && handleOpenModal(record, 'view')" class="action-btn" title="查看问题详情">
+              <SvgIcon name="view" />
+            </button>
+          </div>
+        </template>
+        <template #action="{ record }">
+          <button @click="handleOpenModal(record, 'view')" class="action-btn" title="问题详情">
+            <SvgIcon name="details" />
+          </button>
+        </template>
+      </BasicTable>
+    </TabPane>
+  </Tabs>
+
+  <!-- 处理弹框 -->
+  <ProblemReportModal @register="registerModal" @success="handleModalSuccess" />
+
+  <!-- 导出时间选择Modal-->
+  <Modal
+    v-model:open="exportModalVisible"
+    title="选择导出时间范围"
+    :width="600"
+    @ok="handleExportConfirm"
+    @cancel="handleExportCancel"
+    okText="确认导出"
+    cancelText="取消"
+    centered
+    :confirm-loading="exportConfirmLoading"
+    destroyOnClose
+    :bodyStyle="{ padding: '20px' }"
+  >
+    <a-form
+      ref="exportFormRef"
+      :model="exportFormData"
+      :rules="exportFormRules"
+      layout="horizontal"
+      :label-col="{ span: 6 }"
+      :wrapper-col="{ span: 18 }"
+    >
+      <a-form-item v-for="schema in exportFormSchema" :key="schema.field" :name="schema.field" :label="schema.label" :rules="schema.rules">
+        <component
+          :is="getExportFormComponent(schema.component)"
+          v-model:value="exportFormData[schema.field]"
+          v-bind="schema.componentProps"
+          :placeholder="`请选择${schema.label}`"
+          style="width: 100%"
+        />
+      </a-form-item>
+    </a-form>
+  </Modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, nextTick, computed, onMounted, reactive } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { BasicTable } from '/@/components/Table';
+  import { useModal } from '/@/components/Modal';
+  import { Tabs, TabPane, Popconfirm, message, Modal, DatePicker } from 'ant-design-vue';
+  import type { FormInstance } from 'ant-design-vue/es/form';
+  import ProblemReportModal from './components/ProblemReportModal.vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { getColumns, getSearchFormSchema, type ProductionStatusMap, exportFormSchema } from './problemReport.data';
+  import { getDataQuaQueList, addDataQuaQue, deleteDataQuaQue, editDataQuaQue } from '../basicInfo.api';
+  import { findNode } from '/@/utils/helper/treeHelper';
+  import { useMineDepartmentStore } from '/@/store/modules/mine';
+  import { getDictItemsByCode } from '/@/utils/dict';
+  import dayjs, { Dayjs } from 'dayjs';
+  import { useListPage } from '/@/hooks/system/useListPage';
+
+  // 路由实例
+  const router = useRouter();
+  // 实例化矿井Store
+  const mineStore = useMineDepartmentStore();
+  // 响应式数据
+  const activeKey = ref('unresolved'); // 激活的Tab键
+  const pageMode = ref('add');
+
+  // 导出Modal相关状态
+  const exportModalVisible = ref(false);
+  const exportFormRef = ref<FormInstance>();
+  const exportConfirmLoading = ref(false);
+  const exportType = ref<'resolved' | 'unresolved'>('unresolved'); // 记录当前导出类型
+  const exportConfig = ref({
+    url: '/province/dataQuaQue/exportDataQuaQueList',
+    name: '问题反馈',
+    params: {
+      isOk: 0,
+      startTime: '',
+      endTime: '',
+    },
+  });
+
+  // 导出表单数据和规则(参考DataQualityModal写法)
+  const exportFormData = reactive({
+    startTime: null as Dayjs | null,
+    endTime: null as Dayjs | null,
+  });
+
+  // 提取导出表单规则(从exportFormSchema)
+  const exportFormRules = reactive({
+    startTime: exportFormSchema.find((item) => item.field === 'startTime')?.rules || [],
+    endTime: exportFormSchema.find((item) => item.field === 'endTime')?.rules || [],
+  });
+
+  // 导出表单组件映射
+  const exportFormComponentMap = {
+    DatePicker,
+  };
+
+  // 获取导出表单组件
+  const getExportFormComponent = (componentName: string) => {
+    return exportFormComponentMap[componentName as keyof typeof exportFormComponentMap];
+  };
+
+  // ========== 定义动态状态映射/下拉选项 ==========
+  // 1. 动态生产状态映射(key: 状态value,value: 包含label/color的配置)
+  const dynamicProductionStatusMap = ref<ProductionStatusMap>({});
+  // 2. 动态下拉选项(供搜索表单使用)
+  const dynamicProductionStatusOptions = ref<{ label: string; value: string | number }[]>([]);
+
+  // 3. 颜色分配规则(可根据业务灵活调整)
+  const getStatusColor = (statusText: string) => {
+    if (statusText.includes('正常生产')) return 'green'; // 正常生产 → 绿色
+    if (statusText.includes('拟建矿井'))
+      return 'blue'; // 拟建矿井 → 蓝色
+    else return 'red'; // 停产/停建/关闭/整改/责令 → 红色
+  };
+
+  // 4. 从接口获取生产状态列表并生成动态映射/下拉选项
+  const fetchProductionStatus = async () => {
+    try {
+      // 调用接口获取状态列表
+      const statusList = await getDictItemsByCode('mineProStatus');
+      if (!Array.isArray(statusList)) return;
+
+      // 生成动态映射和下拉选项
+      const statusMap: ProductionStatusMap = {};
+      const statusOptions: { label: string; value: string | number }[] = [];
+
+      statusList.forEach((item) => {
+        const value = item.value; // 接口返回的value(数字/字符串)
+        const label = item.text || item.label; // 接口返回的文本
+        const color = getStatusColor(label); // 按规则分配颜色
+
+        // 填充映射表
+        statusMap[value] = { label, value, color };
+        // 填充下拉选项
+        statusOptions.push({ label, value });
+      });
+
+      // 赋值到响应式变量
+      dynamicProductionStatusMap.value = statusMap;
+      dynamicProductionStatusOptions.value = statusOptions;
+
+      // 刷新表格(确保表格使用最新的映射)
+      await safeReloadActiveTable();
+    } catch (error) {
+      console.error('获取生产状态列表失败:', error);
+      message.error('生产状态数据加载失败');
+    }
+  };
+
+  // 生成动态列和搜索表单配置
+  const columns = computed(() => getColumns(dynamicProductionStatusMap));
+  const searchFormSchema = computed(() => getSearchFormSchema(dynamicProductionStatusOptions));
+
+  // 未解决表格注册
+  const { tableContext: tableContextA, onExportXls: onExportXlsA } = useListPage({
+    tableProps: {
+      api: async (params: any) => {
+        return await getDataQuaQueList({ ...params, isOk: false });
+      },
+      columns: columns, // 绑定动态列
+      formConfig: {
+        labelWidth: 120,
+        schemas: searchFormSchema.value,
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+        actionColOptions: { span: 6 },
+      },
+      useSearchForm: true,
+      pagination: true,
+      showIndexColumn: false,
+      indexColumnProps: {
+        title: '序号',
+      },
+      actionColumn: {
+        width: 200,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+      },
+      immediate: false, // 先不立即加载,等状态数据获取后再加载
+    },
+    exportConfig: exportConfig.value,
+  });
+
+  const [registerUnresolvedTable, { reload: reloadUnresolved }] = tableContextA;
+
+  // 已解决表格注册
+  const { tableContext: tableContextB, onExportXls: onExportXlsB } = useListPage({
+    tableProps: {
+      api: async (params: any) => {
+        return await getDataQuaQueList({ ...params, isOk: true });
+      },
+      columns: columns,
+      formConfig: {
+        labelWidth: 120,
+        schemas: searchFormSchema.value,
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      useSearchForm: true,
+      pagination: true,
+      striped: true,
+      showIndexColumn: false,
+      indexColumnProps: {
+        title: '序号',
+      },
+      showActionColumn: false,
+      // actionColumn: {
+      //   width: 60,
+      //   title: '操作',
+      //   dataIndex: 'action',
+      //   slots: { customRender: 'action' },
+      // },
+      immediate: false, // 先不立即加载
+    },
+    exportConfig: exportConfig.value,
+  });
+
+  const [registerResolvedTable, { reload: reloadResolved }] = tableContextB;
+
+  // 弹框注册
+  const [registerModal, { openModal }] = useModal();
+
+  /**
+   * 打开导出时间选择Modal
+   * @param type 导出类型:resolved 或 unresolved
+   */
+  function handleOpenExportModal(type: 'resolved' | 'unresolved') {
+    exportType.value = type;
+    exportModalVisible.value = true;
+    // 打开Modal时重置表单
+    nextTick(() => {
+      exportFormRef.value?.resetFields();
+      exportFormData.startTime = null;
+      exportFormData.endTime = null;
+    });
+  }
+
+  /**
+   * 导出Modal确认处理
+   */
+  async function handleExportConfirm() {
+    try {
+      exportConfirmLoading.value = true;
+      // 表单校验(参考DataQualityModal的校验方式)
+      if (!exportFormRef.value) return;
+      const validateResult = await exportFormRef.value.validate();
+      if (!validateResult) return;
+
+      // 格式化时间
+      const startTime = exportFormData.startTime?.format('YYYY-MM-DD HH:mm:ss') || '';
+      const endTime = exportFormData.endTime?.format('YYYY-MM-DD HH:mm:ss') || '';
+
+      // 校验时间逻辑(结束时间不能早于开始时间)
+      if (exportFormData.startTime && exportFormData.endTime && dayjs(endTime).isBefore(dayjs(startTime))) {
+        message.error('结束时间不能早于开始时间');
+        return;
+      }
+
+      // 根据类型选择对应的exportConfig和导出方法
+      exportConfig.value.params = {
+        isOk: exportType.value === 'resolved' ? 1 : 0,
+        startTime,
+        endTime,
+      };
+
+      if (exportType.value === 'unresolved') {
+        await onExportXlsA();
+      } else {
+        await onExportXlsB();
+      }
+
+      // 关闭Modal
+      exportModalVisible.value = false;
+      // message.success('导出请求已发送,请注意查收文件');
+    } catch (error: any) {
+      console.error('导出失败:', error);
+      message.error(error.message || '导出失败,请检查表单填写是否正确');
+    } finally {
+      exportConfirmLoading.value = false;
+    }
+  }
+
+  /**
+   * 导出Modal取消处理
+   */
+  function handleExportCancel() {
+    exportModalVisible.value = false;
+    // 取消时重置表单
+    exportFormRef.value?.resetFields();
+    exportFormData.startTime = null;
+    exportFormData.endTime = null;
+  }
+
+  // 解析queJson并拼接orderNum+queCon的辅助函数
+  function formatQueJson(queJsonStr: string) {
+    // 空值处理
+    if (!queJsonStr) return '无问题反馈';
+    try {
+      const queList = JSON.parse(queJsonStr);
+      // 非数组格式处理
+      if (!Array.isArray(queList)) return '问题格式异常';
+      // 空数组处理
+      if (queList.length === 0) return '无问题反馈';
+      return queList
+        .map((item) => {
+          const goafName = item.goafName;
+          const queCon = item.queCon || '无描述';
+          return `<${goafName}工作面老空区永久密闭监测>存在的问题:${queCon}`;
+        })
+        .join('\n'); // 多个问题分行显示
+    } catch (error) {
+      console.error('解析问题反馈JSON失败:', error);
+      return '问题数据解析失败';
+    }
+  }
+
+  // 安全重载当前激活的表格
+  async function safeReloadActiveTable() {
+    await nextTick();
+    if (activeKey.value === 'unresolved') {
+      try {
+        await reloadUnresolved();
+      } catch (e) {
+        console.warn('未解决表格重载失败:', e);
+      }
+    } else {
+      try {
+        await reloadResolved();
+      } catch (e) {
+        console.warn('已解决表格重载失败:', e);
+      }
+    }
+  }
+  // 按需重载双表格(先注释掉)
+  // async function reloadBothTableSafely() {
+  //   await nextTick();
+  //   // 未解决表格:await + try/catch 捕获异步错误
+  //   try {
+  //     await reloadUnresolved();
+  //   } catch (e) {
+  //     console.warn('未解决表格暂未就绪,跳过重载:', e);
+  //   }
+  //   // 已解决表格:同理,一个报错不影响另一个
+  //   try {
+  //     await reloadResolved();
+  //   } catch (e) {
+  //     console.warn('已解决表格暂未就绪,跳过重载:', e);
+  //   }
+  // }
+
+  // tabs切换事件
+  async function handleTabChange(key: string) {
+    activeKey.value = key;
+    await safeReloadActiveTable();
+  }
+
+  /**
+   * 打开弹框函数
+   * @param result 弹框数据
+   */
+  function handleOpenModal(record: any, mode: 'view' | 'edit' | 'add' = 'view') {
+    pageMode.value = mode;
+    openModal(true, { record, mode });
+  }
+
+  /**
+   * 弹框结果处理函数
+   * @param result 弹框数据
+   */
+  async function handleModalSuccess(result: any) {
+    try {
+      if (pageMode.value === 'add') {
+        await addDataQuaQue(result);
+      } else if (pageMode.value === 'edit') {
+        await editDataQuaQue(result);
+      }
+      await safeReloadActiveTable();
+    } catch (error) {
+      console.error('操作失败:', error);
+    }
+  }
+  /**
+   * 通用页面跳转方法
+   * @param record 当前行数据
+   * @param path 目标路径(树形结构所在页面的路由地址)
+   */
+  async function handleGoToPage(record: any) {
+    try {
+      const mineCode = record.mineCode;
+      const targetNode = findNode(mineStore.getDepartTree, (item) => item.id === mineCode, { id: 'id', pid: 'parentId', children: 'childDepart' });
+
+      let minePath = '';
+      if (targetNode) {
+        minePath = targetNode.parentId;
+      } else {
+        message.warning(`未找到矿码【${mineCode}】对应的矿井节点`);
+        return;
+      }
+
+      // 跳转页面(可携带拼接后的矿名/路径等参数)
+      router.push({ path: `/sealed/${minePath}`, query: { mineCode } });
+    } catch (error) {
+      console.error('矿节点定位失败:', error);
+      message.error('矿节点定位失败,请稍后重试');
+    }
+  }
+
+  /**
+   * 气泡取消按钮通用回调
+   */
+  function handleCancel() {
+    // 取消操作,无逻辑(仅关闭气泡)
+  }
+
+  /**
+   * 生成已解决气泡提示文案:XX矿井XX问题是否解决了吗?
+   */
+  const getResolveDesc = computed(() => (record: any) => {
+    const mineName = record.mineName || '该';
+    return `是否确认${mineName}矿井问题已解决?`;
+  });
+
+  /**
+   * 删除记录方法
+   * @param record 当前行数据
+   */
+  async function handleDeleteRecord(record: any) {
+    try {
+      await deleteDataQuaQue({ id: record.id });
+      await nextTick();
+      await safeReloadActiveTable();
+    } catch (error) {
+      console.error('删除失败:', error);
+    }
+  }
+
+  /**
+   * 将记录改为已处理
+   * @param record 当前行数据
+   */
+  async function handleOKRecord(record: any) {
+    const copyRecord = {
+      ...record,
+      isOk: true,
+      updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    };
+    try {
+      await editDataQuaQue(copyRecord);
+      await safeReloadActiveTable();
+      message.success('标记为已解决成功');
+    } catch (error) {
+      console.error('操作失败:', error);
+      message.error('操作失败,请稍后重试');
+    }
+  }
+
+  // ========== 初始化:先获取状态数据,再加载表格 ==========
+  onMounted(async () => {
+    await fetchProductionStatus(); // 先获取动态状态数据
+    await safeReloadActiveTable();
+  });
+</script>
+
+<style scoped lang="less">
+  .form-part {
+    padding: 12px 10px 6px 10px;
+    margin-bottom: 8px;
+    background-color: @white;
+    border-radius: 2px;
+  }
+  .add-button {
+    margin-bottom: 10px;
+  }
+  .action-btn {
+    height: 30px;
+    cursor: pointer;
+    margin-right: 10px;
+
+    &:last-child {
+      margin-right: 0;
+    }
+  }
+</style>

+ 244 - 0
src/views/dashboard/basicInfo/problemReport/problemReport.data.ts

@@ -0,0 +1,244 @@
+import { BasicColumn, FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { Ref } from 'vue';
+// import dayjs from 'dayjs';
+
+// 1. 颜色映射(固定规则,可根据业务调整)
+export const colorHexMap: Record<string, string> = {
+  blue: '#1890ff',
+  green: '#208840',
+  gold: '#faad14',
+  red: '#f5222d',
+  gray: '#8c8c8c',
+  black: '#000000',
+};
+
+// 2. 定义动态映射的类型(供外部传入)
+export type ProductionStatusMap = Record<
+  string | number,
+  {
+    label: string;
+    value: number | string;
+    color: string;
+  }
+>;
+
+// 3. 生成表格列(支持传入动态映射)
+export function getColumns(dynamicStatusMap: Ref<ProductionStatusMap>) {
+  const columns: BasicColumn[] = [
+    {
+      title: '煤矿名称',
+      dataIndex: 'mineName',
+      width: 250,
+    },
+    {
+      title: '煤矿简称',
+      dataIndex: 'mineNameAbbr',
+      width: 150,
+      ifShow: false,
+    },
+    {
+      title: '生产状态',
+      dataIndex: 'mineProStatus',
+      width: 120,
+      customRender: ({ record }) => {
+        // 空值/异常值处理
+        const status = record.mineProStatus;
+        // 从动态映射中取值,兜底未知状态
+        const { label, color } = dynamicStatusMap.value[status] || {
+          label: '-',
+        };
+        return h(
+          'span',
+          {
+            style: { color: colorHexMap[color] },
+          },
+          label
+        );
+      },
+    },
+    {
+      title: '在线状态',
+      dataIndex: 'mineLinkStatus',
+      width: 100,
+      customRender: ({ record }) => {
+        const status = record.mineLinkStatus;
+        if (status === undefined || status === null) {
+          return h('span', { style: { color: colorHexMap.black } }, '-');
+        }
+        const text = status === 1 ? '在线' : '离线';
+        const textColor = status === 1 ? colorHexMap.green : colorHexMap.red;
+        return h('span', { style: { color: textColor } }, text);
+      },
+    },
+    {
+      title: '质量问题详情',
+      dataIndex: 'queJson',
+      width: 400,
+      ellipsis: true,
+      slots: { customRender: 'queJson' },
+    },
+    {
+      title: '当前状态',
+      dataIndex: 'isOk',
+      width: 100,
+      customRender: ({ record }) => {
+        const status = record.isOk;
+        const text = status ? '已解决' : '未解决';
+        const textColor = status ? colorHexMap.green : colorHexMap.red;
+        return h('span', { style: { color: textColor } }, text);
+      },
+    },
+    {
+      title: '处理时间',
+      dataIndex: 'updateTime',
+      width: 180,
+    },
+  ];
+  return columns;
+}
+
+// 4. 查询表单配置(下拉框改为动态options)
+export function getSearchFormSchema(dynamicStatusOptions: Ref<{ label: string; value: string | number }[]>) {
+  const searchFormSchema: FormSchema[] = [
+    {
+      field: 'mineCodeList',
+      label: '煤矿名称',
+      component: 'MineCascader',
+      colProps: { span: 6 },
+      groupName: '常规查询',
+    },
+    // 启用生产状态下拉框(动态options)
+    {
+      field: 'productionStatus',
+      label: '生产状态',
+      component: 'Select',
+      componentProps: {
+        // 动态绑定下拉选项
+        options: dynamicStatusOptions,
+      },
+      colProps: { span: 6 },
+      groupName: '常规查询',
+      show: false,
+    },
+    {
+      field: 'mineLinkStatus',
+      label: '在线状态',
+      component: 'Select',
+      componentProps: {
+        options: [
+          { label: '离线', value: '0' },
+          { label: '在线', value: '1' },
+        ],
+      },
+      colProps: { span: 6 },
+      groupName: '常规查询',
+      show: false,
+    },
+  ];
+  return searchFormSchema;
+}
+export const topFormSchema: FormSchema[] = [
+  {
+    field: 'mineCode',
+    label: '密闭名称',
+    component: 'MineCascader',
+    componentProps: {
+      changeOnSelect: false,
+      initFromStore: false,
+      syncToStore: false,
+    },
+    required: true,
+    rules: [{ required: true, message: '请选择煤矿名称', trigger: 'change' }],
+  },
+];
+
+/** 弹框表单配置 */
+export const formSchema: FormSchema[] = [
+  {
+    field: 'goafName',
+    label: '不完整类型',
+    component: 'Input',
+    required: true,
+    rules: [
+      { required: true, message: '请输入不完整类型', trigger: 'blur' },
+      { max: 100, message: '不完整类型长度不能超过100个字符', trigger: 'blur' },
+    ],
+  },
+  {
+    field: 'startTime',
+    label: '开始时间',
+    component: 'DatePicker',
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+    },
+    required: true,
+    rules: [{ required: true, message: '请选择开始时间', trigger: 'blur' }],
+  },
+  {
+    field: 'endTime',
+    label: '结束时间',
+    component: 'DatePicker',
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+    },
+    required: true,
+    rules: [{ required: true, message: '请选择结束时间', trigger: 'blur' }],
+  },
+  {
+    field: 'queCon',
+    label: '问题描述',
+    component: 'InputTextArea',
+    componentProps: {
+      rows: 4,
+      maxlength: 500,
+      showCount: true,
+    },
+    // componentProps: {
+    //   //是否禁用
+    //   disabled: false,
+    // },
+    required: true,
+    rules: [
+      { required: true, message: '请输入问题描述', trigger: 'blur' },
+      { max: 500, message: '问题描述长度不能超过500个字符', trigger: 'blur' },
+    ],
+  },
+  // {
+  //   field: 'param',
+  //   label: '参数',
+  //   component: 'Input',
+  //   required: true,
+  //   rules: [
+  //     { required: true, message: '请输入参数', trigger: 'blur' },
+  //     { max: 200, message: '参数长度不能超过200个字符', trigger: 'blur' },
+  //   ],
+  // },
+];
+
+export const exportFormSchema: FormSchema[] = [
+  {
+    field: 'startTime',
+    label: '开始时间',
+    component: 'DatePicker',
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+    },
+    required: true,
+    rules: [{ required: true, message: '请选择开始时间', trigger: 'blur' }],
+  },
+  {
+    field: 'endTime',
+    label: '结束时间',
+    component: 'DatePicker',
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+    },
+    required: true,
+    rules: [{ required: true, message: '请选择结束时间', trigger: 'blur' }],
+  },
+];