Explorar o código

Merge branch 'question-alert' into 'dev'

feat: 报警问题数据

See merge request skyeye/skyeye_frontend/skyeye-admin!27
航飞 楼 hai 1 ano
pai
achega
5bf4827b21
Modificáronse 24 ficheiros con 7651 adicións e 4786 borrados
  1. 3 1
      .env.development
  2. 5885 4785
      pnpm-lock.yaml
  3. 50 0
      src/api/datamanagement/alert-default.ts
  4. 77 0
      src/api/datamanagement/alert-show.ts
  5. 25 0
      src/api/datamanagement/alert.ts
  6. BIN=BIN
      src/assets/images/alert/delete.png
  7. BIN=BIN
      src/assets/images/alert/edit.png
  8. BIN=BIN
      src/assets/images/alert/show-off.png
  9. BIN=BIN
      src/assets/images/alert/show-on.png
  10. BIN=BIN
      src/assets/images/alert/urgent-active.png
  11. BIN=BIN
      src/assets/images/alert/urgent.png
  12. 70 0
      src/views/datamanager/alertformdata/AlertformData.vue
  13. 22 0
      src/views/datamanager/alertformdata/components/common/AddDrawer.vue
  14. 148 0
      src/views/datamanager/alertformdata/components/common/AlertTable.vue
  15. 115 0
      src/views/datamanager/alertformdata/components/common/DetailDialog.vue
  16. 22 0
      src/views/datamanager/alertformdata/components/common/EditDrawer.vue
  17. 71 0
      src/views/datamanager/alertformdata/components/common/Pagination.vue
  18. 162 0
      src/views/datamanager/alertformdata/components/common/QueryForm.vue
  19. 181 0
      src/views/datamanager/alertformdata/components/common/QuestionFormBase.vue
  20. 48 0
      src/views/datamanager/alertformdata/components/common/constant.question.ts
  21. 349 0
      src/views/datamanager/alertformdata/components/default/Default.vue
  22. 313 0
      src/views/datamanager/alertformdata/components/show/Show.vue
  23. 58 0
      src/views/datamanager/alertformdata/hooks/useIssueType.ts
  24. 52 0
      src/views/datamanager/alertformdata/hooks/useWorkLocation.ts

+ 3 - 1
.env.development

@@ -19,7 +19,9 @@ VITE_DROP_CONSOLE = true
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.14.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.14.68/eye_api"],["/push_stream_host","http://192.168.14.68/push_stream_host"],["/skyeye-login","http://192.168.14.68/skyeye-login"],["/ws_api_bak","ws://192.168.14.68/ws_api_bak"]]
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.13.68/eye_api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"]]
 # 中建材 staff
-VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68:70/skyeye-admin-api"],["/eye_api_bak","http://192.168.13.68:70/eye_api"],["/push_stream_host","http://192.168.13.68:70/push_stream_host"],["/skyeye-login","http://192.168.13.68:70/skyeye-login"],["/ws_api_bak","ws://192.168.13.68:70/ws_api_bak"]]
+#VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68:70/skyeye-admin-api"],["/eye_api_bak","http://192.168.13.68:70/eye_api"],["/push_stream_host","http://192.168.13.68:70/push_stream_host"],["/skyeye-login","http://192.168.13.68:70/skyeye-login"],["/ws_api_bak","ws://192.168.13.68:70/ws_api_bak"]]
+VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.13.68/eye_api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]
+# VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.163:8800/api"],[],["/eye_api_bak","http://192.168.22.163:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"]]
 
 
 # API 接口地址

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 5885 - 4785
pnpm-lock.yaml


+ 50 - 0
src/api/datamanagement/alert-default.ts

@@ -0,0 +1,50 @@
+import { http } from '@/utils/http/axios';
+
+// 获取默认数据表格
+export interface TableQueryForm {
+  pageNumber: Number,     // 页码
+  pageSize: Number,       // 页大小
+  source?: Number,        // 问题单来源:1-AI检测、2-人工上报
+  issueType?: Number,     // 问题单类型
+  workspaceId?: Number[], // 工位id(地点=车间+工位?)
+  issueState?: Number,    // 问题单状态:1-待审核、2-待处理、3-待复核、4-已退回、5-已处理
+  startTime?: string,     // 开始时间
+  endTime?: string        // 结束时间
+};
+export const getDefaultTableData = (body: TableQueryForm) => {
+  return http.request({
+    url: '/issueManagement/getIssueDefaultListPageByCondition',
+    method: 'post',
+    data: body,
+  });
+};
+
+// 复制到展示问题单列表
+export const copyToShowTableData = (ids: number[]) => {
+  return http.request({
+    url: `/issueManagement/copyToIssueDisplayList?issueId=${ids.join(',')}`,
+    method: 'post',
+  });
+};
+
+// 删除默认问题单
+export const deleteDefaultTableData = (ids: number[]) => {
+  return http.request({
+    url: `/issueManagement/deleteIssueDefault?issueId=${ids.join(',')}`,
+    method: 'delete',
+  });
+};
+
+// 更新默认问题单列表状态(加急/隐藏)
+export interface UpdateList {
+  id: Number[],       // 问题单id,可批量操作
+  hide?: Boolean,     // 是否隐藏
+  priority?: Number,  // 0-未加急,1-加急
+};
+export const updateDefaultTableData = (body: UpdateList) => {
+  return http.request({
+    url: '/issueManagement/updateIssueDefaultList',
+    method: 'put',
+    data: body,
+  });
+};

+ 77 - 0
src/api/datamanagement/alert-show.ts

@@ -0,0 +1,77 @@
+import { http } from '@/utils/http/axios';
+
+// 获取展示数据表格
+export interface TableQueryForm {
+  pageNumber: Number,     // 页码
+  pageSize: Number,       // 页大小
+  source?: Number,        // 问题单来源:1-AI检测、2-人工上报
+  issueType?: Number,     // 问题单类型
+  workspaceId?: Number[], // 工位id/地点
+  issueState?: Number,    // 问题单状态:1-待审核、2-待处理、3-待复核、4-已退回、5-已处理
+};
+export const getShowTableData = (body: TableQueryForm) => {
+  return http.request({
+    url: '/issueManagement/getIssueDisplayListPageByCondition',
+    method: 'post',
+    data: body,
+  });
+};
+
+// 添加展示表格接口(添加btn)
+export interface AddForm {
+  source: Number,         // 问题单来源:1-AI检测、2-人工上报
+  issueType: Number,      // 问题单类型
+  description: String,    // 问题描述
+  pictures: Array<string>,// 图片
+  workspaceId: Number,    // 工位id/地点
+  issueTime: String,      // 问题时间(默认是creatAt)
+  issueState: Number,     // 问题单状态:1-待审核、2-待处理、3-待复核、4-已退回、5-已处理
+};
+export const addShowTableData = (body: AddForm) => {
+  return http.request({
+    url: '/issueManagement/addIssueDisplay',
+    method: 'post',
+    data: body,
+  });
+};
+
+// 删除展示表格接口(delete)
+export const deleteShowTableData = (ids: number[]) => {
+  return http.request({
+    url: `/issueManagement/deleteIssueDisplay?issueId=${ids.join(',')}`,
+    method: 'delete',
+  });
+};
+
+// 编辑展示表格接口(edit)
+export interface EditForm {
+  id: Number,             // 问题单Id,表格data获取
+  source: Number,         // 问题单来源:1-AI检测、2-人工上报
+  issueType: Number,      // 问题单类型
+  description: String,    // 问题描述
+  pictures: Array<string>,// 图片
+  workspaceId: Number,    // 工位id/地点
+  issueTime: String,      // 问题时间(默认是creatAt)
+  issueState: Number,     // 问题单状态:1-待审核、2-待处理、3-待复核、4-已退回、5-已处理
+  isHide?: Boolean,       // 问题单是否隐藏(根据UI编辑/添加表单没有这个选项。改变隐藏显示另有接口) 
+};
+export const EditShowTableData = (body: EditForm) => {
+  return http.request({
+    url: '/issueManagement/editIssueDisplay',
+    method: 'put',
+    data: body,
+  });
+};
+
+// 更新展示表格状态(show/hide)
+export interface UpdateList {
+  id: Number[],         // 问题单id,可批量操作
+  isHide?: Boolean,     // 是否隐藏
+};
+export const updateShowTableData = (body: UpdateList) => {
+  return http.request({
+    url: '/issueManagement/updateIssueDisplayList',
+    method: 'put',
+    data: body,
+  });
+};

