Преглед изворни кода

Merge branch 'dev-wyf' into 'dev'

交通违规管理通知

See merge request product-group-fe/sfy-safety-group/sfy-safety!197
ai0197 пре 7 месеци
родитељ
комит
0ec23ea769

+ 15 - 0
src/api/traffic-violation/traffic-act.ts

@@ -66,3 +66,18 @@ export function exportActViolation(data: ActTableQuery) {
     },
   );
 }
+
+export function updateRealtimeNotice(data: { realtimeNotice: boolean; speedLimit: number }) {
+  return http.request({
+    url: '/trafficViolation/updateRealtimeNoticeConfig',
+    method: 'post',
+    data,
+  });
+}
+
+export function getRealtimeNoticeConfig() {
+  return http.request<{ realtimeNotice: boolean; speedLimit: number | null }>({
+    url: '/trafficViolation/queryRealtimeNoticeConfig',
+    method: 'get',
+  });
+}

BIN
src/assets/icons/edit_2.png


BIN
src/assets/icons/help.png


+ 162 - 82
src/views/traffic/violation/act/Act.vue

@@ -6,11 +6,13 @@
     <main class="safety-platform-container__main">
       <div class="search-table-container">
         <header>
-          <div>
+          <div v-if="actManagementPermission" style="position: relative">
             <el-button type="primary" class="search-table-container--button" :icon="Plus" @click="handleCreateAct">
               新建记录
             </el-button>
-            <el-button plain class="search-table-container--button" @click=""> 批量导入 </el-button>
+            <el-button plain class="search-table-container--button" @click="batchImportVisible = true">
+              批量导入
+            </el-button>
             <RealtimeNotice />
           </div>
 
@@ -70,58 +72,78 @@
           </div>
         </header>
         <!-- 表格 -->
-        <BasicTable
-          :tableData="tableData"
-          :tableConfig="tableConfig"
-          @update:pageSize="handleSizeChange"
-          @update:pageNumber="handleCurrentChange"
-          @update:selection="handleSelectionChange"
-        >
-          <template #violateType="scope">
-            <span>{{ ACT_VIOLATION_TYPE_LABEL[scope.row.violateType] }}</span>
-          </template>
-          <template #capturePhotos="scope">
-            <ImageViewer :file-list="scope.row.capturePhotos" />
-          </template>
-          <template #createSource="scope">
-            <span>{{ ACT_NOTICE_DATA_SOURCE_LABEL[scope.row.createSource] }}</span>
-          </template>
-          <template #isNotice="scope">
-            <div class="notice-state">
-              <div
-                :style="{
-                  backgroundColor: ACT_NOTICE_STATE_COLOR[scope.row.isNotice],
-                  width: '6px',
-                  height: '6px',
-                  borderRadius: '50%',
-                  marginRight: '5px',
-                }"
-              ></div>
-              <span>{{ ACT_NOTICE_STATE_LABEL[scope.row.isNotice] }}</span>
+        <div class="batch-table">
+          <div class="batch-operation--div" v-show="actManagementPermission && selectionItems.length > 0">
+            <span>已选{{ selectionItems.length }}项</span>
+            <div class="batch-operation--div--close">
+              <div class="batch-operation--div--button">
+                <el-button class="custom-el-button" @click="handleBatchNotice">批量通知</el-button>
+                <el-button class="custom-el-button" @click="handleBatchDelete">批量删除</el-button>
+              </div>
+              <el-icon class="close-icon" @click="handleCloseBatchOperation"><Close /></el-icon>
             </div>
