Bladeren bron

feat: 交通安全-车辆信息管理功能完成

bxy 7 maanden geleden
bovenliggende
commit
4e7cb1a854
28 gewijzigde bestanden met toevoegingen van 2177 en 10 verwijderingen
  1. 92 0
      src/api/traffic-accident/index.ts
  2. 81 0
      src/api/traffic-vehicle/index.ts
  3. BIN
      src/assets/images/traffic-overview/accident-bg-pure.png
  4. BIN
      src/assets/images/traffic-overview/accident-bg.png
  5. BIN
      src/assets/images/traffic-overview/accident-icon.png
  6. BIN
      src/assets/images/traffic-overview/regulation-bg-pure.png
  7. BIN
      src/assets/images/traffic-overview/regulation-bg.png
  8. BIN
      src/assets/images/traffic-overview/regulation-icon.png
  9. BIN
      src/assets/images/traffic-overview/vehicle-bg-pure.png
  10. BIN
      src/assets/images/traffic-overview/vehicle-bg.png
  11. BIN
      src/assets/images/traffic-overview/vehicle-icon.png
  12. 311 0
      src/components/batch-import/BatchImport.vue
  13. 2 0
      src/components/batch-import/index.ts
  14. 42 0
      src/components/batch-import/types.ts
  15. 19 4
      src/utils/dateUtil.ts
  16. 21 0
      src/views/traffic/Accident/constant/index.ts
  17. 7 0
      src/views/traffic/constant/index.ts
  18. 61 3
      src/views/traffic/overview/Overview.vue
  19. 76 0
      src/views/traffic/overview/components/AccidentRecords.vue
  20. 123 0
      src/views/traffic/overview/components/RegulationList.vue
  21. 76 0
      src/views/traffic/overview/components/ViolationRecords.vue
  22. 243 0
      src/views/traffic/overview/components/ViolationStatistics.vue
  23. 376 3
      src/views/traffic/vehicle/Vehicle.vue
  24. 304 0
      src/views/traffic/vehicle/components/BatchImport.vue
  25. 225 0
      src/views/traffic/vehicle/components/ManageDrawer.vue
  26. 13 0
      src/views/traffic/vehicle/config/index.ts
  27. 72 0
      src/views/traffic/vehicle/config/table.ts
  28. 33 0
      src/views/traffic/vehicle/constant/index.ts

+ 92 - 0
src/api/traffic-accident/index.ts