+ 25 - 0
src/api/datamanagement/alert.ts

@@ -0,0 +1,25 @@
+import { http } from '@/utils/http/axios';
+
+// 问题类型接口1:AI检测
+export const getAIList = () => {
+  return http.request({
+    url: '/cameraPreview/getAlgoList',
+    method: 'get'
+  });
+};
+
+// 问题类型接口2:人工上报
+export const getManualList = () => {
+  return http.request({
+    url: '/issue/getIssueTypeList',
+    method: 'get'
+  });
+};
+
+// 地点获取接口:获取问题单地点列表
+export const getWorkLocationList = () => {
+  return http.request({
+    url: '/issue/getSiteList',
+    method: 'get'
+  });
+};

BIN=BIN
src/assets/images/alert/delete.png


BIN=BIN
src/assets/images/alert/edit.png


BIN=BIN
src/assets/images/alert/show-off.png


BIN=BIN
src/assets/images/alert/show-on.png


BIN=BIN
src/assets/images/alert/urgent-active.png


BIN=BIN
src/assets/images/alert/urgent.png


+ 70 - 0
src/views/datamanager/alertformdata/AlertformData.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="container-box">
+    <div class="control-btn">
+      <div class="btn" :class="{ 'btn-active': activeName === 'default' }" @click="activeName = 'default'">默认数据</div>
+      <div class="btn" :class="{ 'btn-active': activeName === 'show' }" @click="activeName = 'show'">展示数据</div>
+    </div>
+    <Default class="content-box" v-if="activeName === 'default'" />
+    <Show class="content-box" v-else />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import Default from './components/default/Default.vue';
+import Show from './components/show/Show.vue';
+
+const activeName = ref('default');
+
+</script>
+
+<style scoped lang="scss">
+.container-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  min-height: calc(100vh - 90px);
+  padding: 21px;
+  background-color: rgba(255, 255, 255, 1);
+  border-radius: 10px;
+}
+
+.control-btn {
+  display: flex;
+  margin-bottom: 20px;
+
+  .btn {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 188px;
+    height: 38px;
+    font-size: 14px;
+    font-weight: 400;
+    color: rgba(0, 0, 0, 0.88);
+    border: 1px solid #D9D9D9;
+    background: rgba(0, 0, 0, 0.02);
+    cursor: pointer;
+  }
+
+  :first-child {
+    border-radius: 8px 0px 0px 8px;
+  }
+
+  :last-child {
+    border-radius: 0px 8px 8px 0px;
+  }
+
+  .btn-active {
+    font-weight: 500;
+    color: #1890FF;
+    border: 1px solid #1890FF;
+    background-color: rgba(24, 144, 255, 0.15);
+  }
+}
+
+.content-box {
+  flex: 1;
+}
+</style>

+ 22 - 0
src/views/datamanager/alertformdata/components/common/AddDrawer.vue

@@ -0,0 +1,22 @@
+<template>
+  <QuestionFormBase @save-form="handleAdd" @close-form="handleClose" />
+</template>
+
+<script setup lang="ts">
+import QuestionFormBase from './QuestionFormBase.vue';
+import { addShowTableData } from '@/api/datamanagement/alert-show'
+
+const emits = defineEmits(['close']);
+
+const handleAdd = (formData) => {
+  addShowTableData(formData).then(() => {
+    emits('close');
+  })
+};
+
+const handleClose = () => {
+  emits('close');
+}
+</script>
+
+<style scoped></style>

