ManageAccidentItem.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <template>
  2. <div class="accident-create-container">
  3. <el-form ref="formRef" :model="formData" :rules="rules" class="form-wrap">
  4. <div class="personnel-section">
  5. <div v-for="(person, index) in personnelList" :key="index" class="personnel-item">
  6. <el-row :gutter="40">
  7. <el-col :span="6">
  8. <el-form-item :prop="`accidentPersonnelInfoList.${index}.carNum`" :rules="carNumRules" label="车牌号:">
  9. <el-input
  10. v-model="person.carNum"
  11. placeholder="请输入违规车辆车牌号码"
  12. maxlength="8"
  13. show-word-limit
  14. clearable
  15. />
  16. </el-form-item>
  17. </el-col>
  18. <el-col :span="10">
  19. <el-form-item
  20. :prop="`accidentPersonnelInfoList.${index}.accidentPersonnel`"
  21. :rules="personRules"
  22. label="事故人员:"
  23. >
  24. <el-input
  25. v-model="person.accidentPersonnel"
  26. placeholder="请输入事故人员姓名"
  27. maxlength="20"
  28. show-word-limit
  29. clearable
  30. />
  31. </el-form-item>
  32. </el-col>
  33. <el-col :span="6">
  34. <el-form-item
  35. :prop="`accidentPersonnelInfoList.${index}.phoneNum`"
  36. :rules="phoneRules"
  37. label="联系方式:"
  38. >
  39. <el-input
  40. v-model="person.phoneNum"
  41. placeholder="请输入联系方式"
  42. maxlength="11"
  43. show-word-limit
  44. clearable
  45. />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="2" class="delete-col">
  49. <el-button
  50. type="danger"
  51. link
  52. :icon="Delete"
  53. :disabled="personnelList.length === 1"
  54. @click="removePersonnel(index)"
  55. >删除</el-button
  56. >
  57. </el-col>
  58. </el-row>
  59. </div>
  60. <el-button type="primary" :icon="Plus" :disabled="personnelList.length >= 10" @click="addPersonnel">
  61. 新增
  62. </el-button>
  63. </div>
  64. <el-row :gutter="40" class="basic-info">
  65. <el-col :span="12">
  66. <el-form-item label="事故地点:" prop="trafficAccidentRecord.accidentLocation">
  67. <el-input
  68. v-model="formData.trafficAccidentRecord.accidentLocation"
  69. placeholder="请输入事故发生地点"
  70. clearable
  71. />
  72. </el-form-item>
  73. </el-col>
  74. <el-col :span="12">
  75. <el-form-item label="事故时间:" prop="trafficAccidentRecord.accidentTime">
  76. <el-date-picker
  77. v-model="formData.trafficAccidentRecord.accidentTime"
  78. type="datetime"
  79. value-format="YYYY-MM-DD HH:mm:ss"
  80. placeholder="请选择事故发生时间"
  81. style="width: 100%"
  82. :disabled-date="disabledDate"
  83. :disabled-time="disabledTime"
  84. />
  85. </el-form-item>
  86. </el-col>
  87. </el-row>
  88. <el-form-item label="事故描述:" prop="trafficAccidentRecord.accidentDescription">
  89. <el-input
  90. v-model="formData.trafficAccidentRecord.accidentDescription"
  91. type="textarea"
  92. :rows="4"
  93. placeholder="请输入事故描述"
  94. clearable
  95. />
  96. </el-form-item>
  97. <el-form-item label="事故图片:" label-width="92.54px">
  98. <UploadImages
  99. ref="uploadImagesRef"
  100. :maxCount="5"
  101. :image-list="recordImageList"
  102. @upload-success="handleUploadChange"
  103. />
  104. </el-form-item>
  105. <el-form-item label="备注:" label-width="92.54px">
  106. <el-input v-model="formData.trafficAccidentRecord.remark" placeholder="请输入备注" clearable />
  107. </el-form-item>
  108. <el-form-item label="创建人:" label-width="92.54px">
  109. <el-input v-model="formData.trafficAccidentRecord.createdByName" placeholder="创建人" disabled />
  110. </el-form-item>
  111. </el-form>
  112. </div>
  113. </template>
  114. <script setup lang="ts">
  115. import { ref, reactive, onMounted, computed } from 'vue';
  116. import { useRoute, onBeforeRouteLeave } from 'vue-router';
  117. import { ElForm } from 'element-plus';
  118. import { Delete, Plus } from '@element-plus/icons-vue';
  119. import UploadImages from '@/views/disaster/disaster-control/src/components/UploadImages.vue';
  120. import { useUserInfoHook } from '@/views/disaster/hooks';
  121. import { ImageItem } from '@/types/disaster-control';
  122. import { UPLOAD_BIZ_TYPE, uploadFileApi } from '@/api/minio';
  123. import { AccidentPersonnelInfoStruct, AddAccidentInfoStruct, getAccidentInfoDetail } from '@/api/traffic-accident';
  124. import { msgConfirm } from '@/utils/element-plus/messageBox';
  125. const { realname } = useUserInfoHook();
  126. const route = useRoute();
  127. const operate = route.query.operate;
  128. const id = route.query.id;
  129. const formRef = ref<InstanceType<typeof ElForm>>();
  130. const formData = ref<AddAccidentInfoStruct>({
  131. trafficAccidentRecord: {
  132. accidentImages: '',
  133. accidentLocation: '',
  134. accidentTime: '',
  135. accidentDescription: '',
  136. remark: '',
  137. createdByName: '',
  138. },
  139. accidentPersonnelInfoList: [{ carNum: '', accidentPersonnel: '', phoneNum: '' }],
  140. });
  141. const personnelList = ref<AccidentPersonnelInfoStruct[]>([]);
  142. // 保存初始表单数据用于比较
  143. const initialFormData = ref<AddAccidentInfoStruct>({
  144. trafficAccidentRecord: {
  145. accidentImages: '',
  146. accidentLocation: '',
  147. accidentTime: '',
  148. accidentDescription: '',
  149. remark: '',
  150. createdByName: '',
  151. },
  152. accidentPersonnelInfoList: [],
  153. });
  154. const initialPersonnelList = ref<AccidentPersonnelInfoStruct[]>([]);
  155. const initialImagesString = ref<string>('');
  156. const rules = reactive({
  157. 'trafficAccidentRecord.accidentLocation': [{ required: true, message: '请输入事故地点', trigger: 'blur' }],
  158. 'trafficAccidentRecord.accidentTime': [{ required: true, message: '请选择事故时间', trigger: 'change' }],
  159. 'trafficAccidentRecord.accidentDescription': [{ required: true, message: '请输入事故描述', trigger: 'blur' }],
  160. });
  161. const carNumRules = [
  162. {
  163. validator: (_: unknown, value: string, callback: (err?: Error) => void) => {
  164. if (!value) return callback();
  165. if (!/^.{0,8}$/.test(value)) return callback(new Error('车牌号格式错误'));
  166. callback();
  167. },
  168. trigger: ['blur', 'change'],
  169. },
  170. ];
  171. const personRules = [
  172. { required: true, message: '请输入事故人员姓名', trigger: 'blur' },
  173. { min: 1, max: 20, message: '最多20个字符', trigger: 'blur' },
  174. ];
  175. const phoneRules = [
  176. {
  177. validator: (_: unknown, value: string, callback: (err?: Error) => void) => {
  178. if (!value) return callback();
  179. if (!/^1\d{10}$/.test(value)) return callback(new Error('联系方式非11位数字'));
  180. callback();
  181. },
  182. trigger: ['blur', 'change'],
  183. },
  184. ];
  185. // 新增
  186. const addPersonnel = () => {
  187. if (personnelList.value.length >= 10) return;
  188. personnelList.value.push({ carNum: '', accidentPersonnel: '', phoneNum: '' });
  189. };
  190. // 删除
  191. const removePersonnel = (index: number) => {
  192. if (personnelList.value.length === 1) return;
  193. personnelList.value.splice(index, 1);
  194. };
  195. // 禁用未来日期
  196. const disabledDate = (time: Date) => {
  197. return time.getTime() > Date.now();
  198. };
  199. // 禁用未来时间
  200. const disabledTime = (date: Date) => {
  201. const now = new Date();
  202. if (date.toDateString() === now.toDateString()) {
  203. return {
  204. disabledHours: () => {
  205. const hours: number[] = [];
  206. for (let i = now.getHours() + 1; i < 24; i++) {
  207. hours.push(i);
  208. }
  209. return hours;
  210. },
  211. disabledMinutes: (hour: number) => {
  212. if (hour === now.getHours()) {
  213. const minutes: number[] = [];
  214. for (let i = now.getMinutes() + 1; i < 60; i++) {
  215. minutes.push(i);
  216. }
  217. return minutes;
  218. }
  219. return [];
  220. },
  221. disabledSeconds: (hour: number, minute: number) => {
  222. if (hour === now.getHours() && minute === now.getMinutes()) {
  223. const seconds: number[] = [];
  224. for (let i = now.getSeconds() + 1; i < 60; i++) {
  225. seconds.push(i);
  226. }
  227. return seconds;
  228. }
  229. return [];
  230. },
  231. };
  232. }
  233. return {};
  234. };
  235. // 事故图片
  236. const uploadImagesRef = ref<InstanceType<typeof UploadImages>>();
  237. const uploadImages = ref<ImageItem[]>([]);
  238. const imagesString = ref<string>('');
  239. const recordImageList = computed(() => {
  240. if (!formData.value.trafficAccidentRecord.accidentImages) return [];
  241. return JSON.parse(formData.value.trafficAccidentRecord.accidentImages);
  242. });
  243. // 格式化事故图片
  244. const formatImageList = async (file: File) => {
  245. if (!file) return file;
  246. const fileName = file.name;
  247. const res = await uploadFileApi({ bizType: UPLOAD_BIZ_TYPE.ATTACHMENT, fileName, file });
  248. return '"' + res.url + '"';
  249. };
  250. const handleUploadChange = async () => {
  251. uploadImages.value = uploadImagesRef.value!.getUploadedImages();
  252. const images = await Promise.all(
  253. (uploadImages.value || []).map((item) => {
  254. if (!item.file && item.url) {
  255. return '"' + item.url + '"';
  256. } else {
  257. return formatImageList(item.file!);
  258. }
  259. }),
  260. );
  261. imagesString.value = '[' + images.toString() + ']';
  262. };
  263. // 检测表单是否有修改
  264. const hasFormChanged = () => {
  265. // 比较事故记录数据
  266. const currentRecord = formData.value.trafficAccidentRecord;
  267. const initialRecord = initialFormData.value.trafficAccidentRecord;
  268. if (
  269. currentRecord.accidentLocation !== initialRecord.accidentLocation ||
  270. currentRecord.accidentTime !== initialRecord.accidentTime ||
  271. currentRecord.accidentDescription !== initialRecord.accidentDescription ||
  272. currentRecord.remark !== initialRecord.remark ||
  273. imagesString.value !== initialImagesString.value
  274. ) {
  275. return true;
  276. }
  277. // 比较人员信息数据
  278. if (personnelList.value.length !== initialPersonnelList.value.length) {
  279. return true;
  280. }
  281. for (let i = 0; i < personnelList.value.length; i++) {
  282. const current = personnelList.value[i];
  283. const initial = initialPersonnelList.value[i];
  284. if (
  285. current.carNum !== initial.carNum ||
  286. current.accidentPersonnel !== initial.accidentPersonnel ||
  287. current.phoneNum !== initial.phoneNum
  288. ) {
  289. return true;
  290. }
  291. }
  292. return false;
  293. };
  294. // 保存初始数据
  295. const saveInitialData = () => {
  296. initialFormData.value = JSON.parse(JSON.stringify(formData.value));
  297. initialPersonnelList.value = JSON.parse(JSON.stringify(personnelList.value));
  298. initialImagesString.value = imagesString.value;
  299. };
  300. // 重置表单状态(提交成功后调用)
  301. const resetFormState = () => {
  302. saveInitialData();
  303. };
  304. const handleValidate = async () => {
  305. const form = formRef.value;
  306. if (!form) return false;
  307. try {
  308. await form.validate();
  309. return true;
  310. } catch (_) {
  311. return false;
  312. }
  313. };
  314. const getFormData = (): AddAccidentInfoStruct => {
  315. formData.value.trafficAccidentRecord.accidentImages = imagesString.value;
  316. const payload: AddAccidentInfoStruct = {
  317. trafficAccidentRecord: formData.value.trafficAccidentRecord,
  318. accidentPersonnelInfoList: personnelList.value.map(({ carNum, accidentPersonnel, phoneNum }) => ({
  319. carNum,
  320. accidentPersonnel,
  321. phoneNum,
  322. })),
  323. } as unknown as AddAccidentInfoStruct;
  324. return payload;
  325. };
  326. // 路由守卫 - 离开页面前的确认
  327. onBeforeRouteLeave((to, from, next) => {
  328. const hasChange = hasFormChanged();
  329. if (!hasChange) {
  330. next();
  331. return;
  332. }
  333. setTimeout(() => {
  334. msgConfirm('当前页面存在修改,是否确认离开当前页面?', '提示', {
  335. confirmButtonText: '确认',
  336. cancelButtonText: '取消',
  337. customClass: 'customMessageBox--warning',
  338. })
  339. .then(() => {
  340. next();
  341. })
  342. .catch(() => {
  343. next(false);
  344. });
  345. }, 200);
  346. });
  347. defineExpose({ handleValidate, getFormData, resetFormState });
  348. onMounted(() => {
  349. formData.value.trafficAccidentRecord.createdByName = realname;
  350. personnelList.value = formData.value.accidentPersonnelInfoList;
  351. if (operate === 'edit') {
  352. getAccidentInfoDetail({ accidentRecordId: Number(id) }).then((res) => {
  353. formData.value = res;
  354. personnelList.value = res.accidentPersonnelInfoList;
  355. imagesString.value = res.trafficAccidentRecord.accidentImages;
  356. // 数据加载完成后保存初始数据
  357. setTimeout(() => {
  358. saveInitialData();
  359. }, 100);
  360. });
  361. } else {
  362. // 创建模式,立即保存初始数据
  363. setTimeout(() => {
  364. saveInitialData();
  365. }, 100);
  366. }
  367. });
  368. </script>
  369. <style scoped lang="scss">
  370. .accident-create-container {
  371. .personnel-section {
  372. background: #f5f9ff;
  373. border-radius: 4px;
  374. padding: 20px 34px;
  375. margin-bottom: 30px;
  376. .personnel-item {
  377. margin-bottom: 20px;
  378. :deep(.el-form-item) {
  379. margin-bottom: 0;
  380. }
  381. .el-row {
  382. align-items: center;
  383. }
  384. }
  385. .delete-col {
  386. height: 100%;
  387. display: flex;
  388. align-items: center;
  389. justify-content: flex-end;
  390. }
  391. }
  392. }
  393. </style>