-          </template>
-          <template #action="scope">
-            <ActionButton
-              v-if="scope.row.isNotice === ACT_NOTICE_STATE.INACTIVE"
-              text="编辑"
-              @click="handleEditAct(scope.row.id)"
-            />
-            <ActionButton
-              v-if="scope.row.isNotice === ACT_NOTICE_STATE.INACTIVE"
-              text="通知"
-              @click="handleNoticeAct(scope.row.id)"
-            />
-            <ActionButton
-              text="删除"
-              :popconfirm="{
-                title: '确定要删除?',
-              }"
-              @confirm="handleDeleteAct(scope.row.id)"
-            />
-          </template>
-        </BasicTable>
+          </div>
+          <BasicTable
+            :tableData="tableData"
+            :tableConfig="tableConfig"
+            @update:pageSize="handleSizeChange"
+            @update:pageNumber="handleCurrentChange"
+            @update:selection="handleSelectionChange"
+          >
+            <template #violateType="scope">
+              <span>{{ ACT_VIOLATION_TYPE_LABEL[scope.row.violateType] }}</span>
+            </template>
+            <template #capturePhotos="scope">
+              <ImageViewer :file-list="scope.row.capturePhotos" />
+            </template>
+            <template #createSource="scope">
+              <span>{{ ACT_NOTICE_DATA_SOURCE_LABEL[scope.row.createSource] }}</span>
+            </template>
+            <template #isNotice="scope">
+              <div class="notice-state">
+                <div
+                  :style="{
+                    backgroundColor: ACT_NOTICE_STATE_COLOR[scope.row.isNotice],
+                    width: '6px',
+                    height: '6px',
+                    borderRadius: '50%',
+                    marginRight: '5px',
+                  }"
+                ></div>
+                <span>{{ ACT_NOTICE_STATE_LABEL[scope.row.isNotice] }}</span>
+              </div>
+            </template>
+            <template #action="scope">
+              <ActionButton
+                v-if="scope.row.isNotice === ACT_NOTICE_STATE.INACTIVE"
+                text="编辑"
+                @click="handleEditAct(scope.row.id)"
+              />
+              <ActionButton
+                v-if="scope.row.isNotice === ACT_NOTICE_STATE.INACTIVE"
+                text="通知"
+                @click="handleNoticeAct(scope.row.id)"
+              />
+              <ActionButton
+                text="删除"
+                :popconfirm="{
+                  title: '确定要删除?',
+                }"
+                @confirm="handleDeleteAct(scope.row.id)"
+              />
+            </template>
+          </BasicTable>
+        </div>
       </div>
     </main>
+    <BatchImport
+      :visible="batchImportVisible"
+      :importApiUrl="importApiUrl"
+      :templateUrl="templateUrl"
+      :templateName="'违规行为记录-批量导入模版'"
+      @close="() => (batchImportVisible = false)"
+      @update="handleUpdate"
+    />
   </div>
 </template>
 
@@ -131,8 +153,9 @@
   import SelectableInput from '@/components/formItems/selectableInput/SelectableInput.vue';
   import ActionButton from '@/components/ActionButton.vue';
   import RealtimeNotice from './components/RealtimeNotice.vue';
+  import dayjs from 'dayjs';
   import { ElMessage } from 'element-plus';
-  import { TABLE_OPTIONS, VIOLATION_ACT_TABLE_COLUMNS } from './configs/tables';
+  import { TABLE_OPTIONS, VIOLATION_ACT_TABLE_COLUMNS, VIOLATION_NOTICE_TABLE_COLUMNS } from './configs/tables';
   import {
     ACT_NOTICE_DATA_SOURCE_LABEL,
     ACT_VIOLATION_TYPE,
@@ -143,6 +166,7 @@
     ACT_NOTICE_STATE,
     ACT_NOTICE_STATE_LABEL,
     ACT_NOTICE_STATE_COLOR,
+    ACT_MANAGEMENT_PROMISSION_CODE,
   } from './constants';
   import { ref, reactive, onMounted } from 'vue';
   import { Search, Plus } from '@element-plus/icons-vue';
@@ -158,10 +182,18 @@
   } from '@/api/traffic-violation/traffic-act';
   import { downloadFile } from '@/views/disaster/utils/download';
   import ImageViewer from './components/ImageViewer.vue';
