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

Merge branch 'dev-wyf' into 'dev'

交通违规行为

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

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

@@ -0,0 +1,68 @@
+import { http } from '@/utils/http/axios';
+import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
+
+import type {
+  ActTableQuery,
+  ActTableData,
+  CreateActQuery,
+  UpdateActQuery,
+} from '@/views/traffic/violation/act/types.ts';
+
+export function getActTableList(data: QueryPageRequest<ActTableQuery>) {
+  return http.request<QueryPageResponse<ActTableData>>({
+    url: '/trafficViolation/queryTrafficViolationPage',
+    method: 'post',
+    data,
+  });
+}
+
+export function getActDataDetail(id: number) {
+  return http.request<ActTableData>({
+    url: `/trafficViolation/queryTrafficViolation?id=${id}`,
+    method: 'post',
+  });
+}
+
+export function noticeActData(violationIds: number[] | number) {
+  return http.request({
+    url: `/trafficViolation/updateTrafficViolationNotice?trafficViolationIds=${violationIds}`,
+    method: 'post',
+  });
+}
+
+export function deleteActData(violationIds: number[] | number) {
+  return http.request({
+    url: `/trafficViolation/deleteTrafficViolation?trafficViolationIds=${violationIds}`,
+    method: 'post',
+  });
+}
+
+export function updateActData(data: UpdateActQuery) {
+  return http.request({
+    url: '/trafficViolation/updateTrafficViolation',
+    method: 'post',
+    data,
+  });
+}
+
+export function createActData(data: CreateActQuery) {
+  return http.request({
+    url: '/trafficViolation/saveTrafficViolation',
+    method: 'post',
+    data,
+  });
+}
+
+export function exportActViolation(data: ActTableQuery) {
+  return http.request(
+    {
+      url: '/trafficViolation/exportTrafficViolationList',
+      method: 'post',
+      data,
+      responseType: 'blob',
+    },
+    {
+      isTransformResponse: false,
+    },
+  );
+}

+ 14 - 2
src/components/formItems/selectableInput/SelectableInput.vue

@@ -7,7 +7,13 @@
       clearable
     >
       <template #prepend>
-        <el-select v-model="selectValue" style="width: 100px" value-key="value" :validate-event="false">
+        <el-select
+          v-model="selectValue"
+          style="width: 100px"
+          value-key="value"
+          :validate-event="false"
+          @change="modelValue = ''"
+        >
           <el-option
             v-for="item in options"
             :key="item.value"
@@ -29,7 +35,7 @@
     options: SelectOption[]; // options.value为选择的键
   }>();
 
-  const modelValue = ref(''); // modelValue 为选择的值
+  const modelValue = ref(); // modelValue 为输入的值
   const selectValue = ref<SelectOption>(props.options[0]);
 
   function setValue(key: string, val: string) {
@@ -39,15 +45,21 @@
   }
 
   function getValue() {
+    if (!modelValue.value) return null;
     return {
       key: selectValue.value.value,
       value: modelValue.value,
     };
   }
+  function clearValue() {
+    selectValue.value = props.options[0];
+    modelValue.value = undefined;
+  }
 
   defineExpose({
     setValue,
     getValue,
+    clearValue,
   });
 </script>
 

+ 4 - 3
src/views/traffic/regulation/components/RegulationTable.vue

@@ -46,11 +46,11 @@
       </template>
       <template #effectState="scope">
         <div class="effect-state" v-if="scope.row.effectState === 1">
-          <div style="background-color: #52c41a; width: 5px; height: 5px; border-radius: 50%; margin-right: 5px"></div>
+          <div style="background-color: #52c41a; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px"></div>
           <span>已生效</span>
         </div>
         <div class="effect-state" v-else-if="scope.row.effectState === 0">
-          <div style="background-color: #ff4d4f; width: 5px; height: 5px; border-radius: 50%; margin-right: 5px"></div>
+          <div style="background-color: #ff4d4f; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px"></div>
           <span>未生效</span>
         </div>
       </template>