+ 148 - 0
src/views/datamanager/alertformdata/components/common/AlertTable.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="alert-table-box">
+    <el-table ref="multipleTableRef" :data="tableData" style="width: 100%" height="100%" stripe
+      :cell-style="colorOfState" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55"></el-table-column>
+      <el-table-column label="问题来源" prop="source" width="150">
+        <template #default="{ row }">
+          {{ getNameBySource(row.source) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="类型" prop="issueType" width="150">
+        <template #default="{ row }">
+          {{ getNameByType(row.source, row.issueType, [aiOptions, manualOptions]) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="问题描述" prop="description" width="350">
+        <template #default="{ row }">
+          <span>{{ row.description }}</span>
+          <span class="detail-text" @click="handleDetailClick(row)"> 详情</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="地点" prop="workspaceId" width="300">
+        <template #default="{ row }">
+          {{ getNameByWorkid(row.workshopId, row.workspaceId, locationOptions) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="时间" prop="createdAt" width="250"></el-table-column>
+      <el-table-column label="负责人" prop="personNameInCharge" width="100"></el-table-column>
+      <el-table-column label="处理状态" prop="issueState" width="100">
+        <template #default="{ row }">
+          {{ getNameByState(row.issueState) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="130" fixed="right">
+        <template #default="{ row }">
+          <img src="/src/assets/images/alert/urgent.png" alt="" v-if="!isShowTab && !row.priority"
+            @click="handleUrgent(row)">
+          <img src="/src/assets/images/alert/urgent-active.png" alt="" v-if="!isShowTab && row.priority"
+            @click="handleUrgent(row)">
+          <img src="/src/assets/images/alert/edit.png" alt="" v-if="isShowTab" @click="handleEdit(row)">
+          <img src="/src/assets/images/alert/show-on.png" alt="" v-if="!row.isHide" @click="handleShow(row)">
+          <img src="/src/assets/images/alert/show-off.png" alt="" v-if="row.isHide" @click="handleShow(row)">
+          <img src="/src/assets/images/alert/delete.png" alt="" @click="handleDelete(row)">
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ElTable } from 'element-plus';
+import { onBeforeMount, ref } from 'vue';
+import { getNameBySource, getNameByState } from './constant.question';
+import { useIssueType } from '../../hooks/useIssueType';
+import { useWorkLocation } from '../../hooks/useWorkLocation';
+
+const { aiOptions, manualOptions, getAIOptions, getManualOptions, getNameByType } = useIssueType();
+const { locationOptions, getLocationOptions, getNameByWorkid } = useWorkLocation();
+
+const multipleTableRef = ref<InstanceType<typeof ElTable>>();
+
+interface DataSourceItem {
+  source: Number,         // 问题单来源:1-AI检测、2-人工上报
+  issueType: Number,      // 问题单类型
+  description: String,    // 问题描述
+  workspaceId: Number[],  // 工位id(地点=车间+工位?)
+  createdAt: String,
+  personNameInCharge: String,
+  issueState: Number,     // 问题单状态:1-待审核、2-待处理、3-待复核、4-已退回、5-已处理
+};
+
+interface Props {
+  tableData: Array<DataSourceItem>;
+  isShowTab: boolean;    // true展示数据,false默认数据
+  onDetail: (row: DataSourceItem) => unknown; // 详情事件
+  onUrgent?: (row: DataSourceItem) => unknown; // isShowTab=false时,加急按钮事件
+  onEdit?: (row: DataSourceItem) => unknown;   // isShowTab=true时,编辑按钮事件
+  onShow: (row: DataSourceItem) => unknown;   // 显示/隐藏按钮事件
+  onDelete: (row: DataSourceItem) => unknown; // 删除按钮事件
+};
+
+const props = defineProps<Props>();
+
+const emits = defineEmits(['update:selection'])
+const handleSelectionChange = (selection: any[]) => {
+  emits("update:selection", selection);
+};
+
+const handleDetailClick = (row) => {
+  props.onDetail(row);
+};
+const handleUrgent = (row) => {
+  props.onUrgent?.(row);
+};
+const handleEdit = (row) => {
+  props.onEdit?.(row);
+};
+const handleShow = (row) => {
+  props.onShow(row);
+};
+const handleDelete = (row) => {
+  props.onDelete(row);
+};
+
+const colorOfState = ({ row, columnIndex }) => {
+  if (columnIndex === 7) {
+    if (row.issueState === 4) return { color: "#FF4D4F" };
+    else if (row.issueState === 5) return { color: "#52C41A " };
+    else return { color: "#1890FF " };
+  }
+};
+
+const clearAll = () => {
+  multipleTableRef.value!.clearSelection();
+};
+
+defineExpose({ clearAll })
+
+onBeforeMount(() => {
+  getAIOptions();
+  getManualOptions();
+  getLocationOptions();
+});
+</script>
+
+<style scoped lang="scss">
+.alert-table-box {
+  display: flex;
+  flex-direction: column;
+}
+
+.detail-text {
+  color: #1890FF;
+  font-weight: 600;
+  cursor: pointer;
+}
+
+:deep(.el-table-fixed-column--right) {
+  .cell {
+    display: flex;
+  }
+
+  img {
+    margin-right: 20px;
+    cursor: pointer;
+  }
+}
+</style>

+ 115 - 0
src/views/datamanager/alertformdata/components/common/DetailDialog.vue

@@ -0,0 +1,115 @@
+<template>
+  <div>
+    <el-dialog v-model="visible" title="问题详情" width="80%" align-center :close-on-click-modal="false">
+      <div class="description-box">
+        <div class="title">问题描述</div>
+        <p>{{ description }}</p>
+      </div>
+      <div>
+        <div class="title">问题图片/视频</div>
+        <div class="media-box">
+          <div class="img-box" v-for="(imagePath, index) in imagePaths" :key="index">
+            <img :src="imagePath" alt="" style="object-fit:contain;width:200px;height:200px;border: solid 1px #CCC">
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, toRefs, watch } from 'vue';
+
+const props = defineProps({
+  showDrawer: Boolean,
+  description: String,
+  imagePaths: Array<string>,
+});
+const { showDrawer } = toRefs(props);
+
+const emits = defineEmits(['toggleStatus']);
+const visible = ref(false);
+
+const toggle = (newVal: boolean) => {
+  visible.value = newVal;
+};
+
+watch(
+  () => showDrawer.value,
+  (newVal) => {
+    toggle(newVal)
+  }
+);
+
+watch(
+  () => visible.value,
+  (newVal) => {
+    emits('toggleStatus', newVal)
+  }
+);
+
+</script>
+
+<style scoped>
+:deep(.el-dialog) {
+  padding: 0;
+  background: #FFFFFF;
+  box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
+
+  .el-dialog__header {
+    display: flex;
+    align-items: center;
+    height: 56px;
+    padding-left: 24px;
+    padding-bottom: 0;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+  }
+
+  .el-dialog__title {
+    color: rgba(0, 0, 0, 0.88);
+    font-size: 16px;
+    font-weight: 500;
+  }
+
+  .el-dialog__headerbtn .el-dialog__close {
+    color: #000;
+  }
+
+  .el-dialog__body {
+    height: 564px;
+    padding: 40px;
+    overflow: scroll;
+  }
+}
+
+.title {
+  margin-bottom: 20px;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.title:before {
+  margin-right: 8px;
+  content: '';
+  border-left: 3px solid #1777FF;
+}
+
+.description-box {
+  margin-bottom: 20px;
+
+  p {
+    color: #606266;
+  }
+}
+
+.media-box {
+  display: flex;
+
+  .img-box {
+    width: 200px;
+    height: 200px;
+    margin-right: 10px;
+  }
+}
+</style>

+ 22 - 0
src/views/datamanager/alertformdata/components/common/EditDrawer.vue

@@ -0,0 +1,22 @@
+<template>
+  <QuestionFormBase @save-form="handleEdit" @close-form="handleClose" v-bind="$attrs" />
+</template>
+
+<script setup lang="ts">
+import QuestionFormBase from './QuestionFormBase.vue';
+import { EditShowTableData } from '@/api/datamanagement/alert-show'
+
+const emits = defineEmits(['close']);
+
+const handleEdit = (formData) => {
+  EditShowTableData(formData).then(() => {
+    emits('close');
+  })
+};
+
+const handleClose = () => {
+  emits('close');
+};
+</script>
+
+<style scoped></style>

+ 71 - 0
src/views/datamanager/alertformdata/components/common/Pagination.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="pagination-container">
+    <el-pagination :background="background" v-model:currentPage="currentPage" v-model:pageSize="pageSize"
+      :layout="layout" :pager-count="pagerCount" :total="total" :page-sizes="pageSizes" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits, computed } from "vue";
+
+const emits = defineEmits(["update:page", "update:size"]);
+
+const currentPage = computed({
+  get() {
+    return props.page;
+  },
+  set(val) {
+    emits("update:page", val);
+  },
+});
+
+const pageSize = computed({
+  get() {
+    return props.size;
+  },
+  set(val) {
+    emits("update:size", val);
+  },
+});
+
+const props = defineProps({
+  page: {
+    type: Number,
+    default: 1,
+  },
+  size: {
+    type: Number,
+    default: 10,
+  },
+  background: {
+    type: Boolean,
+    default: false,
+  },
+  layout: {
+    type: String,
+    default: "total, sizes, prev, pager, next, jumper",
+  },
+  //设置最大页码按钮数。 页码按钮的数量,当总页数超过该值时会折叠
+  pagerCount: {
+    type: Number,
+    default: 7,
+  },
+  total: {
+    type: Number,
+    required: true,
+  },
+  pageSizes: {
+    type: Array,
+    default: () => [10, 20, 30, 50],
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  margin: 15px 0;
+  background-color: #ffffff;
+}
+</style>

+ 162 - 0
src/views/datamanager/alertformdata/components/common/QueryForm.vue

@@ -0,0 +1,162 @@
+<template>
+  <div>
+    <el-form :model="queryForm" label-width="auto" :inline="true" ref="formRef">
+      <div class="select-group">
+        <el-form-item label="问题来源:" prop="source">
+          <el-select v-model="queryForm.source" placeholder="全部" clearable @change="handleSelectChange">
+            <el-option v-for="item in sourceOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="类型:" prop="issueType">
+          <el-select v-model="queryForm.issueType" placeholder="全部" clearable :disabled="typeDisable">
+            <el-option v-for="item in options" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="地点:" prop="workspaceId">
+          <el-cascader v-model="workLocation" :options="locationOptions" :props="location" clearable
+            @change="handleCascaderChange" />
+        </el-form-item>
+        <el-form-item label="状态:" prop="issueState">
+          <el-select v-model="queryForm.issueState" clearable>
+            <el-option v-for="item in issueStateOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="日期:" v-if="!isShowTab">
+          <el-date-picker v-model="dateRange" type="datetimerange" range-separator="~" start-placeholder="开始时间"
+            end-placeholder="结束时间" clearable value-format="YYYY-MM-DD HH:mm:ss.SSS" @change="handleDateChange" />
+        </el-form-item>
+      </div>
+      <div class="btn-group">
+        <el-form-item>
+          <el-button class="search-btn" type="primary" @click="handleSearch">查询</el-button>
+          <el-button class="reset-btn" @click="handleReset">重置</el-button>
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { FormInstance } from 'element-plus';
+import { reactive, ref } from 'vue';
+import { sourceOptions, issueStateOptions } from './constant.question';
+
+interface Props {
+  isShowTab: boolean       // true展示数据,false默认数据
+  aiOptions: Array<any>
+  manualOptions: Array<any>
+  locationOptions: Array<any>
+};
+const props = defineProps<Props>();
+const emits = defineEmits(['onSearch', 'onReset']);
+
+interface QueryModel {
+  pageNumber: number,
+  pageSize: number,
+  source?: number,         // 来源
+  issueType?: number,      // 类型
+  workspaceId?: number[],  // 地点=工位id
+  issueState?: number,     // 状态
+  startTime?: string,      // 开始时间(默认)
+  endTime?: string,        // 结束时间(默认)
+};
+
+const formRef = ref<FormInstance>();
+const queryForm = reactive<QueryModel>({
+  pageNumber: 1,
+  pageSize: 10,
+});
+
+interface OptionModel {
+  id: number,
+  name: string
+};
+
+const options = ref<OptionModel[]>([]);
+const typeDisable = ref(true);
+const location = { multiple: true };  // 级联选择器(打开多选)
+const workLocation = ref([]);   // 级联选择器,为二维数组(提取workspaceId)
+const dateRange = ref([]);  // 时间段,拆分成startTime/endTime
+
+const handleSearch = () => {
+  emits('onSearch', queryForm);
+};
+
+const handleReset = () => {
+  typeDisable.value = true;
+  workLocation.value = [];
+  dateRange.value = [];
+  formRef.value?.resetFields();
+  emits('onReset', queryForm);
+};
+
+const handleSelectChange = () => {
+  if (Number(queryForm.source) === 1) {
+    typeDisable.value = false;
+    options.value = props.aiOptions;
+  }
+  else if (Number(queryForm.source) === 2) {
+    typeDisable.value = false;
+    options.value = props.manualOptions;
+  }
+  else {
+    typeDisable.value = true;
+    options.value = [];
+    queryForm.issueType = undefined;
+  }
+};
+
+const handleCascaderChange = () => {
+  const arr = [];
+  workLocation.value.forEach((item) => {
+    arr.push(item[1]);
+  });
+  queryForm.workspaceId = arr;
+};
+
+const handleDateChange = () => {
+  queryForm.startTime = dateRange.value[0];
+  queryForm.endTime = dateRange.value[1];
+}
+</script>
+
+<style scoped lang="scss">
+.el-form {
+  display: flex;
+  justify-content: space-between;
+}
+
+:deep(.el-form--inline .el-form-item) {
+  margin-right: 0;
+}
+
+:deep(.el-form-item__label) {
+  padding: 0;
+}
+
+.select-group {
+  flex: 1;
+}
+
+.btn-group {
+
+  .search-btn {
+    width: 65px;
+    height: 32px;
+    background: #1890FF;
+    border-radius: 2px;
+  }
+
+  .reset-btn {
+    width: 65px;
+    height: 32px;
+    border-radius: 2px;
+    border: 1px solid #1890FF;
+    color: #1890FF;
+  }
+}
+
+.el-select {
+  --el-select-width: 215px;
+}
+</style>

+ 181 - 0
src/views/datamanager/alertformdata/components/common/QuestionFormBase.vue

@@ -0,0 +1,181 @@
+<template>
+  <div>
+    <el-drawer v-model="visible" title="问题详情" direction="rtl" size="30%" :close-on-click-modal="false"
+      @close="handleCancel">
+      <el-form ref="formRef" :model="formData">
+        <el-form-item label="问题来源:" prop="source">
+          <el-select v-model="formData.source" placeholder="请选择问题来源" clearable @change="handleSelectChange">
+            <el-option v-for="item in sourceOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="问题类型:" prop="issueType">
+          <el-select v-model="formData.issueType" placeholder="请选择问题类型" clearable>
+            <el-option v-for="item in options" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="问题描述:" prop="description">
+          <el-input v-model="formData.description" type="textarea"></el-input>
+        </el-form-item>
+        <el-form-item label="问题图片:" prop="pictures">
+          <el-upload v-model:file-list="fileList" action="/skyeye-admin-api/issue/uploadPicture"
+            list-type="picture-card" :on-preview="handlePictureCardPreview" :on-remove="handleRemove"
+            :on-success="handleAvatarSuccess" :headers="getHeaders()" :data="{ bizType: 'PROBLEM_REPORT' }">
+            <el-icon>
+              <Plus />
+            </el-icon>
+          </el-upload>
+          <el-dialog v-model="dialogVisible">
+            <img w-full :src="dialogImageUrl" alt="Preview Image" />
+          </el-dialog>
+        </el-form-item>
+        <el-form-item label="问题地点:" prop="workspaceId">
+          <el-cascader v-model="workLocation" :options="locationOptions" :props="location" clearable
+            @change="handleCascaderChange" />
+        </el-form-item>
+        <el-form-item label="问题时间:" prop="issueTime">
+          <el-date-picker v-model="formData.issueTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss.SSS"
+            placeholder="请选择时间" />
+        </el-form-item>
+        <el-form-item label="问题状态:" prop="issueState">
+          <el-select v-model="formData.issueState" placeholder="请选择问题状态">
+            <el-option v-for="item in issueStateOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleCancel">取消</el-button>
+          <el-button type="primary" @click="handleSubmit">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onBeforeMount, onMounted, reactive, ref } from 'vue';
+import type { FormInstance, UploadProps, UploadUserFile } from 'element-plus';
+import { Plus } from '@element-plus/icons-vue';
+import { sourceOptions, issueStateOptions } from './constant.question';
+import { useIssueType } from '../../hooks/useIssueType';
+import { useWorkLocation } from '../../hooks/useWorkLocation';
+import { useUserStore } from '@/store/modules/user';
+
+enum SourceType {
+  ai = 1,
+  manual = 2
+};
+
+interface Props {
+  initialData?: FormModel
+};
+
+interface FormModel {
+  id?: number,
+  source?: number,         // 来源
+  issueType?: number,      // 类型
+  description?: string,    // 描述
+  pictures?: string[],     // 图片
+  workshopId?: number,     // 车间id
+  workspaceId?: number,    // 工位id
+  issueTime?: string,      // 时间(issueTime为空时填充createdAt)
+  issueState?: number,     // 状态
+};
+
+const visible = ref(true);
+const { aiOptions, manualOptions, getAIOptions, getManualOptions } = useIssueType();
+const { locationOptions, getLocationOptions } = useWorkLocation();
+
+const props = defineProps<Props>();
+const emits = defineEmits(['saveForm', 'closeForm']);
+
+const formRef = ref<FormInstance>();
+const formData = reactive<FormModel>({
+  id: undefined,
+  source: undefined,
+  issueType: undefined,
+  description: '',
+  pictures: [],
+  workshopId: undefined,
+  workspaceId: undefined,
+  issueTime: '',
+  issueState: undefined
+});
+
+const location = { expandTrigger: 'hover' as const };
+const workLocation = ref<[number | undefined, number | undefined] | []>([]);
+
+const handleSelectChange = () => {
+  formData.issueType = undefined;
+};
+const options = computed(() => {
+
+  if (Number(formData.source) === SourceType.ai && aiOptions.value.length > 0) {
+    return aiOptions.value;
+  }
+  if (Number(formData.source) === SourceType.manual && manualOptions.value.length > 0) {
+    return manualOptions.value;
+  }
+  return []
+});
+const handleCascaderChange = () => {
+  formData.workshopId = workLocation.value[0];
+  formData.workspaceId = workLocation.value[1];
+};
+
+const handleCopyData = () => {
+  formData.id = props.initialData?.id;
+  formData.source = props.initialData?.source;
+  formData.issueType = props.initialData?.issueType;
+  formData.description = props.initialData?.description;
+  formData.pictures = props.initialData?.pictures;
+  formData.workshopId = props.initialData?.workshopId;
+  formData.workspaceId = props.initialData?.workspaceId;
+  formData.issueTime = props.initialData?.issueTime;
+  formData.issueState = props.initialData?.issueState;
+
+  workLocation.value = [props.initialData?.workshopId, props.initialData?.workspaceId!];
+  fileList.value = props.initialData?.pictures?.map(str => ({ name: str, url: str })) || [];
+};
+
+// 取消
+const handleCancel = () => {
+  emits('closeForm');
+};
+
+// 保存
+const handleSubmit = () => {
+  emits('saveForm', formData);
+};
+
+// 图片上传
+const userStore = useUserStore();
+const getHeaders = () => {
+  return { Satoken: userStore.getToken, Tenantid: userStore.getTenantId };
+};
+const handleAvatarSuccess = (res) => {
+  if (!formData.pictures) formData.pictures = [];
+  formData.pictures.push(res.data.url);
+};
+const fileList = ref<UploadUserFile[]>([]);
+const dialogImageUrl = ref('');
+const dialogVisible = ref(false);
+const handleRemove: UploadProps['onRemove'] = (uploadFile) => {
+  const index = formData.pictures?.indexOf(uploadFile.url || '')!;
+  if (index !== -1) formData.pictures?.splice(index, 1);
+};
+const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
+  dialogImageUrl.value = uploadFile.url!;
+  dialogVisible.value = true;
+};
+
+onMounted(() => {
+  handleCopyData();
+});
+
+onBeforeMount(() => {
+  getAIOptions();
+  getManualOptions();
+  getLocationOptions();
+});
+</script>
+
+<style scoped></style>

+ 48 - 0
src/views/datamanager/alertformdata/components/common/constant.question.ts

@@ -0,0 +1,48 @@
+// 问题来源
+export enum Source {
+  ai = 1,
+  manual = 2,
+}
+export const sourceNameMap = {
+  [Source.ai]: "AI检测",
+  [Source.manual]: "人工上报",
+}
+// 问题来源下拉框选项
+export const sourceOptions = [
+  { label: sourceNameMap[Source.ai], value: Source.ai },
+  { label: sourceNameMap[Source.manual], value: Source.manual }
+]
+// 表格数据-转换value为label
+export const getNameBySource = (source: Source) => {
+  return sourceNameMap[source] || '-'
+}
+
+
+// 问题状态
+export enum IssueState {
+  toAuth = 1,
+  toDeal = 2,
+  toReview = 3,
+  toRetreat = 4,
+  hasDone = 5,
+}
+export const issueStateNameMap = {
+  [IssueState.toAuth]: '待审核',
+  [IssueState.toDeal]: '待处理',
+  [IssueState.toReview]: '待复核',
+  [IssueState.toRetreat]: '已退回',
+  [IssueState.hasDone]: '已处理',
+}
+// 问题状态下拉框选项
+export const issueStateOptions = [
+  { label: issueStateNameMap[IssueState.toAuth], value: IssueState.toAuth },
+  { label: issueStateNameMap[IssueState.toDeal], value: IssueState.toDeal },
+  { label: issueStateNameMap[IssueState.toReview], value: IssueState.toReview },
+  { label: issueStateNameMap[IssueState.toRetreat], value: IssueState.toRetreat },
+  { label: issueStateNameMap[IssueState.hasDone], value: IssueState.hasDone }
+]
+// 表格数据-转换value为label
+export const getNameByState = (issueState: IssueState) => {
+  return issueStateNameMap[issueState] || '-'
+}
+

+ 349 - 0
src/views/datamanager/alertformdata/components/default/Default.vue

@@ -0,0 +1,349 @@
+<template>
+  <div class="box">
+    <div class="search-form">
+      <QueryForm :is-show-tab="false" :ai-options="aiOptions" :manual-options="manualOptions"
+        :location-options="locationOptions" @on-search="handleSearch" @on-reset="handleReset" />
+    </div>
+    <div class="table-list">
+      <div v-if="showActionBar" class="action-bar">
+        <span class="num-text">已选{{ chooseNum }}项</span>
+        <el-button :class="isActiveHide ? 'btn-active' : 'btn-normal'" @click="handleHideAll">全部隐藏</el-button>
+        <el-button :class="isActiveDelete ? 'btn-active' : 'btn-normal'" @click="handleDeleteAll">删除</el-button>
+        <el-button :class="isActiveUrgent ? 'btn-active' : 'btn-normal'" @click="handleUrgentAll">标记加急</el-button>
+        <el-button :class="isActiveCopy ? 'btn-active' : 'btn-normal'" @click="handleCopyToShow">复制到展示数据</el-button>
+        <span class="close-btn" @click="handleSelectNone"></span>
+      </div>
+      <AlertTable ref="alertTableRef" class="table-bar" :is-show-tab="false" :table-data="tableData"
+        :on-detail="handleDetail" :on-urgent="handleUrgent" :on-show="handleShow" :on-delete="handleDelete"
+        @update:selection="handlePop" />
+    </div>
+    <div class="pagination-box">
+      <Pagination v-model:page="query.pageNumber" v-model:size="query.pageSize" :total="total"
+        @update:page="handlePageChange" @update:size="handleSizeChange" />
+    </div>
+    <DetailDialog :show-drawer="isDetailDialogShow" :description="detailDescription" :image-paths="detailPictures"
+      @toggle-status="switchDetailDialog" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeMount } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import QueryForm from '../common/QueryForm.vue';
+import AlertTable from '../common/AlertTable.vue';
+import DetailDialog from '../common/DetailDialog.vue';
+import Pagination from '../common/Pagination.vue';
+import { useIssueType } from '../../hooks/useIssueType';
+import { useWorkLocation } from '../../hooks/useWorkLocation';
+import {
+  getDefaultTableData,
+  updateDefaultTableData,
+  deleteDefaultTableData,
+  copyToShowTableData
+} from '@/api/datamanagement/alert-default';
+
+const { aiOptions, manualOptions, getAIOptions, getManualOptions } = useIssueType();
+const { locationOptions, getLocationOptions } = useWorkLocation();
+
+const alertTableRef = ref<typeof AlertTable>();
+const tableData = ref([]);
+const showActionBar = ref(false);
+const chooseNum = ref(0);
+const chooseId = ref<number[]>([]);
+const isActiveHide = ref(false);
+const isActiveDelete = ref(false);
+const isActiveUrgent = ref(false);
+const isActiveCopy = ref(false);
+// 详情
+const isDetailDialogShow = ref(false);
+const detailDescription = ref('');
+const detailPictures = ref<string[]>([]);
+// 分页
+const total = ref(0);
+
+const query = ref({
+  pageNumber: 1,
+  pageSize: 10
+});
+// 查询
+const handleSearch = (queryForm) => {
+  query.value = queryForm;
+  getTableData();
+};
+// 重置
+const handleReset = (queryForm) => {
+  query.value = queryForm;
+  getTableData();
+};
+
+// 多选
+const handlePop = (selection) => {
+  selection.forEach((item) => {
+    if (chooseId.value.indexOf(item.id) === -1)
+      chooseId.value.push(item.id);
+  });
+  chooseNum.value = selection.length;
+  showActionBar.value = chooseNum.value > 0 ? true : false;
+};
+// 取消多选
+const handleSelectNone = () => {
+  chooseNum.value = 0;
+  alertTableRef.value?.clearAll();
+  showActionBar.value = false;
+};
+
+// 全部隐藏
+const handleHideAll = () => {
+  if (showActionBar.value) isActiveHide.value = !isActiveHide.value;
+  const updateList = {
+    id: chooseId.value,
+    hide: true,
+  };
+  updateDefaultTableData(updateList).then(() => {
+    getTableData();
+    isActiveHide.value = !isActiveHide.value;
+  });
+};
+
+// 批量删除
+const handleDeleteAll = () => {
+  if (showActionBar.value) isActiveDelete.value = !isActiveDelete.value;
+  ElMessageBox.confirm(
+    '删除之后,数据无法恢复',
+    '请确认是否删除数据',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      customClass: 'deleteMessage',
+      center: true
+    }
+  )
+    .then(() => {
+      deleteDefaultTableData(chooseId.value).then(() => {
+        ElMessage({
+          type: 'success',
+          message: '删除成功',
+        });
+        getTableData();
+        isActiveDelete.value = !isActiveDelete.value;
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消删除',
+      });
+      isActiveDelete.value = !isActiveDelete.value;
+    })
+};
+
+// 标记加急,设置priority = 1
+const handleUrgentAll = () => {
+  if (showActionBar.value) isActiveUrgent.value = !isActiveUrgent.value;
+  const updateList = {
+    id: chooseId.value,
+    priority: 1,
+  };
+  updateDefaultTableData(updateList).then(() => {
+    getTableData();
+    isActiveUrgent.value = !isActiveUrgent.value;
+  });
+};
+
+// 复制到展示数据
+const handleCopyToShow = () => {
+  if (showActionBar.value) isActiveCopy.value = !isActiveCopy.value;
+  copyToShowTableData(chooseId.value).then(() => {
+    ElMessage({
+      message: '复制成功!',
+      type: 'success',
+    });
+    setTimeout(function () {
+      isActiveCopy.value = !isActiveCopy.value;
+    }, 1000);
+  })
+};
+
+// 详情
+const switchDetailDialog = (show: boolean) => {
+  isDetailDialogShow.value = show;
+};
+const handleDetail = (row) => {
+  isDetailDialogShow.value = true;
+  detailDescription.value = row.description;
+  detailPictures.value = row.pictures;
+};
+
+// 单个加急priority=1/取消加急priority=0
+const handleUrgent = (row) => {
+  const tempPriority = row.priority === 0 ? 1 : 0;
+  const updateList = {
+    id: [row.id],
+    priority: tempPriority,
+  };
+  updateDefaultTableData(updateList).then(() => {
+    getTableData();
+  });
+};
+
+// 单个显示hide=false/隐藏hide=true
+const handleShow = (row) => {
+  const tempHide = row.isHide === false ? true : false;
+  const updateList = {
+    id: [row.id],
+    hide: tempHide,
+  };
+  updateDefaultTableData(updateList).then(() => {
+    getTableData();
+  });
+};
+
+// 删除
+const handleDelete = (row) => {
+  ElMessageBox.confirm(
+    '删除之后,数据无法恢复',
+    '请确认是否删除数据',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      customClass: 'deleteMessage',
+      center: true
+    }
+  )
+    .then(() => {
+      deleteDefaultTableData([row.id]).then(() => {
+        ElMessage({
+          type: 'success',
+          message: '删除成功',
+        });
+        getTableData();
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消删除',
+      })
+    })
+};
+
+// 换页,重新获取表格
+const handlePageChange = (val) => {
+  query.value.pageNumber = val;
+  getTableData();
+};
+const handleSizeChange = (val) => {
+  query.value.pageSize = val;
+  getTableData();
+};
+
+const getTableData = () => {
+  getDefaultTableData(query.value).then((res) => {
+    console.log(res);
+    tableData.value = res.records;
+    total.value = res.totalRow;
+  })
+};
+
+onMounted(() => {
+  getTableData();
+});
+
+onBeforeMount(() => {
+  getAIOptions();
+  getManualOptions();
+  getLocationOptions();
+});
+</script>
+
+<style scoped lang="scss">
+.box {
+  display: flex;
+  flex-direction: column;
+}
+
+.table-list {
+  height: calc(100vh - 400px);
+  overflow-y: scroll;
+
+  .action-bar {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    min-width: calc(100vw - 266px);
+    height: 50px;
+    border-radius: 4px 4px 0px 0px;
+    background-color: #DDEFFF;
+    z-index: 10;
+
+    .num-text {
+      margin: 0 34px 0 25px;
+      color: rgba(0, 0, 0, 0.85);
+      font-weight: 500;
+    }
+
+    .btn-normal {
+      color: #1890FF;
+      background: transparent;
+      border: 1px solid #1890FF;
+      border-radius: 2px;
+    }
+
+    .btn-active {
+      color: #FFFFFF;
+      background-color: #1890FF;
+    }
+
+    .close-btn {
+      margin-left: auto;
+      margin-right: 20px;
+    }
+
+    .close-btn:before {
+      content: '\2716';
+      color: #000;
+      cursor: pointer;
+    }
+  }
+
+  .table-bar {
+    position: relative;
+  }
+}
+
+.pagination-box {
+  height: 50px;
+  margin-top: 10px;
+}
+</style>
+<style lang="scss">
+.deleteMessage {
+  padding: 20px 24px;
+  box-shadow: 0px 12px 48px 16px rgba(0, 0, 0, 0.03), 0px 9px 28px 0px rgba(0, 0, 0, 0.05), 0px 6px 16px -8px rgba(0, 0, 0, 0.08);
+  border-radius: 8px;
+
+  .el-message-box__headerbtn {
+    margin-top: 12px;
+    margin-right: 12px;
+  }
+
+  .el-message-box__title {
+    justify-content: start;
+    color: rgba(0, 0, 0, 0.88);
+    font-size: 16px;
+    font-weight: 500;
+  }
+
+  .el-message-box__container {
+    justify-content: start;
+    margin-left: 23px;
+  }
+
+  .el-message-box__btns {
+    display: block;
+    float: right;
+  }
+}
+</style>

+ 313 - 0
src/views/datamanager/alertformdata/components/show/Show.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="box">
+    <div class="search-form">
+      <QueryForm :is-show-tab="true" :ai-options="aiOptions" :manual-options="manualOptions"
+        :location-options="locationOptions" @on-search="handleSearch" @on-reset="handleReset" />
+      <div class="button-group">
+        <el-button type="primary" :icon="Plus" @click="handleAdd">添加</el-button>
+      </div>
+    </div>
+    <div class="table-list">
+      <div v-if="showActionBar" class="action-bar">
+        <span class="num-text">已选{{ chooseNum }}项</span>
+        <el-button :class="isActiveHide ? 'btn-active' : 'btn-normal'" @click="handleHideAll">全部隐藏</el-button>
+        <el-button :class="isActiveDelete ? 'btn-active' : 'btn-normal'" @click="handleDeleteAll">删除</el-button>
+        <span class="close-btn" @click="handleSelectNone"></span>
+      </div>
+      <AlertTable ref="alertTableRef" class="table-bar" :is-show-tab="true" :table-data="tableData"
+        :on-detail="handleDetail" :on-edit="handleEdit" :on-show="handleShow" :on-delete="handleDelete"
+        @update:selection="handlePop" />
+    </div>
+    <div class="pagination-box">
+      <Pagination v-model:page="query.pageNumber" v-model:size="query.pageSize" :total="total"
+        @update:page="handlePageChange" @update:size="handleSizeChange" />
+    </div>
+    <DetailDialog :show-drawer="isDetailDialogShow" :description="detailDescription" :image-paths="detailPictures"
+      @toggle-status="switchDetailDialog" />
+
+    <AddDrawer v-if="isAddDrawer" @close="handleAddDrawerClose" />
+    <EditDrawer v-if="isEditDrawer" :initial-data="rowData" @close="handleEditDrawerClose" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeMount } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { Plus } from '@element-plus/icons-vue';
+import QueryForm from '../common/QueryForm.vue';
+import AlertTable from '../common/AlertTable.vue';
+import DetailDialog from '../common/DetailDialog.vue';
+import Pagination from '../common/Pagination.vue';
+import AddDrawer from '../common/AddDrawer.vue';
+import EditDrawer from '../common/EditDrawer.vue';
+import { useIssueType } from '../../hooks/useIssueType';
+import { useWorkLocation } from '../../hooks/useWorkLocation';
+import {
+  getShowTableData,
+  updateShowTableData,
+  deleteShowTableData
+} from '@/api/datamanagement/alert-show'
+
+const { aiOptions, manualOptions, getAIOptions, getManualOptions } = useIssueType();
+const { locationOptions, getLocationOptions } = useWorkLocation();
+
+const alertTableRef = ref<typeof AlertTable>();
+const tableData = ref([]);
+const showActionBar = ref(false);
+const chooseNum = ref(0);
+const chooseId = ref<number[]>([]);
+const isActiveHide = ref(false);
+const isActiveDelete = ref(false);
+// 详情
+const isDetailDialogShow = ref(false);
+const detailDescription = ref('');
+const detailPictures = ref<string[]>([]);
+// 添加
+const isAddDrawer = ref(false);
+const isEditDrawer = ref(false);
+const rowData = ref();        // 编辑时填充row的tableData
+// 分页
+const total = ref(0);
+
+const query = ref({
+  pageNumber: 1,
+  pageSize: 10
+});
+// 查询
+const handleSearch = (queryForm) => {
+  query.value = queryForm;
+  getTableData();
+};
+// 重置
+const handleReset = (queryForm) => {
+  query.value = queryForm;
+  getTableData();
+};
+
+// 多选
+const handlePop = (selection) => {
+  selection.forEach((item) => {
+    if (chooseId.value.indexOf(item.id) === -1)
+      chooseId.value.push(item.id);
+  });
+  chooseNum.value = selection.length;
+  showActionBar.value = chooseNum.value > 0 ? true : false;
+};
+// 取消多选
+const handleSelectNone = () => {
+  chooseNum.value = 0;
+  alertTableRef.value?.clearAll();
+  showActionBar.value = false;
+};
+
+// 全部隐藏
+const handleHideAll = () => {
+  if (showActionBar.value) isActiveHide.value = !isActiveHide.value;
+  const updateList = {
+    id: chooseId.value,
+    isHide: true,
+  };
+  updateShowTableData(updateList).then(() => {
+    getTableData();
+    isActiveHide.value = !isActiveHide.value;
+  });
+};
+
+// 批量删除
+const handleDeleteAll = () => {
+  if (showActionBar.value) isActiveDelete.value = !isActiveDelete.value;
+  ElMessageBox.confirm(
+    '删除之后,数据无法恢复',
+    '请确认是否删除数据',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      customClass: 'deleteMessage',
+      center: true
+    }
+  )
+    .then(() => {
+      deleteShowTableData(chooseId.value).then(() => {
+        ElMessage({
+          type: 'success',
+          message: '删除成功',
+        });
+        getTableData();
+        isActiveDelete.value = !isActiveDelete.value;
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消删除',
+      });
+      isActiveDelete.value = !isActiveDelete.value;
+    })
+};
+
+// 详情
+const switchDetailDialog = (show: boolean) => {
+  isDetailDialogShow.value = show;
+};
+const handleDetail = (row) => {
+  isDetailDialogShow.value = true;
+  detailDescription.value = row.description;
+  detailPictures.value = row.pictures;
+};
+
+// 添加
+const handleAdd = () => {
+  isAddDrawer.value = true;
+}
+// 编辑
+const handleEdit = (row) => {
+  isEditDrawer.value = true;
+  rowData.value = { ...row };
+};
+
+const handleAddDrawerClose = () => {
+  isAddDrawer.value = false;
+  getTableData();
+};
+
+const handleEditDrawerClose = () => {
+  isEditDrawer.value = false;
+  getTableData();
+};
+
+// 单个显示hide=false/隐藏hide=true
+const handleShow = (row) => {
+  const tempHide = row.isHide === false ? true : false;
+  const updateList = {
+    id: [row.id],
+    isHide: tempHide,
+  };
+  updateShowTableData(updateList).then(() => {
+    getTableData();
+  });
+};
+
+// 删除
+const handleDelete = (row) => {
+  ElMessageBox.confirm(
+    '删除之后,数据无法恢复',
+    '请确认是否删除数据',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      customClass: 'deleteMessage',
+      center: true
+    }
+  )
+    .then(() => {
+      deleteShowTableData([row.id]).then(() => {
+        ElMessage({
+          type: 'success',
+          message: '删除成功',
+        });
+        getTableData();
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消删除',
+      })
+    })
+};
+
+// 换页,重新获取表格
+const handlePageChange = (val) => {
+  query.value.pageNumber = val;
+  getTableData();
+};
+const handleSizeChange = (val) => {
+  query.value.pageSize = val;
+  getTableData();
+};
+
+const getTableData = () => {
+  getShowTableData(query.value).then((res) => {
+    console.log(res);
+    tableData.value = res.records;
+    total.value = res.totalRow;
+  });
+};
+
+onMounted(() => {
+  getTableData();
+});
+
+onBeforeMount(() => {
+  getAIOptions();
+  getManualOptions();
+  getLocationOptions();
+});
+</script>
+
+<style scoped lang="scss">
+.box {
+  display: flex;
+  flex-direction: column;
+}
+
+.search-form {
+  margin-bottom: 20px;
+}
+
+.table-list {
+  height: calc(100vh - 400px);
+  overflow-y: scroll;
+
+  .action-bar {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    min-width: calc(100vw - 266px);
+    height: 50px;
+    border-radius: 4px 4px 0px 0px;
+    background-color: #DDEFFF;
+    z-index: 10;
+
+    .num-text {
+      margin: 0 34px 0 25px;
+      color: rgba(0, 0, 0, 0.85);
+      font-weight: 500;
+    }
+
+    .btn-normal {
+      color: #1890FF;
+      background: transparent;
+      border: 1px solid #1890FF;
+      border-radius: 2px;
+    }
+
+    .btn-active {
+      color: #FFFFFF;
+      background-color: #1890FF;
+    }
+
+    .close-btn {
+      margin-left: auto;
+      margin-right: 20px;
+    }
+
+    .close-btn:before {
+      content: '\2716';
+      color: #000;
+      cursor: pointer;
+    }
+  }
+
+  .table-bar {
+    position: relative;
+  }
+}
+
+.pagination-box {
+  height: 50px;
+  margin-top: 10px;
+}
+</style>