-  import dayjs from 'dayjs';
+  import { BatchImport } from '@/components/batch-import';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
 
   const router = useRouter();
 
+  const { permissions } = useUserInfoHook();
+  const actManagementPermission = ref<Boolean>(
+    Boolean(permissions.find((item: { code: string }) => item.code === ACT_MANAGEMENT_PROMISSION_CODE)),
+  );
+
   // 搜索栏
   const selectableInputRef = ref<InstanceType<typeof SelectableInput>>();
   const searchData = reactive<ActTableSearch>({});
@@ -189,7 +221,7 @@
 
   function handleSearch() {
     getQuery();
-    getTabelData();
+    getTableData();
   }
 
   function handleReset() {
@@ -218,7 +250,10 @@
   // 表格
   const basicTableRef = ref<InstanceType<typeof BasicTable>>();
 
-  const { tableConfig, pagination } = useTableConfig(VIOLATION_ACT_TABLE_COLUMNS, TABLE_OPTIONS);
+  const { tableConfig, pagination } = useTableConfig(
+    actManagementPermission ? VIOLATION_ACT_TABLE_COLUMNS : VIOLATION_NOTICE_TABLE_COLUMNS,
+    TABLE_OPTIONS,
+  );
 
   const tableData = ref<ActTableData[]>([]);
 
@@ -233,39 +268,43 @@
   const handleSizeChange = (value: number) => {
     pagination.pageNumber = value;
     tabelQuery.pageSize = value;
-    getTabelData();
+    getTableData();
   };
   const handleCurrentChange = (value: number) => {
     pagination.pageNumber = value;
     tabelQuery.pageSize = value;
-    getTabelData();
+    getTableData();
   };
 
-  const handleSelectionChange = (value: any[]) => {};
+  const selectionItems = ref<any[]>([]);
 
-  // const handleCloseBatchOperation = () => {
-  //   if (!basicTableRef.value) return;
-  //   basicTableRef.value.clearSelection();
-  // };
+  const handleSelectionChange = (selection: any[]) => {
+    selectionItems.value = selection;
+  };
 
-  // const handleBatchNotice = async () => {
-  //   const confirmed = await openMessageBox('', '确认通知任务吗?', 'warning');
-  //   if (!confirmed) return;
-  //   const noticeIds = getSelectionIds(ACTIVE_STATUS.NOT_EFFECTIVE);
-  //     await noticeActData(noticeIds);
-  //     ElMessage.success('批量通知成功');
-  //     getTableData();
-  // };
-  // const handleBatchDelete = async () => {
-  //   const confirmed = await openMessageBox('', '删除后任务不可恢复,确认删除吗?', 'warning');
-  //   if (!confirmed) return;
-  //   const deleteIds = getSelectionIds(ACTIVE_STATUS.NOT_EFFECTIVE);
-  //   await deleteActData(deleteIds);
-  //   ElMessage.success('批量删除成功');
-  //   getTableData();
-  // };
+  const handleCloseBatchOperation = () => {
+    if (!basicTableRef.value) return;
+    basicTableRef.value.clearSelection();
+  };
 
-  async function getTabelData() {
+  const handleBatchNotice = async () => {
+    const confirmed = await openMessageBox('', '确认通知任务吗?', 'warning');
+    if (!confirmed) return;
+    const noticeIds = selectionItems.value.map((item) => item.id);
+    await noticeActData(noticeIds);
+    ElMessage.success('批量通知成功');
+    getTableData();
+  };
+  const handleBatchDelete = async () => {
+    const confirmed = await openMessageBox('', '删除后任务不可恢复,确认删除吗?', 'warning');
+    if (!confirmed) return;
+    const deleteIds = selectionItems.value.map((item) => item.id);
+    await deleteActData(deleteIds);
+    ElMessage.success('批量删除成功');
+    getTableData();
+  };
+
+  async function getTableData() {
     tableConfig.loading = true;
     const res = await getActTableList(tabelQuery);
     tableData.value = res.records;
@@ -273,8 +312,19 @@
     tableConfig.loading = false;
   }
 
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  const importApiUrl = ref(urlJoin(urlPrefix, '/trafficViolation/importTrafficViolationList'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-vehicle-template.xlsx');
+
+  const handleUpdate = () => {
+    batchImportVisible.value = false;
+    getTableData();
+  };
+
   onMounted(async () => {
-    await getTabelData();
+    await getTableData();
   });
 
   function handleCreateAct() {
@@ -305,7 +355,7 @@
     } finally {
       tableConfig.loading = false;
     }
-    getTabelData();
+    getTableData();
   }
 
   async function handleDeleteAct(id: number) {
@@ -318,7 +368,7 @@
     } finally {
       tableConfig.loading = false;
     }
-    getTabelData();
+    getTableData();
   }
 </script>
 
@@ -361,4 +411,34 @@
     align-items: center;
     justify-self: center;
   }
+
+  .batch-table {
+    position: relative;
+    width: 100%;
+    height: 100%;
+  }
+  .batch-operation--div {
+    @include flex-center;
+    justify-content: flex-start;
+    position: absolute;
+    top: 0;
+    left: 0;
+    gap: 60px;
+    width: 100%;
+    height: 48px;
+    border: 4px;
+    padding: 16px 25px;
+    background-color: #ddefff;
+    z-index: 100;
+    &--close {
+      @include flex-center;
+      justify-content: space-between;
+      flex: 1;
+    }
+    .close-icon {
+      font-size: 20px;
+      color: #ff4d4f;
+      cursor: pointer;
+    }
+  }
 </style>

+ 43 - 3
src/views/traffic/violation/act/components/ActEdit.vue

@@ -41,7 +41,7 @@
         <UploadImages
           :maxCount="9"
           ref="uploadImagesRef"
-          :image-list="recordImageList"
+          :image-list="imageList"
           @upload-success="handleUploadChange"
         />
       </template>
@@ -64,9 +64,10 @@
   import { useFormConfigHook } from '@/hooks/useFormConfigHook';
   import { useUserInfoHook } from '@/views/disaster/hooks';
   import { getActDataDetail, updateActData } from '@/api/traffic-violation/traffic-act';
-  import { CreateActRuleForm, CreateActQuery, ActTableData } from '../types';
+  import { CreateActRuleForm, UpdateActQuery, ActTableData } from '../types';
   import { ACT_FORM_CONFIG, ACT_FORM_DATA, ACT_FORM_RULES } from '../configs/form';
   import { ACT_VIOLATION_TYPE, ACT_VIOLATION_TYPE_OPTIONS } from '../constants';
+  import { unformatImage, formatImageList } from '../utils';
 
   const props = defineProps<{
     id: number;
@@ -87,15 +88,54 @@
   const actDetail = ref<ActTableData>();
   const getDetail = async () => {
     actDetail.value = await getActDataDetail(props.id);
+    if (!actDetail.value) return;
+    ruleFormData.carNumber = actDetail.value.carNumber;
+    ruleFormData.violateType = actDetail.value.violateType;
+    ruleFormData.speed = actDetail.value.speed;
+    ruleFormData.violateLocation = actDetail.value.violateLocation;
+    ruleFormData.captureTime = actDetail.value.captureTime;
+    ruleFormData.capturePhotos = unformatImage(actDetail.value.capturePhotos)?.map((x) => {
+      return { url: x };
+    });
+    ruleFormData.creatName = actDetail.value.creatName;
+
+    imageList.value = unformatImage(actDetail.value.capturePhotos)!;
   };
 
-  const getFormData = async () => {};
+  const imageList = ref<string[]>([]);
+
+  const getFormData = async () => {
+    if (ruleFormData.violateType !== ACT_VIOLATION_TYPE.SPEEDING) {
+      ruleFormData.speed = null;
+    }
+    cloneRuleFormData();
+    const res: UpdateActQuery = {
+      id: props.id,
+      createSource: 1,
+      ...ruleFormData,
+      violateType: ruleFormData.violateType!,
+      capturePhotos: JSON.stringify(await formatImageList(ruleFormData.capturePhotos)),
+    };
+
+    return res;
+  };
 
   const formLoading = ref(false);
   const router = useRouter();
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    try {
+      formLoading.value = true;
+      const params = await getFormData();
+      await updateActData(params);
+      ElMessage.success('编辑成功');
+      router.back();
+    } catch (e) {
+      console.log(e);
+    } finally {
+      formLoading.value = false;
+    }
   };
 
   const handleUploadChange = () => {

+ 72 - 2
src/views/traffic/violation/act/components/RealtimeNotice.vue

@@ -1,10 +1,80 @@
 <template>
-  <div class="realtime-notice"> </div>
+  <div class="realtime-notice">
+    <el-tooltip effect="light" content="开启后,将推送符合通知条件的违规行为至车主" placement="top">
+      <img src="@/assets/icons/help.png" alt="" />
+    </el-tooltip>
+    <span class="label">实时通知</span>
+    <el-switch v-model="switchVal" class="switch" @change="handleRealtimeNoticeChange" />
+    <span class="condition" v-if="switchVal">条件:车速≥{{ speed }}km/h</span>
+    <el-popconfirm
+      width="300"
+      hide-icon
+      placement="bottom-end"
+      title="设置通知条件"
+      @confirm="handleRealtimeNoticeChange(true)"
+    >
+      <template #reference>
+        <img style="margin: 0 10px; cursor: pointer" src="@/assets/icons/edit_2.png" alt="" />
+      </template>
+      <template #actions="{ confirm, cancel }">
+        <div class="popconfirm-container"></div>
+        <el-form label-width="50px">
+          <el-form-item label="车速">
+            <el-input v-model="speedEdit" type="number" min="0">
+              <template #suffix>km/h</template>
+            </el-input>
+          </el-form-item>
+        </el-form>
+        <el-button @click="cancel">取消</el-button>
+        <el-button type="primary" @click="confirm">确定</el-button>
+      </template>
+    </el-popconfirm>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+
+  import { updateRealtimeNotice, getRealtimeNoticeConfig } from '@/api/traffic-violation/traffic-act';
+
+  const switchVal = ref(false);
+  const speed = ref(45);
+  const speedEdit = ref(45);
+
+  async function handleRealtimeNoticeChange(val: boolean) {
+    await updateRealtimeNotice({ realtimeNotice: val, speedLimit: speedEdit.value });
+    getRealtimeNoticeConfig().then((res) => {
+      switchVal.value = res.realtimeNotice;
+      speed.value = res.speedLimit ? res.speedLimit : 45;
+    });
+  }
+
+  onMounted(() => {
+    getRealtimeNoticeConfig().then((res) => {
+      switchVal.value = res.realtimeNotice;
+      speed.value = res.speedLimit ? res.speedLimit : 45;
+    });
+  });
+</script>
 
 <style scoped>
   .realtime-notice {
+    position: absolute;
+    right: 0px;
+    top: 0px;
+    display: flex;
+    align-items: center;
+  }
+  .label {
+    padding: 0 10px;
+    color: rgba(0, 0, 0, 0.85);
+    font-size: 14px;
+  }
+  .condition {
+    width: 180px;
+    padding-right: 10px;
+    text-align: end;
+    color: rgba(0, 0, 0, 0.7);
+    font-size: 14px;
   }
 </style>

+ 78 - 4
src/views/traffic/violation/act/configs/tables.ts

@@ -22,14 +22,14 @@ export const VIOLATION_ACT_TABLE_COLUMNS: TableColumnProps[] = [
     type: 'index',
   },
   {
-    label: '车',
-    prop: 'violateName',
+    label: '车牌号',
+    prop: 'carNumber',
     align: 'center',
     minWidth: '120px',
   },
   {
-    label: '车牌号',
-    prop: 'carNumber',
+    label: '车',
+    prop: 'violateName',
     align: 'center',
     minWidth: '120px',
   },
@@ -95,3 +95,77 @@ export const VIOLATION_ACT_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'center',
   },
 ];