@@ -0,0 +1,92 @@
+/**
+ * @description: 交通事故记录管理
+ */
+import { http } from '@/utils/http/axios';
+import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
+
+/**
+ * @description: 分页查询交通事故记录列表
+ */
+export interface AccidentListQuery {
+  fieldType?: string;
+  fieldContent?: string;
+  startTime?: string;
+  endTime?: string;
+}
+export interface AccidentInfoStruct {
+  id?: number; // 自增主键
+  accidentLocation?: string; // 事故地点
+  accidentTime?: string; // 事故时间
+  accidentDescription?: string; // 事故描述
+  accidentImages?: string; // 事故图片
+  remark?: string; // 备注
+  createdById?: number; // 创建人id
+  createdByName?: string; // 创建人姓名
+  createdAt?: string; // 创建时间
+  updatedAt?: string; // 更新时间
+  isDeleted?: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getAccidentInfoList = (params: QueryPageRequest<AccidentListQuery>) => {
+  return http.request<QueryPageResponse<AccidentInfoStruct>>({
+    url: '/trafficAccident/queryTrafficAccidentRecordPage',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 添加交通事故记录
+ */
+export interface AccidentPersonnelInfoStruct {
+  id?: number; // 自增主键
+  accidentRecordId?: number; // 事故记录id
+  carNum?: string; // 车牌号
+  accidentPersonnel?: string; // 事故人员
+  phoneNum?: string; // 联系方式
+  createdAt?: string; // 创建时间
+  updatedAt?: string; // 更新时间
+  isDeleted?: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export interface AddAccidentInfoStruct {
+  trafficAccidentRecord: AccidentInfoStruct;
+  accidentPersonnelInfoList: AccidentPersonnelInfoStruct[];
+}
+export const addVehicleInfo = (params: AddAccidentInfoStruct) => {
+  return http.request({
+    url: '/trafficAccident/saveTrafficAccidentRecord',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 更新交通事故记录
+ */
+export const updateVehicleInfo = (params: AccidentInfoStruct) => {
+  return http.request({
+    url: '/trafficAccident/updateTrafficAccidentRecord',
+    method: 'put',
+    params,
+  });
+};
+
+/**
+ * @description: 删除交通事故记录(单个/批量)
+ */
+export const deleteVehicleInfo = (params: { accidentRecordIds: number[] }) => {
+  return http.request({
+    url: '/trafficAccident/deleteTrafficAccidentRecord',
+    method: 'delete',
+    params,
+  });
+};
+
+/**
+ * @description: 查询交通事故记录详情
+ */
+export const getAccidentInfoDetail = (params: { accidentRecordId: number }) => {
+  return http.request({
+    url: `/trafficAccident/queryTrafficAccidentRecordDetail?accidentRecordId=${params.accidentRecordId}`,
+    method: 'get',
+  });
+};

+ 81 - 0
src/api/traffic-vehicle/index.ts

@@ -0,0 +1,81 @@
+/**
+ * @description: 车辆信息管理
+ */
+import { http } from '@/utils/http/axios';
+import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
+
+/**
+ * @description: 分页查询车辆信息列表
+ */
+export interface VehicleListQuery {
+  fieldType?: string;
+  fieldContent?: string;
+}
+export interface VehicleInfoStruct {
+  id?: number; // 自增主键
+  carNum?: string; // 车牌号
+  userId?: number; // 用户id
+  userName?: string; // 用户姓名
+  staffNo?: string; // 工号
+  deptId?: number; // 部门id
+  deptName?: string; // 部门名称
+  phoneNum?: string; // 联系方式
+  createdAt?: string; // 创建时间
+  updatedAt?: string; // 更新时间
+  isDeleted?: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getVehicleInfoList = (params: QueryPageRequest<VehicleListQuery>) => {
+  return http.request<QueryPageResponse<VehicleInfoStruct>>({
+    url: '/vehicleInfo/queryVehicleInfoPage',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 添加车辆信息
+ */
+export const addVehicleInfo = (params: VehicleInfoStruct) => {
+  return http.request({
+    url: '/vehicleInfo/saveVehicleInfo',
+    method: 'post',
+    params,
+  });
+};
+
+/**
+ * @description: 更新车辆信息
+ */
+export const updateVehicleInfo = (params: VehicleInfoStruct) => {
+  return http.request({
+    url: '/vehicleInfo/updateVehicleInfo',
+    method: 'put',
+    params,
+  });
+};
+
+/**
+ * @description: 删除车辆信息(单个/批量)
+ */
+export const deleteVehicleInfo = (params: { vehicleInfoIds: number[] }) => {
+  return http.request({
+    url: '/vehicleInfo/deleteVehicleInfo',
+    method: 'delete',
+    params,
+  });
+};
+
+/**
+ * @description: 导出车辆信息
+ */
+export function exportVehicleInfo(params: VehicleListQuery) {
+  return http.request(
+    {
+      url: '/vehicleInfo/exportVehicleInfo',
+      method: 'post',
+      responseType: 'blob',
+      params,
+    },
+    { isTransformResponse: false },
+  );
+}

BIN
src/assets/images/traffic-overview/accident-bg-pure.png


BIN
src/assets/images/traffic-overview/accident-bg.png


BIN
src/assets/images/traffic-overview/accident-icon.png


BIN
src/assets/images/traffic-overview/regulation-bg-pure.png


BIN
src/assets/images/traffic-overview/regulation-bg.png


BIN
src/assets/images/traffic-overview/regulation-icon.png


BIN
src/assets/images/traffic-overview/vehicle-bg-pure.png


BIN
src/assets/images/traffic-overview/vehicle-bg.png


BIN
src/assets/images/traffic-overview/vehicle-icon.png


+ 311 - 0
src/components/batch-import/BatchImport.vue

@@ -0,0 +1,311 @@
+<template>
+  <div v-if="visible">
+    <div class="overlay"></div>
+    <el-card class="pop-card">
+      <template #header>
+        <div style="font-size: 16px; font-weight: 600">批量导入</div>
+        <el-icon :size="18" @click="handleClose" style="cursor: pointer">
+          <Close />
+        </el-icon>
+      </template>
+      <div class="upload-content">
+        <el-upload
+          ref="upload"
+          style="width: 384px; height: 192px; border-radius: 5px"
+          :headers="getHeaders()"
+          :multiple="false"
+          :limit="1"
+          drag
+          :action="importApiUrl"
+          :with-credentials="true"
+          :auto-upload="false"
+          :before-upload="beforeUpload"
+          :on-success="handleUploadSuccess"
+          :on-exceed="handleExceed"
+          :on-change="handleChange"
+          :on-remove="handleRemove"
+        >
+          <el-icon class="el-icon--upload" style="width: 33px; height: 42px; color: #409efc">
+            <Document />
+          </el-icon>
+          <div class="el-upload__text">
+            <div style="font-size: 12px; color: red; margin-bottom: 5px">请下载模板并按要求填写后上传</div>
+            <div style="font-size: 16px">点击或将文件拖拽到这里上传</div>
+            <div style="font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: 5px"
+              >文件支持.xlsx .xls格式,仅支持上传一个文件</div
+            >
+          </div>
+        </el-upload>
+        <div style="margin-top: 72px; display: flex; justify-content: flex-end">
+          <el-button @click="handleDownloadTemplate">{{ templateName }}</el-button>
+          <el-button type="primary" @click="handleImport" :disabled="isImportDisable">导入</el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-dialog v-model="dialogVisibleErr" title="Warning" width="544" align-center @close="handleUpdate">
+      <template #header>
+        <el-icon :size="24" color="#f2b20a" style="margin: 0 5px 2px">
+          <WarnTriangleFilled />
+        </el-icon>
+        <div class="header-text">导入提示</div>
+      </template>
+      <div class="sum-count">
+        成功导入 <span class="succ-sum">{{ sucCount }}</span> 条, 失败 <span class="err-sum">{{ errCount }}</span> 条
+      </div>
+      <el-table :data="errDetail" style="width: 100%" height="190">
+        <el-table-column prop="rowNum" label="行数" width="100" fixed align="center" />
+        <el-table-column prop="failReason" label="失败原因" show-overflow-tooltip />
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="handleErrConfirm"> 确定 </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import axios, { AxiosRequestConfig } from 'axios';
+  import { getHeaders } from '@/utils/http/axios';
+  import { genFileId, ElMessage } from 'element-plus';
+  import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+  import { Close, Document, WarnTriangleFilled } from '@element-plus/icons-vue';
+  import type { BatchImportProps, ErrorDetailItem, ImportResponseData } from './types';
+
+  const props = withDefaults(defineProps<BatchImportProps>(), {
+    successMessage: '添加成功',
+    templateName: '下载模板',
+  });
+
+  const emit = defineEmits<{
+    (e: 'close'): void;
+    (e: 'update'): void;
+  }>();
+
+  const upload = ref<UploadInstance>();
+  const isImportDisable = ref<boolean>(true);
+  const dialogVisibleErr = ref<boolean>(false);
+  const sucCount = ref<number>(0);
+  const errCount = ref<number>(0);
+  const errDetail = ref<ErrorDetailItem[]>([]);
+
+  // 监听visible属性变化,重置状态
+  watch(
+    () => props.visible,
+    (newVal) => {
+      if (newVal) {
+        isImportDisable.value = true;
+        dialogVisibleErr.value = false;
+        sucCount.value = 0;
+        errCount.value = 0;
+        errDetail.value = [];
+        // 清空上传文件
+        if (upload.value) {
+          upload.value.clearFiles();
+        }
+      }
+    },
+  );
+
+  // 下载模板
+  const handleDownloadTemplate = async () => {
+    try {
+      const config: AxiosRequestConfig = {
+        headers: getHeaders(),
+        responseType: 'blob',
+      };
+      const response = await axios.get(props.templateUrl, config);
+      const blob = new Blob([response.data], {
+        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      });
+      // 创建下载链接
+      let downloadLink: HTMLAnchorElement | null = document.createElement('a');
+      const url = window.URL.createObjectURL(blob);
+      downloadLink.href = url;
+      downloadLink.download = `${props.templateName}.xlsx`;
+      downloadLink.click();
+      // 移除下载链接
+      window.URL.revokeObjectURL(url);
+      downloadLink = null;
+    } catch (error) {
+      console.error('Error downloading file:', error);
+      ElMessage.error('模板下载失败');
+    }
+  };
+
+  // 导入
+  const handleImport = async () => {
+    upload.value?.submit();
+  };
+
+  // 上传文件之前的钩子,参数为上传的文件。即上传之前验证文件类型/后缀
+  const beforeUpload = (file) => {
+    const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase());
+    if (!isExcel) {
+      // 提示用户选择正确的文件类型
+      ElMessage({
+        message: '仅支持上传.xlsx .xls格式文件',
+        type: 'error',
+      });
+      return false; // 阻止上传
+    }
+    return true; // 允许上传
+  };
+
+  function mergeFailReasons(data: ErrorDetailItem[]) {
+    const grouped = data.reduce((acc, item) => {
+      if (!acc[item.rowNum]) {
+        acc[item.rowNum] = [];
+      }
+      // 避免重复添加
+      if (!acc[item.rowNum].includes(item.failReason)) {
+        acc[item.rowNum].push(item.failReason);
+      }
+      return acc;
+    }, {} as Record<string, string[]>);
+
+    return Object.entries(grouped).map(([rowNum, reasons]) => ({
+      rowNum: parseInt(rowNum),
+      failReason: reasons.join('、'),
+    }));
+  }
+
+  const handleUploadSuccess = (response: { data: ImportResponseData }) => {
+    sucCount.value = response.data.successCount;
+    errCount.value = response.data.failCount;
+    errDetail.value = response.data.failInfoList;
+
+    try {
+      if (sucCount.value != 0 && errCount.value === 0 && errDetail.value.length === 0) {
+        ElMessage({
+          message: props.successMessage,
+          type: 'success',
+        });
+        emit('update');
+        emit('close');
+      } else {
+        dialogVisibleErr.value = true; // 显示错误dialog
+        errDetail.value = mergeFailReasons(errDetail.value);
+      }
+    } catch (error) {
+      ElMessage({
+        message: '导入失败',
+        type: 'error',
+      });
+      emit('update');
+    }
+  };
+
+  const handleErrConfirm = () => {
+    dialogVisibleErr.value = false;
+    emit('update');
+    emit('close');
+  };
+
+  // 当超出只能上传一个文件的限制时,自动替换上一个文件
+  const handleExceed: UploadProps['onExceed'] = (files) => {
+    upload.value?.clearFiles();
+    const file = files[0] as UploadRawFile;
+    file.uid = genFileId();
+    upload.value?.handleStart(file);
+  };
+
+  const handleChange = () => {
+    isImportDisable.value = false;
+  };
+
+  const handleRemove = () => {
+    isImportDisable.value = true;
+  };
+
+  const handleClose = () => {
+    emit('close');
+  };
+
+  const handleUpdate = () => {
+    emit('update');
+  };
+</script>
+
+<style scoped lang="scss">
+  .overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 999;
+  }
+
+  .pop-card {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1000;
+  }
+
+  .upload-content {
+    margin: 40px 60px 10px 60px;
+  }
+
+  :deep(.el-card) {
+    .el-card__header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+  }
+
+  :deep(.el-dialog) {
+    padding: 0px;
+    border-radius: 5px;
+
+    .el-dialog__header {
+      display: flex;
+      align-items: flex-end;
+      height: 70px;
+      padding: 0px 0px 15px 15px;
+      border-bottom: 1px solid #e7e7e7;
+
+      .header-text {
+        font-size: 20px;
+      }
+    }
+
+    .el-dialog__headerbtn {
+      top: 15px;
+      right: 3px;
+
+      .el-dialog__close {
+        font-size: 20px;
+        color: black;
+      }
+    }
+
+    .el-dialog__body {
+      padding: 20px;
+
+      .sum-count {
+        font-size: 20px;
+        display: flex;
+        justify-content: center;
+        margin-bottom: 20px;
+
+        .succ-sum {
+          color: #52c41a;
+        }
+
+        .err-sum {
+          color: #ff4d4f;
+        }
+      }
+    }
+
+    .el-dialog__footer {
+      margin: 0 20px 20px 0;
+    }
+  }
+</style>

+ 2 - 0
src/components/batch-import/index.ts

@@ -0,0 +1,2 @@
+export { default as BatchImport } from './BatchImport.vue';
+export type { BatchImportProps, ErrorDetailItem, ImportResponseData } from './types';

+ 42 - 0
src/components/batch-import/types.ts

@@ -0,0 +1,42 @@
+/**
+ * 批量导入组件的属性定义
+ */
+export interface BatchImportProps {
+  /**
+   * 是否显示组件
+   */
+  visible: boolean;
+  /**
+   * 导入接口URL
+   */
+  importApiUrl: string;
+  /**
+   * 模板下载URL
+   */
+  templateUrl: string;
+  /**
+   * 模板名称
+   */
+  templateName: string;
+  /**
+   * 成功消息文本
+   */
+  successMessage?: string;
+}
+
+/**
+ * 错误详情项
+ */
+export interface ErrorDetailItem {
+  rowNum: number;
+  failReason: string;
+}
+
+/**
+ * 导入响应数据
+ */
+export interface ImportResponseData {
+  successCount: number;
+  failCount: number;
+  failInfoList: ErrorDetailItem[];
+}

+ 19 - 4
src/utils/dateUtil.ts

@@ -3,13 +3,28 @@ import dayjs, { Dayjs } from 'dayjs';
 const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
 const DATE_FORMAT = 'YYYY-MM-DD ';
 
-export function formatToDateTime(
-  date: Date | Dayjs | string,
-  formatStr = DATE_TIME_FORMAT,
-): string {
+export function formatToDateTime(date: Date | Dayjs | string, formatStr = DATE_TIME_FORMAT): string {
   return dayjs(date).format(formatStr);
 }
 
 export function formatToDate(date: Date | Dayjs | string, formatStr = DATE_FORMAT): string {
   return dayjs(date).format(formatStr);
 }
+
+/**
+ * 获取当前时间的年月日时分格式字符串
+ * 格式:YYYYMMDDHHmm (例如:202509051538)
+ * @returns 格式化后的时间字符串
+ */
+export const getCurrentDateTimeString = (): string => {
+  const now = new Date();
+  const year = now.getFullYear().toString();
+  // 月、日、时、分需要补零,确保是两位数
+  const month = String(now.getMonth() + 1).padStart(2, '0');
+  const day = String(now.getDate()).padStart(2, '0');
+  const hours = String(now.getHours()).padStart(2, '0');
+  const minutes = String(now.getMinutes()).padStart(2, '0');
+
+  // 拼接成指定格式
+  return year + month + day + hours + minutes;
+};

+ 21 - 0
src/views/traffic/Accident/constant/index.ts

@@ -0,0 +1,21 @@
+// 查询字段对应:1-事故地点,2-事故描述
+export enum FIELDTYPE {
+  LOCATION = '1',
+  DESCRIPTION = '2',
+}
+
+export const FIELD_CONTENT = {
+  [FIELDTYPE.LOCATION]: '事故地点',
+  [FIELDTYPE.DESCRIPTION]: '事故描述',
+};
+
+export const FIELD_CONTENT_OPTIONS = [
+  {
+    label: FIELD_CONTENT[FIELDTYPE.LOCATION],
+    value: FIELDTYPE.LOCATION,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.LOCATION],
+    value: FIELDTYPE.LOCATION,
+  },
+];

+ 7 - 0
src/views/traffic/constant/index.ts

@@ -0,0 +1,7 @@
+// 交通管理权限
+export const TRAFFIC_PERMISSIONS = {
+  // 总览——当月车辆违规数量编辑权限
+  EDIT_VEHICLE_VIOLATIONS: 'traffic_business_module:edit_vehicle_violations',
+  // 车辆信息管理——车辆信息记录管理权限
+  VEHICLE_MANAGE: 'traffic_business_module:vehicle_manage',
+};

+ 61 - 3
src/views/traffic/overview/Overview.vue

@@ -1,7 +1,65 @@
 <template>
-  <div> </div>
+  <div class="traffic-overview">
+    <div class="traffic-overview-top">
+      <ViolationStatistics class="violation" />
+      <RegulationList class="regulation" />
+    </div>
+    <div class="traffic-overview-bottom">
+      <ViolationRecords class="violation-records" />
+      <AccidentRecords class="accident-records" />
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import ViolationStatistics from './components/ViolationStatistics.vue';
+  import RegulationList from './components/RegulationList.vue';
+  import ViolationRecords from './components/ViolationRecords.vue';
+  import AccidentRecords from './components/AccidentRecords.vue';
+</script>
 
-<style scoped></style>
+<style scoped lang="scss">
+  .traffic-overview {
+    width: 100%;
+    height: 100%;
+  }
+
+  .traffic-overview-top {
+    width: 100%;
+    height: 174px;
+    margin-bottom: 10px;
+    display: flex;
+
+    .violation {
+      width: 70%;
+      height: 100%;
+      background-color: #fff;
+      border-radius: 4px;
+      margin-right: 10px;
+    }
+
+    .regulation {
+      width: calc(30% - 10px);
+      height: 100%;
+      background-color: #fff;
+      border-radius: 4px;
+    }
+  }
+
+  .traffic-overview-bottom {
+    width: 100%;
+    height: calc(100% - 174px - 10px);
+    background-color: #fff;
+    border-radius: 4px;
+
+    .violation-records {
+      width: 100%;
+      height: 50%;
+    }
+
+    .accident-records {
+      width: 100%;
+      height: 50%;
+    }
+  }
+</style>

+ 76 - 0
src/views/traffic/overview/components/AccidentRecords.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="accident-records-container">
+    <div class="container-title">
+      <span class="line"></span>
+      <span class="title">本月交通事故记录</span>
+      <span class="more" @click="handleClickMore"
+        >查看全部<el-icon><ArrowRight /></el-icon
+      ></span>
+    </div>
+    <div class="table-container"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ElIcon } from 'element-plus';
+  import { ArrowRight } from '@element-plus/icons-vue';
+
+  const handleClickMore = () => {
+    // TODO: 点击跳转“管理规定与通知”菜单
+    console.log('1111111111111111111111');
+  };
+</script>
+
+<style scoped lang="scss">
+  .container-title {
+    height: 24px;
+    display: flex;
+    align-items: center;
+    font-weight: 500;
+    font-size: 16px;
+    color: #000000;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+      margin-right: 10px;
+    }
+
+    .title {
+      margin-right: 12px;
+    }
+
+    .more {
+      margin-left: auto;
+      margin-right: 17px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #1777ff;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .more:hover {
+      background-color: #1777ff;
+      color: #fff;
+      border-radius: 4px;
+      padding: 2px 4px 2px 6px;
+    }
+  }
+
+  .accident-records-container {
+    width: 100%;
+    height: 100%;
+    padding: 14px 0;
+
+    .table-container {
+      width: 100%;
+      height: calc(100% - 24px - 18px);
+      margin-top: 18px;
+      padding: 0 16px;
+      overflow: auto;
+    }
+  }
+</style>

+ 123 - 0
src/views/traffic/overview/components/RegulationList.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="regulation-list-container">
+    <div class="container-title">
+      <span class="line"></span>
+      <span class="title">交通管理规定与通知</span>
+      <span class="more" @click="handleClickMore"
+        >更多<el-icon><ArrowRight /></el-icon
+      ></span>
+    </div>
+    <div class="regulation-list">
+      <div
+        class="regulation-item"
+        v-for="(item, index) in regulationList"
+        :key="index"
+        :title="item.title"
+        @click="handleClick(item)"
+      >
+        {{ item.title }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { ElIcon } from 'element-plus';
+  import { ArrowRight } from '@element-plus/icons-vue';
+
+  const regulationList = ref([
+    {
+      id: 1,
+      title: '应急预案',
+    },
+    {
+      id: 2,
+      title: '长文案去去去去去去去去去前期前期前期前期前期前期前期前期前期',
+    },
+    {
+      id: 3,
+      title: '交通管理通知是指在交通管理中,为了保障交通安全、有序进行而发布的一系列通知。',
+    },
+  ]);
+
+  const handleClickMore = () => {
+    // TODO: 点击跳转“管理规定与通知”菜单
+    console.log('1111111111111111111111');
+  };
+
+  const handleClick = (item) => {
+    // TODO: 点击跳转到具体详情页
+    console.log(item);
+  };
+</script>
+
+<style scoped lang="scss">
+  .container-title {
+    height: 24px;
+    display: flex;
+    align-items: center;
+    font-weight: 500;
+    font-size: 16px;
+    color: #000000;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+      margin-right: 10px;
+    }
+
+    .title {
+      margin-right: 12px;
+    }
+
+    .more {
+      margin-left: auto;
+      margin-right: 17px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #1777ff;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .more:hover {
+      background-color: #1777ff;
+      color: #fff;
+      border-radius: 4px;
+      padding: 2px 4px 2px 6px;
+    }
+  }
+
+  .regulation-list-container {
+    width: 100%;
+    height: 100%;
+    padding-top: 14px;
+
+    .regulation-list {
+      width: 100%;
+      height: calc(100% - 24px - 18px);
+      margin-top: 18px;
+      padding: 0 16px;
+
+      .regulation-item {
+        width: 100%;
+        height: 33px;
+        font-size: 16px;
+        color: #333333;
+        border-bottom: 1px solid #ededed;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        overflow: hidden;
+        margin-bottom: 10px;
+        cursor: pointer;
+      }
+
+      .regulation-item:hover {
+        color: #1777ff;
+      }
+    }
+  }
+</style>

+ 76 - 0
src/views/traffic/overview/components/ViolationRecords.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="violation-records-container">
+    <div class="container-title">
+      <span class="line"></span>
+      <span class="title">本月车辆违规记录</span>
+      <span class="more" @click="handleClickMore"
+        >查看全部<el-icon><ArrowRight /></el-icon
+      ></span>
+    </div>
+    <div class="table-container"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ElIcon } from 'element-plus';
+  import { ArrowRight } from '@element-plus/icons-vue';
+
+  const handleClickMore = () => {
+    // TODO: 点击跳转“管理规定与通知”菜单
+    console.log('1111111111111111111111');
+  };
+</script>
+
+<style scoped lang="scss">
+  .container-title {
+    height: 24px;
+    display: flex;
+    align-items: center;
+    font-weight: 500;
+    font-size: 16px;
+    color: #000000;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+      margin-right: 10px;
+    }
+
+    .title {
+      margin-right: 12px;
+    }
+
+    .more {
+      margin-left: auto;
+      margin-right: 17px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #1777ff;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .more:hover {
+      background-color: #1777ff;
+      color: #fff;
+      border-radius: 4px;
+      padding: 2px 4px 2px 6px;
+    }
+  }
+
+  .violation-records-container {
+    width: 100%;
+    height: 100%;
+    padding: 14px 0;
+
+    .table-container {
+      width: 100%;
+      height: calc(100% - 24px - 18px);
+      margin-top: 18px;
+      padding: 0 16px;
+      overflow: auto;
+    }
+  }
+</style>

+ 243 - 0
src/views/traffic/overview/components/ViolationStatistics.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="violation-statistics-container">
+    <div class="container-title">
+      <span class="line"></span>
+      <span class="title">违规行为统计</span>
+    </div>
+    <div class="violation-statistics-content">
+      <div class="violation-statistics-item vehicle-count">
+        <span class="item-title">当月车辆违规数</span>
+        <span class="item-count">{{ vehicleCount }}</span>
+        <img class="item-icon" src="@/assets/images/traffic-overview/vehicle-icon.png" alt="" />
+        <div class="edit-icon" v-if="editVehicleViolations" @click="handleOpenEditDialog">
+          <el-icon><Edit /></el-icon>
+          编辑
+        </div>
+      </div>
+      <div class="violation-statistics-item regulation-count">
+        <span class="item-title">当月违规通知数</span>
+        <span class="item-count">{{ regulationCount }}</span>
+        <img class="item-icon" src="@/assets/images/traffic-overview/regulation-icon.png" alt="" />
+      </div>
+      <div class="violation-statistics-item accident-count">
+        <span class="item-title">当月交通事故数</span>
+        <span class="item-count">{{ accidentCount }}</span>
+        <img class="item-icon" src="@/assets/images/traffic-overview/accident-icon.png" alt="" />
+      </div>
+    </div>
+    <el-dialog v-model="dialogVisible" width="424" center align-center class="violation-statistics-dialog">
+      <div class="dialog-content">
+        <span class="dialog-content-title">当月违章</span>
+        <el-input v-model="vehicleCountCopy" class="input-count" />
+        <div v-if="showError" class="error-message">需≥{{ regulationCount }}</div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="handleConfirm" :disabled="!isValid"> 确认 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, watch } from 'vue';
+  import { ElIcon, ElDialog, ElInput } from 'element-plus';
+  import { Edit } from '@element-plus/icons-vue';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { TRAFFIC_PERMISSIONS } from '@/views/traffic/constant';
+
+  const { permissions } = useUserInfoHook();
+  const editVehicleViolations = ref<boolean>();
+
+  const vehicleCount = ref<number>(1111);
+  const regulationCount = ref<number>(2222);
+  const accidentCount = ref<number>(3333);
+
+  const vehicleCountCopy = ref<number>(0);
+  const dialogVisible = ref<boolean>(false);
+  const isValid = ref<boolean>(true);
+  const showError = ref<boolean>(false);
+
+  const handleOpenEditDialog = () => {
+    dialogVisible.value = true;
+    vehicleCountCopy.value = JSON.parse(JSON.stringify(vehicleCount.value));
+  };
+
+  const validateCount = (value: number) => {
+    const valid = value >= regulationCount.value;
+    isValid.value = valid;
+    showError.value = !valid;
+  };
+
+  watch(vehicleCountCopy, (newVal) => {
+    validateCount(newVal);
+  });
+
+  const handleConfirm = async () => {
+    if (!isValid.value) return;
+    // TODO: 调用后端API更新数据,重新调用接口获取数据
+    dialogVisible.value = false;
+  };
+
+  onMounted(() => {
+    editVehicleViolations.value = Boolean(
+      permissions.find((item: { code: string }) => item.code === TRAFFIC_PERMISSIONS.EDIT_VEHICLE_VIOLATIONS),
+    );
+  });
+</script>
+
+<style scoped lang="scss">
+  .container-title {
+    height: 24px;
+    display: flex;
+    align-items: center;
+    font-weight: 500;
+    font-size: 16px;
+    color: #000000;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+      margin-right: 10px;
+    }
+
+    .title {
+      margin-right: 12px;
+    }
+  }
+
+  .violation-statistics-container {
+    width: 100%;
+    height: 100%;
+    padding-top: 14px;
+  }
+
+  .violation-statistics-content {
+    width: 100%;
+    height: calc(100% - 24px);
+    padding: 20px;
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    grid-gap: 20px;
+  }
+
+  .violation-statistics-item {
+    border-radius: 8px;
+    color: #ffffff;
+    padding: 12px 16px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    position: relative;
+
+    .item-title {
+      font-size: 14px;
+      font-weight: 400;
+    }
+
+    .item-count {
+      font-size: 30px;
+      font-weight: bold;
+    }
+
+    .item-icon {
+      position: absolute;
+      top: 50%;
+      right: 16px;
+      width: 48px;
+      height: 48px;
+      transform: translateY(-50%);
+    }
+
+    .edit-icon {
+      position: absolute;
+      top: 0;
+      right: 0;
+      font-size: 12px;
+      color: #ffffff;
+      background: rgba(255, 255, 255, 0.29);
+      border-radius: 0px 8px 0px 8px;
+      padding: 3px 6px;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+    }
+
+    .edit-icon:hover {
+      background: rgba(0, 0, 0, 0.3);
+    }
+  }
+
+  .vehicle-count {
+    background-image: url('@/assets/images/traffic-overview/vehicle-bg-pure.png');
+    background-size: 100% 100%;
+    background-position: center;
+    background-repeat: no-repeat;
+  }
+
+  .regulation-count {
+    background-image: url('@/assets/images/traffic-overview/regulation-bg-pure.png');
+    background-size: 100% 100%;
+    background-position: center;
+    background-repeat: no-repeat;
+  }
+
+  .accident-count {
+    background-image: url('@/assets/images/traffic-overview/accident-bg-pure.png');
+    background-size: 100% 100%;
+    background-position: center;
+    background-repeat: no-repeat;
+  }
+
+  :deep(.violation-statistics-dialog) {
+    .el-dialog__header {
+      display: none;
+    }
+
+    .dialog-content {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin: 18px 0 4px 0;
+    }
+
+    .dialog-content-title {
+      font-size: 16px;
+      font-weight: 400;
+      color: #000000;
+    }
+
+    .dialog-footer {
+      margin-bottom: 4px;
+    }
+  }
+
+  .input-count {
+    width: 188px;
+    border-bottom: 1px solid rgba(151, 151, 151, 0.2);
+    margin-top: 20px;
+  }
+
+  :deep(.el-input) {
+    --el-input-border-color: transparent;
+    --el-input-hover-border-color: transparent;
+    --el-input-focus-border-color: transparent;
+
+    .el-input__inner {
+      font-weight: bold;
+      font-size: 24px;
+      color: #000000;
+      text-align: center;
+    }
+  }
+
+  .error-message {
+    font-size: 12px;
+    color: #ff4d4f;
+    margin-top: 4px;
+  }
+</style>

+ 376 - 3
src/views/traffic/vehicle/Vehicle.vue

@@ -1,7 +1,380 @@
 <template>
-  <div> </div>
+  <div class="safety-platform-container">
+    <div class="safety-platform-container__header">
+      <div class="breadcrumb-title">车辆信息管理</div>
+    </div>
+    <div class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header class="disaster-precaution__header">
+          <el-button
+            v-if="vehicleManagePermission"
+            class="search-table-container--button"
+            type="primary"
+            :icon="Plus"
+            @click="handleAddVehicleInfo"
+          >
+            添加车辆信息记录
+          </el-button>
+          <el-button
+            v-if="vehicleManagePermission"
+            class="search-table-container--button"
+            @click="batchImportVisible = true"
+          >
+            批量导入
+          </el-button>
+          <div class="search-container">
+            <el-input
+              v-model="searchKeyword"
+              :placeholder="`请输入${curSearchTypeLabel}进行搜索`"
+              clearable
+              @input="handleSearch"
+              @clear="handleClear"
+              @keyup.enter="handleSearch"
+              style="width: 340px"
+            >
+              <template #prefix>
+                <el-icon color="#1777ff"><Search /></el-icon>
+              </template>
+              <template #prepend>
+                <el-select
+                  v-model="searchSelectedType"
+                  placeholder="选择搜索项"
+                  @change="handleSelectedTypeChange"
+                  style="width: 90px"
+                >
+                  <el-option
+                    v-for="item in vehicleQueryOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </template>
+            </el-input>
+            <div class="search-container-btn">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+              <el-button @click="handleExport">导出</el-button>
+            </div>
+          </div>
+        </header>
+        <div class="batch-table">
+          <div class="batch-operation--div" v-show="vehicleManagePermission && 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="handleBatchDelete">批量删除</el-button>
+              </div>
+              <el-icon class="close-icon" @click="handleCloseBatchOperation"><Close /></el-icon>
+            </div>
+          </div>
+          <BasicTable
+            ref="basicTableRef"
+            :tableData="tableData"
+            :tableConfig="tableConfig"
+            @update:page-number="handleCurrentPageChange"
+            @update:page-size="handlePageSizeChange"
+            @update:selection="handleSelectionChange"
+          >
+            <template #action="scope">
+              <div class="action-container--div">
+                <ActionButton text="编辑" @click="handleEditVehicleInfo(scope.row)" />
+                <ActionButton
+                  text="删除"
+                  :popconfirm="{
+                    title: '是否删除该车辆信息?',
+                  }"
+                  @confirm="handleDeleteVehicleInfo(scope.row)"
+                />
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+    </div>
+  </div>
+  <ManageDrawer v-if="drawerVisible" :type="drawerType" :initial-data="initialData" @close="handleCloseDrawer" />
+  <!-- <BatchImport
+    class="batch-import"
+    v-if="showBatchImportPopover"
+    @update="handleUpdateBatchImport"
+    @close="handleCloseBatchImport"
+  /> -->
+  <BatchImport
+    :visible="batchImportVisible"
+    :importApiUrl="importApiUrl"
+    :templateUrl="templateUrl"
+    :templateName="'车辆信息管理-批量导入模版'"
+    :successMessage="'车辆信息添加成功'"
+    @close="() => (batchImportVisible = false)"
+    @update="handleUpdate"
+  />
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { computed, onMounted, ref } from 'vue';
+  import urlJoin from 'url-join';
+  import { ElMessage } from 'element-plus';
+  import { Plus, Search, Close } from '@element-plus/icons-vue';
+  import { openMessageBox } from '@/utils/element-plus/messageBox';
+  import BasicTable from '@/components/BasicTable.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import ManageDrawer from './components/ManageDrawer.vue';
+  import { BatchImport } from '@/components/batch-import';
+  import { downloadByData } from '@/utils/file/download';
+  import { msgConfirm } from '@/utils/element-plus/messageBox';
+  import { getCurrentDateTimeString } from '@/utils/dateUtil';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import { useGlobSetting } from '@/hooks/setting';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { TRAFFIC_PERMISSIONS } from '@/views/traffic/constant';
+  import {
+    VEHICLE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+    VEHICLE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+    VEHICLE_LIST_TABLE_OPTIONS,
+    VEHICLE_LIST_TABLE_COLUMNS,
+  } from './config';
+  import { FIELDTYPE, FIELD_CONTENT, vehicleQueryOptions } from './constant';
+  import {
+    VehicleListQuery,
+    VehicleInfoStruct,
+    getVehicleInfoList,
+    deleteVehicleInfo,
+    exportVehicleInfo,
+  } from '@/api/traffic-vehicle';
 
-<style scoped></style>
+  const { tableConfig, pagination } = useTableConfig(VEHICLE_LIST_TABLE_COLUMNS, VEHICLE_LIST_TABLE_OPTIONS);
+
+  const { permissions } = useUserInfoHook();
+  const vehicleManagePermission = ref<boolean>(false);
+
+  const searchSelectedType = ref(FIELDTYPE.VEHICLE_NUMBER);
+  const searchKeyword = ref('');
+  const curSearchTypeLabel = computed(() => {
+    const option = vehicleQueryOptions.find((item) => item.value === searchSelectedType.value);
+    return option ? option.label : FIELD_CONTENT[searchSelectedType.value];
+  });
+
+  const vehicleTableQuery: QueryPageRequest<VehicleListQuery> = {
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  };
+
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+  const tableData = ref<VehicleInfoStruct[]>([]);
+
+  const selectionItems = ref<any[]>([]);
+
+  // add or edit
+  const drawerVisible = ref(false);
+  const drawerType = ref('');
+  const initialData = ref<VehicleInfoStruct>({});
+
+  const handleSelectedTypeChange = () => {
+    searchKeyword.value = '';
+  };
+
+  const handleSearch = () => {
+    if (!searchKeyword.value.trim()) {
+      handleClear();
+      return;
+    }
+    getTableData();
+  };
+
+  const handleClear = () => {
+    searchKeyword.value = '';
+    vehicleTableQuery.pageNumber = 1;
+    vehicleTableQuery.pageSize = 20;
+    getTableData();
+  };
+
+  const handleReset = () => {
+    searchSelectedType.value = FIELDTYPE.VEHICLE_NUMBER;
+    searchKeyword.value = '';
+    vehicleTableQuery.pageNumber = 1;
+    vehicleTableQuery.pageSize = 20;
+    getTableData();
+  };
+
+  // 导出
+  const handleExport = () => {
+    msgConfirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        exportVehicleInfo(vehicleTableQuery.queryParam).then(async (responnse) => {
+          if (!responnse) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(responnse, `车辆信息记录_${getCurrentDateTimeString()}.xlsx`);
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+  };
+
+  const handleCurrentPageChange = (pageNumber: number) => {
+    pagination.pageNumber = pageNumber;
+    vehicleTableQuery.pageNumber = pageNumber;
+    getTableData();
+  };
+
+  const handlePageSizeChange = (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    vehicleTableQuery.pageSize = pageSize;
+    getTableData();
+  };
+
+  const handleAddVehicleInfo = () => {
+    drawerVisible.value = true;
+    drawerType.value = 'add';
+    initialData.value = {};
+  };
+
+  const handleEditVehicleInfo = (row: VehicleInfoStruct) => {
+    drawerVisible.value = true;
+    drawerType.value = 'edit';
+    initialData.value = row;
+  };
+
+  const handleCloseDrawer = () => {
+    drawerVisible.value = false;
+    vehicleTableQuery.pageNumber = 1;
+    vehicleTableQuery.pageSize = 20;
+    getTableData();
+  };
+
+  const handleDeleteVehicleInfo = (row: VehicleInfoStruct) => {
+    if (!row.id) return;
+    deleteVehicleInfo({ vehicleInfoIds: [row.id] }).then(() => {
+      ElMessage.success('删除成功');
+      vehicleTableQuery.pageNumber = 1;
+      vehicleTableQuery.pageSize = 20;
+      getTableData();
+    });
+  };
+
+  // 批量删除
+  const handleSelectionChange = (selection: any[]) => {
+    selectionItems.value = selection;
+  };
+
+  const handleCloseBatchOperation = () => {
+    if (!basicTableRef.value) return;
+    basicTableRef.value.clearSelection();
+  };
+
+  const handleBatchDelete = async () => {
+    const confirmed = await openMessageBox('', '删除后信息不可恢复,确认删除吗?', 'warning');
+    if (!confirmed) return;
+    const deleteIds = selectionItems.value.map((item) => item.id);
+    if (!deleteIds.length) return;
+    deleteVehicleInfo({ vehicleInfoIds: deleteIds }).then(() => {
+      ElMessage.success('批量删除成功');
+      vehicleTableQuery.pageNumber = 1;
+      vehicleTableQuery.pageSize = 20;
+      getTableData();
+    });
+  };
+
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  const importApiUrl = ref(urlJoin(urlPrefix, '/vehicleInfo/importVehicleInfo'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-vehicle-template.xlsx');
+
+  const handleUpdate = () => {
+    batchImportVisible.value = false;
+    vehicleTableQuery.pageNumber = 1;
+    vehicleTableQuery.pageSize = 20;
+    getTableData();
+  };
+
+  const getTableData = () => {
+    tableConfig.loading = true;
+    vehicleTableQuery.queryParam = {
+      fieldType: searchSelectedType.value,
+      fieldContent: searchKeyword.value,
+    };
+    getVehicleInfoList(vehicleTableQuery).then((res) => {
+      tableData.value = res?.records || [];
+      pagination.total = res?.totalRow || 0;
+    });
+    tableConfig.loading = false;
+  };
+
+  // 动态生成表格列配置
+  const getTableColumns = () => {
+    if (vehicleManagePermission.value) {
+      return VEHICLE_LIST_TABLE_COLUMNS;
+    } else {
+      // 过滤掉操作列
+      return VEHICLE_LIST_TABLE_COLUMNS.filter((column) => column.prop !== 'action');
+    }
+  };
+
+  onMounted(() => {
+    getTableData();
+    vehicleManagePermission.value = Boolean(
+      permissions.find((item: { code: string }) => item.code === TRAFFIC_PERMISSIONS.VEHICLE_MANAGE),
+    );
+    tableConfig.maxHeight = vehicleManagePermission.value
+      ? VEHICLE_LIST_TABLE_MAX_HEIGHT_PERMISSION
+      : VEHICLE_LIST_TABLE_MAX_HEIGHT_DEFAULT;
+    tableConfig.columns = getTableColumns();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+
+  .search-container {
+    display: flex;
+
+    .search-container-btn {
+      margin-left: auto;
+    }
+  }
+
+  .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>

+ 304 - 0
src/views/traffic/vehicle/components/BatchImport.vue

@@ -0,0 +1,304 @@
+<template>
+  <div>
+    <div class="overlay" v-if="cardVisible"></div>
+    <el-card v-if="cardVisible" class="pop-card">
+      <template #header>
+        <div style="font-size: 16px; font-weight: 600">批量导入</div>
+        <el-icon
+          :size="18"
+          @click="
+            () => {
+              emits('close');
+            }
+          "
+          style="cursor: pointer"
+        >
+          <Close />
+        </el-icon>
+      </template>
+      <div class="upload-content">
+        <el-upload
+          ref="upload"
+          style="width: 384px; height: 192px; border-radius: 5px"
+          :headers="getHeaders()"
+          :multiple="false"
+          :limit="1"
+          drag
+          :action="actionUrl"
+          :with-credentials="true"
+          :auto-upload="false"
+          :before-upload="beforeUpload"
+          :on-success="handleUploadSuccess"
+          :on-exceed="handleExceed"
+          :on-change="handleChange"
+          :on-remove="handleRemove"
+        >
+          <el-icon class="el-icon--upload" style="width: 33px; height: 42px; color: #409efc">
+            <Document />
+          </el-icon>
+          <div class="el-upload__text">
+            <div style="font-size: 12px; color: red; margin-bottom: 5px">请下载模板并按要求填写后上传</div>
+            <div style="font-size: 16px">点击或将文件拖拽到这里上传</div>
+            <div style="font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: 5px"
+              >文件支持.xlsx .xls格式,仅支持上传一个文件</div
+            >
+          </div>
+        </el-upload>
+        <div style="margin-top: 72px; display: flex; justify-content: flex-end">
+          <el-button @click="handleDownloadTemplate">下载模板</el-button>
+          <el-button type="primary" @click="handleImport" :disabled="isImportDisable">导入</el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-dialog
+      v-model="DialogVisibleErr"
+      title="Warning"
+      width="544"
+      align-center
+      @close="
+        () => {
+          emits('update');
+        }
+      "
+    >
+      <template #header>
+        <el-icon :size="24" color="#f2b20a" style="margin: 0 5px 2px">
+          <WarnTriangleFilled />
+        </el-icon>
+        <div class="header-text">导入提示</div>
+      </template>
+      <div class="sum-count">
+        成功导入 <span class="succ-sum">{{ sucCount }}</span> 条, 失败 <span class="err-sum">{{ errCount }}</span> 条
+      </div>
+      <el-table :data="errDetail" style="width: 100%" height="190">
+        <el-table-column prop="rowNum" label="行数" width="100" fixed align="center" />
+        <el-table-column prop="failReason" label="失败原因" show-overflow-tooltip />
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="handleErrComfirm"> 确定 </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import urlJoin from 'url-join';
+  import axios, { AxiosRequestConfig } from 'axios';
+  import { getHeaders } from '@/utils/http/axios';
+  import { genFileId, ElMessage } from 'element-plus';
+  import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+  import { Close, Document, WarnTriangleFilled } from '@element-plus/icons-vue';
+  import { useGlobSetting } from '@/hooks/setting';
+
+  const emits = defineEmits(['close', 'update']);
+
+  const upload = ref<UploadInstance>();
+  const cardVisible = ref<boolean>(true);
+  const isImportDisable = ref<boolean>(true);
+  const DialogVisibleErr = ref<boolean>(false);
+  const sucCount = ref<number>(0);
+  const errCount = ref<number>(0);
+  const errDetail = ref<{ rowNum: number; failReason: string }[]>([]);
+
+  const { urlPrefix } = useGlobSetting();
+
+  const actionUrl = computed(() => {
+    return urlJoin(urlPrefix, `/vehicleInfo/importVehicleInfo`);
+  });
+
+  // 下载模板
+  const handleDownloadTemplate = async () => {
+    try {
+      const config: AxiosRequestConfig = {
+        headers: getHeaders(),
+        responseType: 'blob',
+      };
+      const response = await axios.get(
+        './skyeye-file-upload/sfysecurity/TEMPLATE/import-vehicle-template.xlsx',
+        config,
+      );
+      const blob = new Blob([response.data], {
+        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+      });
+      // 创建下载链接
+      let downloadLink: HTMLAnchorElement | null = document.createElement('a');
+      const url = window.URL.createObjectURL(blob);
+      downloadLink.href = url;
+      downloadLink.download = '车辆信息管理-批量导入模版.xlsx';
+      downloadLink.click();
+      // 移除下载链接
+      window.URL.revokeObjectURL(url);
+      downloadLink = null;
+    } catch (error) {
+      console.error('Error downloading file:', error);
+    }
+  };
+
+  // 导入
+  const handleImport = async () => {
+    upload.value!.submit();
+  };
+
+  // 上传文件之前的钩子,参数为上传的文件。即上传之前验证文件类型/后缀
+  const beforeUpload = (file) => {
+    const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase());
+    if (!isExcel) {
+      // 提示用户选择正确的文件类型
+      ElMessage({
+        message: '仅支持上传.xlsx .xls格式文件',
+        type: 'error',
+      });
+      return false; // 阻止上传
+    }
+    return true; // 允许上传
+  };
+
+  function mergeFailReasons(data) {
+    const grouped = data.reduce((acc, item) => {
+      if (!acc[item.rowNum]) {
+        acc[item.rowNum] = [];
+      }
+      // 避免重复添加
+      if (!acc[item.rowNum].includes(item.failReason)) {
+        acc[item.rowNum].push(item.failReason);
+      }
+      return acc;
+    }, {});
+
+    return Object.entries(grouped).map(([rowNum, reasons]) => ({
+      rowNum: parseInt(rowNum),
+      failReason: (reasons as string[]).join('、'),
+    }));
+  }
+
+  const handleUploadSuccess = (response) => {
+    sucCount.value = response.data.successCount;
+    errCount.value = response.data.failCount;
+    errDetail.value = response.data.failInfoList;
+
+    try {
+      if (sucCount.value != 0 && errCount.value === 0 && errDetail.value.length === 0) {
+        ElMessage({
+          message: '添加成功', // 1.全部添加成功 —— failCount === 0
+          type: 'success',
+        });
+        emits('update');
+      } else {
+        DialogVisibleErr.value = true; // 2.有错误 —— 显示错误dialog
+        errDetail.value = mergeFailReasons(errDetail.value);
+      }
+      cardVisible.value = false;
+    } catch (error) {
+      ElMessage({
+        message: response.message,
+        type: 'error',
+      });
+      emits('update');
+    }
+  };
+
+  const handleErrComfirm = () => {
+    DialogVisibleErr.value = false;
+    emits('update');
+  };
+
+  // 当超出只能上传一个文件的限制时,自动替换上一个文件
+  const handleExceed: UploadProps['onExceed'] = (files) => {
+    upload.value!.clearFiles();
+    const file = files[0] as UploadRawFile;
+    file.uid = genFileId();
+    upload.value!.handleStart(file);
+  };
+
+  const handleChange = () => {
+    isImportDisable.value = false;
+  };
+
+  const handleRemove = () => {
+    isImportDisable.value = true;
+  };
+</script>
+
+<style scoped lang="scss">
+  .overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 999;
+  }
+
+  .pop-card {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1000;
+  }
+
+  .upload-content {
+    margin: 40px 60px 10px 60px;
+  }
+
+  :deep(.el-card) {
+    .el-card__header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+  }
+
+  :deep(.el-dialog) {
+    padding: 0px;
+    border-radius: 5px;
+
+    .el-dialog__header {
+      display: flex;
+      align-items: flex-end;
+      height: 70px;
+      padding: 0px 0px 15px 15px;
+      border-bottom: 1px solid #e7e7e7;
+
+      .header-text {
+        font-size: 20px;
+      }
+    }
+
+    .el-dialog__headerbtn {
+      top: 15px;
+      right: 3px;
+
+      .el-dialog__close {
+        font-size: 20px;
+        color: black;
+      }
+    }
+
+    .el-dialog__body {
+      padding: 20px;
+
+      .sum-count {
+        font-size: 20px;
+        display: flex;
+        justify-content: center;
+        margin-bottom: 20px;
+
+        .succ-sum {
+          color: #52c41a;
+        }
+
+        .err-sum {
+          color: #ff4d4f;
+        }
+      }
+    }
+
+    .el-dialog__footer {
+      margin: 0 20px 20px 0;
+    }
+  }
+</style>

+ 225 - 0
src/views/traffic/vehicle/components/ManageDrawer.vue

@@ -0,0 +1,225 @@
+<template>
+  <el-drawer
+    v-model="isDrawer"
+    size="480"
+    :title="props.type === 'add' ? '添加车辆信息记录' : '编辑车辆信息记录'"
+    @close="handleCloseDrawer"
+  >
+    <el-form ref="formRef" :model="formParams" :rules="rules" label-placement="left" :label-width="80">
+      <el-form-item label="车牌号" prop="carNum">
+        <el-input placeholder="请输入车牌号" v-model="formParams.carNum" />
+      </el-form-item>
+      <el-form-item label="工号" prop="staffNo">
+        <el-tree-select
+          v-model="formParams.staffNo"
+          placeholder="请输入工号进行搜索"
+          class="protocal-select"
+          filterable
+          remote
+          clearable
+          :loading="loading"
+          :data="staffNoOptions"
+          :render-after-expand="false"
+          :default-expand-all="true"
+          :remote-method="debouncedRemoteMethod"
+          @clear="handleClearStaffNo"
+          @change="handleChangeStaffNo"
+        />
+      </el-form-item>
+      <el-form-item label="姓名" prop="userName">
+        <el-input placeholder="请选择工号,此项自动填充" v-model="formParams.userName" disabled />
+      </el-form-item>
+      <el-form-item label="所属部门" prop="deptName">
+        <el-input placeholder="请选择工号,此项自动填充" v-model="formParams.deptName" disabled />
+      </el-form-item>
+      <el-form-item label="联系方式" prop="phoneNum">
+        <el-input placeholder="请选择工号,此项自动填充" v-model="formParams.phoneNum" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-space>
+        <el-button @click="handleCloseDrawer">取消</el-button>
+        <el-button type="primary" @click="handleSubmitForm(formRef)">提交</el-button>
+      </el-space>
+    </template>
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive, watch } from 'vue';
+  import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
+  import { debounce } from 'lodash-es';
+  import { queryOrganizationUserTree } from '@/api/system/user';
+  import { OrganizationUserTree } from '@/views/system/user/types';
+  import {
+    findUserByWorkNo,
+    transformTreeData,
+    TransformedTreeNode,
+    findOrgCodeByWorkNo,
+  } from '@/utils/findUserByWorkNo';
+  import { VehicleInfoStruct, addVehicleInfo, updateVehicleInfo } from '@/api/traffic-vehicle';
+
+  const props = defineProps<{
+    type: string; // add or edit
+    initialData: VehicleInfoStruct;
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'close'): void;
+  }>();
+
+  const formRef = ref<FormInstance>();
+  const formParams = ref<VehicleInfoStruct>({
+    id: 0,
+    carNum: '',
+    userId: 0,
+    userName: '',
+    staffNo: '',
+    deptId: 0,
+    deptName: '',
+    phoneNum: '',
+    createdAt: '',
+    updatedAt: '',
+    isDeleted: 0,
+  });
+
+  const rules = reactive<FormRules<VehicleInfoStruct>>({
+    carNum: [
+      { required: true, message: '车牌号不能为空', trigger: 'blur' },
+      { min: 1, max: 8, message: '车牌号格式错误(最长8位)', trigger: 'blur' },
+    ],
+    staffNo: {
+      required: true,
+      message: '工号不能为空',
+      trigger: 'blur',
+    },
+    userName: {
+      required: true,
+      message: '姓名不能为空',
+      trigger: 'blur',
+    },
+    deptName: {
+      required: true,
+      message: '所属部门不能为空',
+      trigger: 'blur',
+    },
+    phoneNum: [
+      { required: true, message: '联系方式不能为空', trigger: 'blur' },
+      {
+        pattern: /^1\d{10}$/,
+        message: '请输入11位有效的手机号码',
+        trigger: 'blur',
+      },
+    ],
+  });
+
+  const isDrawer = ref(true);
+  const loading = ref(false);
+  const staffNoOptions = ref<TransformedTreeNode[]>([]); // 工号选择列表
+  const OrganizationSourceData = ref<OrganizationUserTree[]>([]); // 组织结构树原始数据
+  const departmentArr = ref<{ value: string | number; label: string }[]>([]);
+
+  // 通过工号查询组织结构树
+  const remoteMethod = (query: string) => {
+    if (query) {
+      loading.value = true;
+      queryOrganizationUserTree(Number(query)).then((res) => {
+        if (res) {
+          loading.value = false;
+          staffNoOptions.value = transformTreeData(res, true);
+          OrganizationSourceData.value = res;
+          departmentArr.value = transformTreeData(OrganizationSourceData.value, false);
+        }
+      });
+    } else {
+      staffNoOptions.value = [];
+    }
+  };
+
+  // 防抖
+  const debouncedRemoteMethod = debounce(remoteMethod, 1000);
+
+  const handleClearStaffNo = () => {
+    formParams.value.staffNo = '';
+    formParams.value.userName = '';
+    formParams.value.deptName = '';
+    formParams.value.phoneNum = '';
+  };
+
+  // 递归查找树结构中的节点
+  const findNodeInTree = (tree: any[], value: string | number): any => {
+    for (const node of tree) {
+      // 检查当前节点
+      if (node.value === value) {
+        return node;
+      }
+      // 检查子节点(如果有)
+      if (node.children && node.children.length > 0) {
+        const foundNode = findNodeInTree(node.children, value);
+        if (foundNode) {
+          return foundNode;
+        }
+      }
+    }
+    return null;
+  };
+
+  const handleChangeStaffNo = (value) => {
+    const findUser = findUserByWorkNo(OrganizationSourceData.value, value);
+    const deptId = Number(findOrgCodeByWorkNo(OrganizationSourceData.value, value));
+    const dept = findNodeInTree(departmentArr.value, deptId);
+    if (findUser) {
+      formParams.value.userId = findUser.id;
+      formParams.value.userName = findUser.appAccountAccountName;
+      formParams.value.staffNo = findUser.idtUserWorkNo;
+      formParams.value.deptId = deptId;
+      formParams.value.deptName = dept.label;
+      formParams.value.phoneNum = findUser.idtUserMobile;
+    }
+  };
+
+  const handleCloseDrawer = () => {
+    emits('close');
+  };
+
+  const handleSubmitForm = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    await formEl.validate((valid) => {
+      if (!valid) return;
+      const addParams = {
+        carNum: formParams.value.carNum,
+        userId: formParams.value.userId,
+        userName: formParams.value.userName,
+        staffNo: formParams.value.staffNo,
+        deptId: formParams.value.deptId,
+        deptName: formParams.value.deptName,
+        phoneNum: formParams.value.phoneNum,
+      };
+      if (props.type === 'add') {
+        addVehicleInfo(addParams).then(() => {
+          ElMessage.success('添加成功');
+          handleCloseDrawer();
+        });
+      } else {
+        updateVehicleInfo(formParams.value).then(() => {
+          ElMessage.success('编辑成功');
+          handleCloseDrawer();
+        });
+      }
+    });
+  };
+
+  watch(
+    () => props.initialData,
+    (newData) => {
+      if (newData) formParams.value = { ...newData };
+    },
+    { immediate: true, deep: true },
+  );
+</script>
+
+<style lang="scss" scoped>
+  .protocal-select:deep(.el-select-dropdown__wrap) {
+    max-height: 600px;
+  }
+</style>

+ 13 - 0
src/views/traffic/vehicle/config/index.ts

@@ -0,0 +1,13 @@
+import {
+  VEHICLE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  VEHICLE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  VEHICLE_LIST_TABLE_OPTIONS,
+  VEHICLE_LIST_TABLE_COLUMNS,
+} from './table';
+
+export {
+  VEHICLE_LIST_TABLE_MAX_HEIGHT_DEFAULT,
+  VEHICLE_LIST_TABLE_MAX_HEIGHT_PERMISSION,
+  VEHICLE_LIST_TABLE_OPTIONS,
+  VEHICLE_LIST_TABLE_COLUMNS,
+};

+ 72 - 0
src/views/traffic/vehicle/config/table.ts

@@ -0,0 +1,72 @@
+/**
+ * 车辆信息表格配置
+ */
+import type { TableColumnProps } from '@/types/basic-table';
+
+export const VEHICLE_LIST_TABLE_MAX_HEIGHT_DEFAULT = 'calc(70vh - 80px)';
+export const VEHICLE_LIST_TABLE_MAX_HEIGHT_PERMISSION = 'calc(70vh - 130px)';
+
+// 基础表格样式配置
+const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+};
+
+// 车辆信息表格样式配置
+export const VEHICLE_LIST_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+};
+
+// 应急处置表格列配置
+export const VEHICLE_LIST_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '',
+    width: '55px',
+    type: 'selection',
+  },
+  {
+    label: '序号',
+    prop: 'index',
+    width: '80px',
+    type: 'index',
+    align: 'center',
+  },
+  {
+    label: '车牌号',
+    prop: 'carNum',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '姓名',
+    prop: 'userName',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '工号',
+    prop: 'staffNo',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '所属部门',
+    prop: 'deptName',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    label: '联系方式',
+    prop: 'phoneNum',
+    align: 'center',
+    minWidth: '180px',
+  },
+  {
+    prop: 'action',
+    label: '操作',
+    align: 'center',
+    slot: 'action',
+    fixed: 'right',
+    width: '180px',
+  },
+];