+ 58 - 0
src/views/datamanager/alertformdata/hooks/useIssueType.ts

@@ -0,0 +1,58 @@
+import { ref } from 'vue';
+import { getAIList, getManualList } from '@/api/datamanagement/alert'
+
+type AIOption = {
+  id: number
+  name: string
+};
+
+type ManualOption = {
+  id: number
+  name: string
+};
+
+export function useIssueType() {
+  // AI检测
+  const aiOptions = ref<AIOption[]>([]);
+  // 人工上报
+  const manualOptions = ref<ManualOption[]>([]);
+
+  const getAIOptions = () => {
+    getAIList().then((res) => {
+      res.forEach((item) => {
+        aiOptions.value.push({
+          id: item.id,
+          name: item.name
+        })
+      });
+    })
+  };
+
+  const getManualOptions = () => {
+    getManualList().then((res) => {
+      res.forEach((item) => {
+        manualOptions.value.push({
+          id: item.id,
+          name: item.name
+        })
+      })
+    })
+  };
+
+  // 根据 问题来源id + 问题类型id 决定表格类型栏展示文字
+  const getNameByType = (source, type, arrayOfOptions) => {
+    // arrayOfOptions = [ aiOptions, manualOptions ]
+    const targetArray = arrayOfOptions[source - 1];
+    const foundObject = targetArray.find(obj => obj.id === type);
+    if (foundObject) return foundObject.name;
+    else return '-'
+  };
+
+  return {
+    aiOptions,
+    manualOptions,
+    getAIOptions,
+    getManualOptions,
+    getNameByType
+  };
+}