+
+export const VIOLATION_NOTICE_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '序号',
+    align: 'center',
+    width: '80px',
+    type: 'index',
+  },
+  {
+    label: '车牌号',
+    prop: 'carNumber',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '车主',
+    prop: 'violateName',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '所属部门',
+    prop: 'deptName',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '违规类型',
+    prop: 'violateType',
+    slot: 'violateType',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '车速',
+    prop: 'speed',
+    align: 'center',
+    minWidth: '120px',
+  },
+
+  {
+    label: '抓拍图片',
+    prop: 'capturePhotos',
+    slot: 'capturePhotos',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '违规地点',
+    prop: 'violateLocation',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '时间',
+    prop: 'captureTime',
+    align: 'center',
+    width: '200px',
+  },
+  {
+    label: '数据来源',
+    prop: 'createSource',
+    slot: 'createSource',
+    align: 'center',
+    width: '200px',
+  },
+  {
+    label: '通知状态',
+    prop: 'isNotice',
+    slot: 'isNotice',
+    align: 'center',
+    width: '200px',
+  },
+];

+ 3 - 0
src/views/traffic/violation/act/constants.ts

@@ -83,3 +83,6 @@ export const ACT_NOTICE_DATA_SOURCE_LABEL = {
   [ACT_NOTICE_DATA_SOURCE.ARTIFICAL]: '人工上报',
   [ACT_NOTICE_DATA_SOURCE.OUTSIDE]: '外部创建',
 };