+ 33 - 0
src/views/traffic/vehicle/constant/index.ts

@@ -0,0 +1,33 @@
+// 查询字段对应:1-车牌号,2-姓名,3-工号,4-部门
+export enum FIELDTYPE {
+  VEHICLE_NUMBER = '1',
+  NAME = '2',
+  JOB_NUMBER = '3',
+  DEPARTMENT_NAME = '4',
+}
+
+export const FIELD_CONTENT = {
+  [FIELDTYPE.VEHICLE_NUMBER]: '车牌号',
+  [FIELDTYPE.NAME]: '姓名',
+  [FIELDTYPE.JOB_NUMBER]: '工号',
+  [FIELDTYPE.DEPARTMENT_NAME]: '部门',
+};
+
+export const vehicleQueryOptions = [
+  {
+    label: FIELD_CONTENT[FIELDTYPE.VEHICLE_NUMBER],
+    value: FIELDTYPE.VEHICLE_NUMBER,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.NAME],
+    value: FIELDTYPE.NAME,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.JOB_NUMBER],
+    value: FIELDTYPE.JOB_NUMBER,
+  },
+  {
+    label: FIELD_CONTENT[FIELDTYPE.DEPARTMENT_NAME],
+    value: FIELDTYPE.DEPARTMENT_NAME,
+  },
+];