@@ -162,11 +162,12 @@
   };
   const handleCurrentChange = (value: number) => {
     pagination.pageNumber = value;
-    tabelQuery.value.pageSize = value;
+    tabelQuery.value.pageNumber = value;
     getTabelData();
   };
 
   async function getTabelData() {
+    tableConfig.loading = true;
     const res = await getRegulationList(tabelQuery.value);
     tableData.value = res.records;
     pagination.total = res.totalRow;

+ 1 - 1
src/views/traffic/regulation/utils.ts

@@ -1,5 +1,5 @@
 import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
-import type { FileItem } from '@/views/disaster/types';
+import type { FileItem } from './types';
 
 export function stringToArray(str?: string): number[] | undefined {
   if (!str) return undefined;

+ 270 - 41
src/views/traffic/violation/act/Act.vue

@@ -6,41 +6,119 @@
     <main class="safety-platform-container__main">
       <div class="search-table-container">
         <header>
-          <!-- 按钮 -->
-          <el-button type="primary" class="search-table-container--button" :icon="Plus" @click=""> 新建记录 </el-button>
-          <el-button plain class="search-table-container--button" @click=""> 批量导入 </el-button>
+          <div>
+            <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>
+            <RealtimeNotice />
+          </div>
 
           <div class="act-search">
             <section class="select-box">
-              <SelectableInput :options="ACT_TABLE_SEARCH_OPTIONS" />
-              <span>违规类型:</span>
-              <el-select v-model="searchData.actName" placeholder="请选择违规类型" class="select-box--select">
-              </el-select>
-              <span>通知状态:</span>
-              <el-select v-model="searchData.status" placeholder="请选择通知状态" class="select-box--select">
-              </el-select>
-              <el-date-picker
-                v-model="searchData.actTime"
-                type="datetimerange"
-                range-separator="至"
-                start-placeholder="开始时间"
-                end-placeholder="结束时间"
-              />
+              <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="">重置</el-button>
-              <el-button @click="">导出</el-button>
+              <el-button @click="handleReset">重置</el-button>
+              <el-button @click="handleDownload">导出</el-button>
             </section>
           </div>
         </header>
         <!-- 表格 -->
         <BasicTable
-          :tableConfig="tableConfig"
           :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>
     </main>
@@ -51,63 +129,197 @@
   import BasicTable from '@/components/BasicTable.vue';
   import useTableConfig from '@/hooks/useTableConfigHook';
   import SelectableInput from '@/components/formItems/selectableInput/SelectableInput.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import RealtimeNotice from './components/RealtimeNotice.vue';
+  import { ElMessage } from 'element-plus';
   import { TABLE_OPTIONS, VIOLATION_ACT_TABLE_COLUMNS } from './configs/tables';
-  import { ACT_TABLE_SEARCH_OPTIONS } from './constants';
+  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,
+  } from './constants';
   import { ref, reactive, onMounted } from 'vue';
   import { Search, Plus } from '@element-plus/icons-vue';
   import { useRouter } from 'vue-router';
+  import { openMessageBox } from '@/utils/element-plus/messageBox';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import type { ActTableSearch, ActTableQuery, ActTableData, UpdateActQuery } from './types';
+  import {
+    getActTableList,
+    noticeActData,
+    deleteActData,
+    exportActViolation,
+  } from '@/api/traffic-violation/traffic-act';
+  import { downloadFile } from '@/views/disaster/utils/download';
+  import ImageViewer from './components/ImageViewer.vue';
+  import dayjs from 'dayjs';
 
   const router = useRouter();
 
   // 搜索栏
-  const searchData = reactive<any>({
-    actName: null,
-    status: null,
-    actTime: null,
-  });
+  const selectableInputRef = ref<InstanceType<typeof SelectableInput>>();
+  const searchData = reactive<ActTableSearch>({});
+
+  function getQuery() {
+    if (!selectableInputRef.value) return;
+    tabelQuery.queryParam = {
+      pageType: 1,
+    };
+    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() {
-    // tabelQuery.value.queryParam = {};
-    // if (searchData.plateNo) {
-    //   tabelQuery.value.queryParam.plateNo = searchData.plateNo;
-    // }
+    getQuery();
     getTabelData();
   }
+
+  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_ACT_TABLE_COLUMNS, TABLE_OPTIONS);
 
-  const tableData = ref<any[]>([]);
+  const tableData = ref<ActTableData[]>([]);
 
-  const tabelQuery = ref({
+  const tabelQuery = reactive<QueryPageRequest<ActTableQuery>>({
     pageNumber: pagination.pageNumber,
     pageSize: pagination.pageSize,
-    queryParam: {},
+    queryParam: {
+      pageType: 1,
+    },
   });
 
   const handleSizeChange = (value: number) => {
-    pagination.pageSize = value;
-    tabelQuery.value.pageSize = value;
+    pagination.pageNumber = value;
+    tabelQuery.pageSize = value;
     getTabelData();
   };
   const handleCurrentChange = (value: number) => {
     pagination.pageNumber = value;
-    tabelQuery.value.pageSize = value;
+    tabelQuery.pageSize = value;
     getTabelData();
   };
 
-  async function getTabelData() {}
+  const handleSelectionChange = (value: any[]) => {};
+
+  // const handleCloseBatchOperation = () => {
+  //   if (!basicTableRef.value) return;
+  //   basicTableRef.value.clearSelection();
+  // };
+
+  // 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();
+  // };
+
+  async function getTabelData() {
+    tableConfig.loading = true;
+    const res = await getActTableList(tabelQuery);
+    tableData.value = res.records;
+    pagination.total = res.totalRow;
+    tableConfig.loading = false;
+  }
 
   onMounted(async () => {
-    getTabelData();
+    await getTabelData();
   });
 
-  function handleCreateRegulation() {
+  function handleCreateAct() {
     router.push({
-      name: 'traffic-regulation-item',
+      name: 'traffic-violation-act-item',
       query: {
-        operate: 'regulation-create',
+        operate: 'act-create',
       },
     });
   }
+
+  function handleEditAct(id: number) {
+    router.push({
+      name: 'traffic-violation-act-item',
+      query: {
+        id,
+        operate: 'act-edit',
+      },
+    });
+  }
+  async function handleNoticeAct(id: number) {
+    tableConfig.loading = true;
+    try {
+      await noticeActData(id);
+    } catch (e) {
+      ElMessage.error('通知失败');
+      return;
+    } finally {
+      tableConfig.loading = false;
+    }
+    getTabelData();
+  }
+
+  async function handleDeleteAct(id: number) {
+    tableConfig.loading = true;
+    try {
+      await deleteActData(id);
+    } catch (e) {
+      ElMessage.error('删除失败');
+      return;
+    } finally {
+      tableConfig.loading = false;
+    }
+    getTabelData();
+  }
 </script>
 
 <style scoped lang="scss">
@@ -128,8 +340,25 @@
     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>

+ 50 - 3
src/views/traffic/violation/act/ActItem.vue

@@ -1,7 +1,54 @@
 <template>
-  <div> </div>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <BreadcrumbBack />
+      <span class="breadcrumb-title">{{ headerTitle }}</span>
+    </header>
+    <component :is="dynamicComponent" :id="id" ref="dynamicComponentRef" @record-submitted="handleRecordSubmitted" />
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { ref, computed, defineAsyncComponent } from 'vue';
+  import { useRoute } from 'vue-router';
 
-<style scoped></style>
+  const route = useRoute();
+  const operate = route.query.operate;
+  const id = Number(route.query.id);
+  const headerTitle = computed(() => {
+    switch (operate) {
+      case 'act-create':
+        return `创建违规行为记录`;
+      case 'act-edit':
+        return `编辑违规行为记录`;
+      default:
+        return '未知操作';
+    }
+  });
+
+  const dynamicComponent = computed(() => {
+    switch (operate) {
+      case 'act-create':
+        return defineAsyncComponent(() => import('./components/ActCreate.vue'));
+      case 'act-edit':
+        return defineAsyncComponent(() => import('./components/ActEdit.vue'));
+      default:
+        return '';
+    }
+  });
+
+  const dynamicComponentRef = ref();
+
+  function handleRecordSubmitted() {}
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+
+  .safety-platform-container__header {
+    flex-direction: row !important;
+    justify-content: flex-start !important;
+    gap: 8px !important;
+  }
+</style>

+ 124 - 0
src/views/traffic/violation/act/components/ActCreate.vue

@@ -0,0 +1,124 @@
+<template>
+  <main class="safety-platform-container__main">
+    <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig">
+      <template #violateType>
+        <el-select v-model="ruleFormData.violateType" placeholder="请选择违规类型">
+          <el-option
+            v-for="item in ACT_VIOLATION_TYPE_OPTIONS"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          ></el-option>
+        </el-select>
+        <div v-if="ruleFormData.violateType === ACT_VIOLATION_TYPE.SPEEDING" style="margin-top: 10px; width: 100%">
+          <el-form-item
+            label-width="auto"
+            label="车速:"
+            prop="speed"
+            :rules="[
+              {
+                required: true,
+                validator: (_rule, value, callback) => {
+                  if (!value) return callback(new Error('请输入车速'));
+                  if (value > 999) {
+                    callback(new Error('超过车速时速上限'));
+                  } else {
+                    callback();
+                  }
+                },
+                trigger: 'blur',
+              },
+            ]"
+          >
+            <el-input v-model.number="ruleFormData.speed" placeholder="请输入车速" type="number" min="0">
+              <template #suffix>km/h</template>
+            </el-input>
+          </el-form-item>
+        </div>
+      </template>
+      <template #capturePhotos>
+        <UploadImages :maxCount="9" ref="uploadImagesRef" @upload-success="handleUploadChange" />
+      </template>
+    </BasicForm>
+  </main>
+  <footer class="safety-platform-container__footer">
+    <el-button @click="router.back()">取消</el-button>
+    <el-button type="primary" @click="handleSubmit">提交</el-button>
+  </footer>
+  <UploadLoading :form-loading="formLoading" v-if="formLoading" />
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import BasicForm from '@/components/BasicForm.vue';
+  import UploadLoading from '@/components/UploadLoading.vue';
+  import UploadImages from '@/views/disaster/disaster-control/src/components/UploadImages.vue';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { useUserInfoHook } from '@/views/disaster/hooks';
+  import { createActData } from '@/api/traffic-violation/traffic-act';
+  import { CreateActRuleForm, CreateActQuery } 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 { formatImageList } from '../utils';
+
+  const { realname } = useUserInfoHook();
+
+  const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData, beforeRouteLeave } =
+    useFormConfigHook<CreateActRuleForm>(ACT_FORM_CONFIG, ACT_FORM_DATA, ACT_FORM_RULES);
+
+  const basicFormRef = ref<InstanceType<typeof BasicForm>>();
+  const uploadImagesRef = ref<InstanceType<typeof UploadImages>>();
+  const handleValidate = async () => {
+    if (!basicFormRef.value) return;
+    const res = await basicFormRef.value.validateForm();
+    return res;
+  };
+
+  const getFormData = async () => {
+    if (ruleFormData.violateType !== ACT_VIOLATION_TYPE.SPEEDING) {
+      ruleFormData.speed = null;
+    }
+    cloneRuleFormData();
+    const res: CreateActQuery = {
+      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 createActData(params);
+      ElMessage.success('创建成功');
+      router.back();
+    } catch (e) {
+      console.log(e);
+    } finally {
+      formLoading.value = false;
+    }
+  };
+  const handleUploadChange = () => {
+    ruleFormData.capturePhotos = uploadImagesRef.value!.getUploadedImages();
+  };
+
+  onMounted(() => {
+    ruleFormData.creatName = realname;
+    cloneRuleFormData();
+    beforeRouteLeave();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+</style>

+ 114 - 0
src/views/traffic/violation/act/components/ActEdit.vue

@@ -0,0 +1,114 @@
+<template>
+  <main class="safety-platform-container__main">
+    <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig">
+      <template #violateType>
+        <el-select v-model="ruleFormData.violateType" placeholder="请选择违规类型">
+          <el-option
+            v-for="item in ACT_VIOLATION_TYPE_OPTIONS"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          ></el-option>
+        </el-select>
+        <div v-if="ruleFormData.violateType === ACT_VIOLATION_TYPE.SPEEDING" style="margin-top: 10px; width: 100%">
+          <el-form-item
+            label-width="auto"
+            label="车速:"
+            prop="speed"
+            :rules="[
+              {
+                required: true,
+                validator: (_rule, value, callback) => {
+                  console.log(value);
+                  if (!value) return callback(new Error('请输入车速'));
+                  if (value > 999) {
+                    callback(new Error('超过车速时速上限'));
+                  } else {
+                    callback();
+                  }
+                },
+                trigger: 'blur',
+              },
+            ]"
+          >
+            <el-input v-model.number="ruleFormData.speed" placeholder="请输入车速" type="number" min="0">
+              <template #suffix>km/h</template>
+            </el-input>
+          </el-form-item>
+        </div>
+      </template>
+      <template #capturePhotos>
+        <UploadImages
+          :maxCount="9"
+          ref="uploadImagesRef"
+          :image-list="recordImageList"
+          @upload-success="handleUploadChange"
+        />
+      </template>
+    </BasicForm>
+  </main>
+  <footer class="safety-platform-container__footer">
+    <el-button @click="router.back()">取消</el-button>
+    <el-button type="primary" @click="handleSubmit">提交</el-button>
+  </footer>
+  <UploadLoading :form-loading="formLoading" v-if="formLoading" />
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import BasicForm from '@/components/BasicForm.vue';
+  import UploadLoading from '@/components/UploadLoading.vue';
+  import UploadImages from '@/views/disaster/disaster-control/src/components/UploadImages.vue';
+  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 { ACT_FORM_CONFIG, ACT_FORM_DATA, ACT_FORM_RULES } from '../configs/form';
+  import { ACT_VIOLATION_TYPE, ACT_VIOLATION_TYPE_OPTIONS } from '../constants';
+
+  const props = defineProps<{
+    id: number;
+  }>();
+
+  const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData, beforeRouteLeave } =
+    useFormConfigHook<CreateActRuleForm>(ACT_FORM_CONFIG, ACT_FORM_DATA, ACT_FORM_RULES);
+
+  const basicFormRef = ref<InstanceType<typeof BasicForm>>();
+  const uploadImagesRef = ref<InstanceType<typeof UploadImages>>();
+
+  const handleValidate = async () => {
+    if (!basicFormRef.value) return;
+    const res = await basicFormRef.value.validateForm();
+    return res;
+  };
+
+  const actDetail = ref<ActTableData>();
+  const getDetail = async () => {
+    actDetail.value = await getActDataDetail(props.id);
+  };
+
+  const getFormData = async () => {};
+
+  const formLoading = ref(false);
+  const router = useRouter();
+  const handleSubmit = async () => {
+    const res = await handleValidate();
+    if (!res) return;
+  };
+
+  const handleUploadChange = () => {
+    ruleFormData.capturePhotos = uploadImagesRef.value!.getUploadedImages();
+  };
+
+  onMounted(async () => {
+    await getDetail();
+    cloneRuleFormData();
+    beforeRouteLeave();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+</style>

+ 53 - 0
src/views/traffic/violation/act/components/ImageViewer.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="image-viewer">
+    <div v-if="images && images.length">
+      <el-image
+        fit="cover"
+        :src="images[0]"
+        :preview-src-list="images"
+        class="image-viewer__image"
+        :preview-teleported="true"
+      ></el-image>
+      <div class="image-viewer__text">共{{ images.length }}张</div>
+    </div>
+    <div v-else> - </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+  import { unformatImage } from '../utils';
+
+  const props = defineProps<{
+    fileList: string;
+  }>();
+
+  const images = computed(() => {
+    return unformatImage(props.fileList)?.map((x) => {
+      return x.url;
+    });
+  });
+</script>
+
+<style scoped>
+  .image-viewer {
+    width: 136px;
+  }
+  .image-viewer__image {
+    width: 120px;
+    height: 90px;
+    border-radius: 5px;
+  }
+  .image-viewer__text {
+    position: absolute;
+    bottom: 17px;
+    right: 27px;
+    background-color: rgba(0, 0, 0, 0.6);
+    padding: 3px;
+    color: rgba(255, 255, 255, 0.7);
+    font-size: 14px;
+    border-top-left-radius: 5px;
+    border-bottom-right-radius: 5px;
+    pointer-events: none;
+  }
+</style>

+ 10 - 0
src/views/traffic/violation/act/components/RealtimeNotice.vue

@@ -0,0 +1,10 @@
+<template>
+  <div class="realtime-notice"> </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style scoped>
+  .realtime-notice {
+  }
+</style>

+ 79 - 0
src/views/traffic/violation/act/configs/form.ts

@@ -0,0 +1,79 @@
+import { FormConfig } from '@/types/basic-form';
+
+export const ACT_FORM_CONFIG: FormConfig[] = [
+  {
+    prop: 'carNumber',
+    label: '车牌号:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入违规车辆车牌号码',
+      maxlength: 8,
+      showWordLimit: false,
+    },
+  },
+  {
+    prop: 'violateType',
+    slot: 'violateType',
+    label: '违规类型:',
+  },
+  {
+    prop: 'violateLocation',
+    label: '违规地点:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入违规地点',
+    },
+  },
+  {
+    prop: 'captureTime',
+    label: '抓拍时间:',
+    component: 'ElDatePicker',
+    componentProps: {
+      placeholder: '请选择抓拍时间',
+      type: 'datetime',
+      format: 'YYYY-MM-DD HH:mm',
+      dateFormat: 'MMM DD, YYYY',
+      timeFormat: 'HH:mm',
+      valueFormat: 'YYYY-MM-DD HH:mm',
+      disabledDate: (time) => time.getTime() > Date.now(),
+    },
+  },
+  {
+    prop: 'capturePhotos',
+    label: '抓拍图片:',
+    slot: 'capturePhotos',
+  },
+  {
+    label: '备注:',
+    prop: 'remark',
+    component: 'ElInput',
+    componentProps: {
+      type: 'textarea',
+      rows: 5,
+    },
+  },
+  {
+    label: '创建人:',
+    prop: 'creatName',
+    component: 'ElInput',
+    componentProps: {
+      disabled: true,
+    },
+  },
+];
+
+export const ACT_FORM_DATA = {
+  carNumber: '',
+  violateType: null,
+  captureTime: '',
+  capturePhotos: [],
+  violateLocation: '',
+  remark: '',
+  creatName: '',
+};
+
+export const ACT_FORM_RULES = {
+  carNumber: [{ required: true, message: '车牌号格式错误', trigger: 'blur' }],
+  violateType: [{ required: true, message: '请选择违规类型', trigger: 'change' }],
+  captureTime: [{ required: true, message: '请选择抓拍时间', trigger: 'change' }],
+};

+ 24 - 14
src/views/traffic/violation/act/configs/tables.ts

@@ -8,6 +8,13 @@ export const TABLE_OPTIONS = {
 };
 
 export const VIOLATION_ACT_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    prop: 'select',
+    label: '',
+    type: 'selection',
+    align: 'center',
+    width: '80px',
+  },
   {
     label: '序号',
     align: 'center',
@@ -16,27 +23,28 @@ export const VIOLATION_ACT_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '车主',
-    prop: 'driverName',
+    prop: 'violateName',
     align: 'center',
-    width: '80px',
+    minWidth: '120px',
   },
   {
     label: '车牌号',
-    prop: 'vehicleNo',
+    prop: 'carNumber',
     align: 'center',
-    width: '80px',
+    minWidth: '120px',
   },
   {
     label: '所属部门',
     prop: 'deptName',
     align: 'center',
-    width: '80px',
+    minWidth: '120px',
   },
   {
     label: '违规类型',
-    prop: 'actName',
+    prop: 'violateType',
+    slot: 'violateType',
     align: 'center',
-    width: '80px',
+    minWidth: '120px',
   },
   {
     label: '车速',
@@ -47,32 +55,34 @@ export const VIOLATION_ACT_TABLE_COLUMNS: TableColumnProps[] = [
 
   {
     label: '抓拍图片',
-    prop: 'actPic',
-    slot: 'actPic',
+    prop: 'capturePhotos',
+    slot: 'capturePhotos',
     align: 'center',
-    minWidth: '120px',
+    minWidth: '180px',
   },
   {
     label: '违规地点',
-    prop: 'actPosition',
+    prop: 'violateLocation',
     align: 'center',
     minWidth: '120px',
   },
   {
     label: '时间',
-    prop: 'actTime',
+    prop: 'captureTime',
     align: 'center',
     width: '200px',
   },
   {
     label: '数据来源',
-    prop: 'dataSource',
+    prop: 'createSource',
+    slot: 'createSource',
     align: 'center',
     width: '200px',
   },
   {
     label: '通知状态',
-    prop: 'status',
+    prop: 'isNotice',
+    slot: 'isNotice',
     align: 'center',
     width: '200px',
   },

+ 69 - 1
src/views/traffic/violation/act/constants.ts

@@ -1,7 +1,7 @@
 export const ACT_TABLE_SEARCH_OPTIONS: SelectOption[] = [
   {
     label: '车牌号',
-    value: 'vehicleNo',
+    value: 'carNumber',
     disabled: false,
   },
   {
@@ -15,3 +15,71 @@ export const ACT_TABLE_SEARCH_OPTIONS: SelectOption[] = [
     disabled: false,
   },
 ];
+
+export enum ACT_VIOLATION_TYPE {
+  SPEEDING = 1,
+  WRONG_WAY_DRIVING = 2,
+  ILLEGAL_PARKING = 3,
+}
+
+export const ACT_VIOLATION_TYPE_LABEL = {
+  [ACT_VIOLATION_TYPE.SPEEDING]: '超速',
+  [ACT_VIOLATION_TYPE.WRONG_WAY_DRIVING]: '逆行',
+  [ACT_VIOLATION_TYPE.ILLEGAL_PARKING]: '违规停车',
+};
+
+export const ACT_VIOLATION_TYPE_OPTIONS: SelectOption[] = [
+  {
+    label: ACT_VIOLATION_TYPE_LABEL[ACT_VIOLATION_TYPE.SPEEDING],
+    value: ACT_VIOLATION_TYPE.SPEEDING,
+    disabled: false,
+  },
+  {
+    label: ACT_VIOLATION_TYPE_LABEL[ACT_VIOLATION_TYPE.WRONG_WAY_DRIVING],
+    value: ACT_VIOLATION_TYPE.WRONG_WAY_DRIVING,
+    disabled: false,
+  },
+  {
+    label: ACT_VIOLATION_TYPE_LABEL[ACT_VIOLATION_TYPE.ILLEGAL_PARKING],
+    value: ACT_VIOLATION_TYPE.ILLEGAL_PARKING,
+    disabled: false,
+  },
+];
+
+export enum ACT_NOTICE_STATE {
+  INACTIVE = 0,
+  ACTIVE = 1,
+}
+
+export const ACT_NOTICE_STATE_LABEL = {
+  [ACT_NOTICE_STATE.INACTIVE]: '未通知',
+  [ACT_NOTICE_STATE.ACTIVE]: '已通知',
+};
+
+export const ACT_NOTICE_STATE_COLOR = {
+  [ACT_NOTICE_STATE.INACTIVE]: '#ff4d4f',
+  [ACT_NOTICE_STATE.ACTIVE]: '#52c41a',
+};
+
+export const ACT_NOTICE_STATE_OPTIONS: SelectOption[] = [
+  {
+    label: ACT_NOTICE_STATE_LABEL[ACT_NOTICE_STATE.ACTIVE],
+    value: ACT_NOTICE_STATE.ACTIVE,
+    disabled: false,
+  },
+  {
+    label: ACT_NOTICE_STATE_LABEL[ACT_NOTICE_STATE.INACTIVE],
+    value: ACT_NOTICE_STATE.INACTIVE,
+    disabled: false,
+  },
+];
+
+export enum ACT_NOTICE_DATA_SOURCE {
+  ARTIFICAL = 1,
+  OUTSIDE = 2,
+}
+
+export const ACT_NOTICE_DATA_SOURCE_LABEL = {
+  [ACT_NOTICE_DATA_SOURCE.ARTIFICAL]: '人工上报',
+  [ACT_NOTICE_DATA_SOURCE.OUTSIDE]: '外部创建',
+};

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

@@ -0,0 +1,69 @@
+export interface ImageItem {
+  url: string;
+  name?: string;
+  size?: number;
+  file?: File;
+}
+
+export interface ActTableSearch {
+  carNumber?: string;
+  violateName?: string;
+  deptName?: string;
+  violationType?: number;
+  isNotice?: number;
+  searchTime?: string[];
+}
+
+export interface ActTableQuery extends Omit<ActTableSearch, 'searchTime'> {
+  pageType: number; //1-违规行为 2-违规通知
+  startTime?: string | null;
+  endTime?: string | null;
+}
+
+export interface ActTableData {
+  id: number;
+  createSource: number; //1-人工创建 2-外部获取
+  carNumber: string;
+  violateBy?: number;
+  violateName?: string;
+  deptName?: string;
+  staffNo?: string;
+  violateType: number; //1-超速 2-逆行 3-违规停车
+  speed?: number;
+  violateLocation?: string;
+  captureTime: string;
+  capturePhotos?: string;
+  createdBy?: number;
+  remark?: string;
+  isNotice?: number; //0-未通知 1-已通知
+}
+
+export interface CreateActRuleForm {
+  carNumber: string;
+  violateType: number | null; //1-超速 2-逆行 3-违规停车
+  speed?: number | null;
+  violateLocation?: string;
+  captureTime: string;
+  capturePhotos?: ImageItem[];
+  remark?: string;
+  creatName?: string;
+}
+
+export interface CreateActQuery {
+  createSource: 1;
+  carNumber: string;
+  violateType: number; //1-超速 2-逆行 3-违规停车
+  speed?: number | null;
+  violateLocation?: string;
+  captureTime: string;
+  capturePhotos?: string;
+  remark?: string;
+  creatName?: string;
+}
+
+// export interface UpdateActRuleForm extends CreateActRuleForm {
+//   id: number;
+// }
+export interface UpdateActQuery extends CreateActQuery {
+  id: number;
+}

+ 37 - 0
src/views/traffic/violation/act/utils.ts

@@ -0,0 +1,37 @@
+import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
+import type { ImageItem } from './types';
+
+export function stringToArray(str?: string): number[] | undefined {
+  if (!str) return undefined;
+  return JSON.parse('[' + str + ']');
+}
+
+export function unformatImage(file?: string) {
+  if (!file) return undefined;
+  const fileData: ImageItem[] = 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,
+  };
+};
+
+export const formatImageList = async (data: ImageItem[] | undefined) => {
+  if (!data || data.length === 0) return null;
+  const res = await Promise.all(
+    data.map(async (item) => {
+      const res = await formatImage(item);
+      return res;
+    }),
+  );
+  return res;
+};

+ 2 - 2
utils/devProxy/staff/proxy.ts

@@ -3,8 +3,8 @@ import path from 'path';
 
 // staff环境
 const proxyStaff: PROXY_TYPE = {
-  // serverHost: 'http://192.168.13.68:8802/',
-  serverHost: 'http://192.168.22.121:8802/',
+  serverHost: 'http://192.168.13.68:8802/',
+  // serverHost: 'http://192.168.22.121:8802/',
   // serverHost: 'http://192.168.22.146:8802/',
   loginHost: 'http://192.168.13.68:7200/login/#/',
   fileUploadHost: 'http://192.168.13.102:9000/',