+
+export const ACT_MANAGEMENT_PROMISSION_CODE = 'traffic_business_module:violation_record';
+export const NOTICE_MANAGEMENT_PROMISSION_CODE = 'traffic_business_module:violation_notice';

+ 1 - 0
src/views/traffic/violation/act/types.ts

@@ -36,6 +36,7 @@ export interface ActTableData {
   createdBy?: number;
   remark?: string;
   isNotice?: number; //0-未通知 1-已通知
+  creatName?: string;
 }
 
 export interface CreateActRuleForm {

+ 4 - 9
src/views/traffic/violation/act/utils.ts

@@ -8,27 +8,22 @@ export function stringToArray(str?: string): number[] | undefined {
 
 export function unformatImage(file?: string) {
   if (!file) return undefined;
-  const fileData: ImageItem[] = JSON.parse(file);
+  const fileData: string[] = JSON.parse(file);
   return fileData;
 }
 
 const formatImage = async (data: ImageItem) => {
   if (!data.file) return data;
   const name = data.file.name;
-  const size = data.size;
   const res = await uploadFileApi({ bizType: UPLOAD_BIZ_TYPE.ATTACHMENT, fileName: name, file: data.file });
-  const url = res.url;
-  return {
-    name,
-    size,
-    url,
-  };
+  return res.url;
 };
 
-export const formatImageList = async (data: ImageItem[] | undefined) => {
+export const formatImageList = async (data: Array<ImageItem> | undefined) => {
   if (!data || data.length === 0) return null;
   const res = await Promise.all(
     data.map(async (item) => {
+      if (!item.file) return item.url;
       const res = await formatImage(item);
       return res;
     }),

+ 260 - 3
src/views/traffic/violation/notice/Notice.vue

@@ -1,7 +1,264 @@
 <template>
-  <div> </div>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title"> 违规行为记录 </div>
+    </header>
+    <main class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header>
+          <div class="act-search">
+            <section class="select-box">
+              <SelectableInput ref="selectableInputRef" :options="ACT_TABLE_SEARCH_OPTIONS" />
+              <div class="select-box--item">
+                <span>违规类型:</span>
+                <el-select
+                  v-model="searchData.violationType"
+                  placeholder="请选择违规类型"
+                  class="select-box--select"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in ACT_VIOLATION_TYPE_OPTIONS"
+                    :key="item.value"
+                    :value="item.value"
+                    :label="item.label"
+                    :disabled="item.disabled"
+                  />
+                </el-select>
+              </div>
+              <div class="select-box--item">
+                <span>通知状态:</span>
+                <el-select
+                  v-model="searchData.isNotice"
+                  placeholder="请选择通知状态"
+                  class="select-box--select"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in ACT_NOTICE_STATE_OPTIONS"
+                    :key="item.value"
+                    :value="item.value"
+                    :label="item.label"
+                    :disabled="item.disabled"
+                  />
+                </el-select>
+              </div>
+              <div>
+                <span>时间:</span>
+                <el-date-picker
+                  v-model="searchData.searchTime"
+                  type="datetimerange"
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                />
+              </div>
+            </section>
+            <section class="search-btn">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+              <el-button v-if="noticeManagementPermission" @click="handleDownload">导出</el-button>
+            </section>
+          </div>
+        </header>
+        <!-- 表格 -->
+        <BasicTable
+          :tableData="tableData"
+          :tableConfig="tableConfig"
+          @update:pageSize="handleSizeChange"
+          @update:pageNumber="handleCurrentChange"
+        >
+          <template #violateType="scope">
+            <span>{{ ACT_VIOLATION_TYPE_LABEL[scope.row.violateType] }}</span>
+          </template>
+          <template #capturePhotos="scope">
+            <ImageViewer :file-list="scope.row.capturePhotos" />
+          </template>
+          <template #createSource="scope">
+            <span>{{ ACT_NOTICE_DATA_SOURCE_LABEL[scope.row.createSource] }}</span>
+          </template>
+          <template #isNotice="scope">
+            <div class="notice-state">
+              <div
+                :style="{
+                  backgroundColor: ACT_NOTICE_STATE_COLOR[scope.row.isNotice],
+                  width: '6px',
+                  height: '6px',
+                  borderRadius: '50%',
+                  marginRight: '5px',
+                }"
+              ></div>
+              <span>{{ ACT_NOTICE_STATE_LABEL[scope.row.isNotice] }}</span>
+            </div>
+          </template>
+        </BasicTable>
+      </div>
+    </main>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import BasicTable from '@/components/BasicTable.vue';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import SelectableInput from '@/components/formItems/selectableInput/SelectableInput.vue';
+  import dayjs from 'dayjs';
+  import { ElMessage } from 'element-plus';
+  import { TABLE_OPTIONS, VIOLATION_NOTICE_TABLE_COLUMNS } from '../act/configs/tables';
+  import {
+    ACT_NOTICE_DATA_SOURCE_LABEL,
+    ACT_VIOLATION_TYPE,
+    ACT_VIOLATION_TYPE_LABEL,
+    ACT_TABLE_SEARCH_OPTIONS,
+    ACT_VIOLATION_TYPE_OPTIONS,
+    ACT_NOTICE_STATE_OPTIONS,
+    ACT_NOTICE_STATE,
+    ACT_NOTICE_STATE_LABEL,
+    ACT_NOTICE_STATE_COLOR,
+    NOTICE_MANAGEMENT_PROMISSION_CODE,
+  } from '../act/constants';
+  import { ref, reactive, onMounted } from 'vue';
+  import { useRouter } from 'vue-router';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import type { ActTableSearch, ActTableQuery, ActTableData } from '../act/types';
+  import { getActTableList, exportActViolation } from '@/api/traffic-violation/traffic-act';
+  import { downloadFile } from '@/views/disaster/utils/download';
+  import ImageViewer from '../act/components/ImageViewer.vue';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
 
-<style scoped></style>
+  const { permissions } = useUserInfoHook();
+  const noticeManagementPermission = ref<Boolean>(
+    Boolean(permissions.find((item: { code: string }) => item.code === NOTICE_MANAGEMENT_PROMISSION_CODE)),
+  );
+
+  // 搜索栏
+  const selectableInputRef = ref<InstanceType<typeof SelectableInput>>();
+  const searchData = reactive<ActTableSearch>({});
+
+  function getQuery() {
+    if (!selectableInputRef.value) return;
+    tabelQuery.queryParam = {
+      pageType: 2,
+    };
+    const selectableSearch = selectableInputRef.value.getValue();
+    if (selectableSearch) {
+      tabelQuery.queryParam[selectableSearch.key as string] = selectableSearch.value;
+    }
+    if (searchData.isNotice) {
+      tabelQuery.queryParam.isNotice = searchData.isNotice;
+    }
+    if (searchData.violationType) {
+      tabelQuery.queryParam.violationType = searchData.violationType;
+    }
+    if (searchData.searchTime) {
+      tabelQuery.queryParam.startTime = dayjs(searchData.searchTime[0]).format('YYYY-MM-DD HH:MM:ss');
+      tabelQuery.queryParam.endTime = dayjs(searchData.searchTime[1]).format('YYYY-MM-DD HH:MM:ss');
+    }
+  }
+
+  function handleSearch() {
+    getQuery();
+    getTableData();
+  }
+
+  function handleReset() {
+    selectableInputRef.value?.clearValue();
+    searchData.carNumber = undefined;
+    searchData.violateName = undefined;
+    searchData.deptName = undefined;
+    searchData.violationType = undefined;
+    searchData.searchTime = undefined;
+  }
+
+  async function handleDownload() {
+    getQuery();
+    try {
+      const res = await exportActViolation(tabelQuery.queryParam);
+      if (res.size === 0) return;
+      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      const url = window.URL.createObjectURL(blob);
+      downloadFile(url, '违规行为记录.xlsx');
+    } catch (e) {
+      ElMessage.error('下载失败');
+      console.log(e);
+    }
+  }
+
+  // 表格
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+
+  const { tableConfig, pagination } = useTableConfig(VIOLATION_NOTICE_TABLE_COLUMNS, TABLE_OPTIONS);
+
+  const tableData = ref<ActTableData[]>([]);
+
+  const tabelQuery = reactive<QueryPageRequest<ActTableQuery>>({
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {
+      pageType: 2,
+    },
+  });
+
+  const handleSizeChange = (value: number) => {
+    pagination.pageNumber = value;
+    tabelQuery.pageSize = value;
+    getTableData();
+  };
+  const handleCurrentChange = (value: number) => {
+    pagination.pageNumber = value;
+    tabelQuery.pageSize = value;
+    getTableData();
+  };
+
+  async function getTableData() {
+    tableConfig.loading = true;
+    const res = await getActTableList(tabelQuery);
+    tableData.value = res.records;
+    pagination.total = res.totalRow;
+    tableConfig.loading = false;
+  }
+
+  onMounted(async () => {
+    await getTableData();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  .act-search-input {
+    max-width: 500px;
+  }
+  .act-search {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+  }
+  .select-box {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 32px;
+    &--item {
+      @include flex-center;
+      white-space: nowrap;
+    }
+    span {
+      color: rgba(0, 0, 0, 0.85);
+      font-size: 14px;
+    }
+    .el-select {
+      width: 200px;
+    }
+  }
+  .search-btn {
+    display: flex;
+  }
+
+  .notice-state {
+    display: flex;
+    align-items: center;
+    justify-self: center;
+  }
+</style>