+ 52 - 0
src/views/datamanager/alertformdata/hooks/useWorkLocation.ts

@@ -0,0 +1,52 @@
+import { ref } from 'vue';
+import { getWorkLocationList } from '@/api/datamanagement/alert'
+
+type Location = {
+  value: number,
+  label: string,
+  children: [
+    {
+      value: number,
+      label: string
+    }
+  ]
+}
+
+export function useWorkLocation() {
+  const locationOptions = ref<Location[]>([]);
+
+  const getLocationOptions = () => {
+    getWorkLocationList().then((res) => {
+      res.forEach((item) => {
+        locationOptions.value.push({
+          value: item.workshopId,
+          label: item.workshopName,
+          children: [
+            {
+              value: item.workspaceList[0].workspaceId,
+              label: item.workspaceList[0].workspaceName
+            }
+          ]
+        })
+      })
+    })
+  };
+
+  // 根据workshopId + workspaceId 指定表格显示 地点 label
+  const getNameByWorkid = (workshopId, workspaceId, array) => {
+    if (workshopId < 0 || workshopId > array.length) return '-';
+    const obj = array[workshopId - 1];
+
+    if (!obj.children || !Array.isArray(obj.children)) return '-';
+    if (workspaceId < 0 || workspaceId > obj.children.length) return '-';
+    const subObj = obj.children.find(subobj => subobj.value === workspaceId);
+
+    return obj.label + ' - ' + subObj.label;
+  }
+
+  return {
+    locationOptions,
+    getLocationOptions,
+    getNameByWorkid
+  }
+}