basicInfo.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <template>
  2. <div class="basic-info">
  3. <div class="caseTitle">案件信息</div>
  4. <!-- 展示模式:按来源路由分支展示 -->
  5. <div class="show-view-content" v-if="props.editOrShow === 'show'">
  6. <!-- criminal 展示 -->
  7. <div v-if="props.fromRoute === 'criminal'" class="camera-from show-view">
  8. <!-- <div class="form-title">案件信息</div> -->
  9. <div class="info-row"><span class="label">案件名称:</span><span class="value">{{ bindFire?.caseTitle || '-' }}</span></div>
  10. <div class="info-row"><span class="label">详细地址:</span><span class="value">{{ bindFire?.mapUrl || '-' }}</span></div>
  11. </div>
  12. <!-- fire 展示(原有) -->
  13. <div v-else class="camera-from show-view">
  14. <div class="all-content no-scrollbar">
  15. <!-- <div class="form-title">案件信息</div> -->
  16. <div class="info-row"><span class="label">项目编号:</span><span class="value">{{ bindFire.projectSn || '-' }}</span></div>
  17. <div class="info-row"><span class="label">起火对象:</span><span class="value">{{ bindFire.projectName || '-' }}</span></div>
  18. <div class="info-row"><span class="label">详细地址:</span><span class="value">{{ bindFire.mapUrl || '-' }}</span></div>
  19. <div class="info-row"><span class="label">起火地址:</span><span class="value">{{ bindFire.projectAddress || '-' }}</span></div>
  20. <div class="info-row"><span class="label">起火场所:</span><span class="value">{{ bindFire.projectSite || '-' }}</span></div>
  21. <div class="info-row"><span class="label">承办单位:</span><span class="value">{{ bindFire.organizerDeptName || '-' }}</span></div>
  22. <div class="info-row"><span class="label">承办人员:</span><span class="value">{{ bindFire.organizerUsers || '-' }}</span></div>
  23. <div class="info-row"><span class="label">事故日期:</span><span class="value">{{ bindFire.accidentDate || '-' }}</span></div>
  24. <div class="info-row"><span class="label">火灾原因:</span><span class="value">{{ bindFire.fireReason || '-' }}</span></div>
  25. </div>
  26. </div>
  27. </div>
  28. <!-- 编辑模式:按来源路由分支展示 -->
  29. <template v-else>
  30. <!-- criminal 编辑(复原 edit.vue) -->
  31. <el-form v-if="props.fromRoute === 'criminal'" ref="form" label-width="96px" class="camera-from">
  32. <div class="all-content">
  33. <!-- <div class="form-title">案件信息</div> -->
  34. <el-form-item label="案件名称">
  35. <el-input v-model="bindFire.caseTitle" maxlength="50" placeholder="请输入案件名称" />
  36. </el-form-item>
  37. <el-form-item label="详细地址" class="mandatory">
  38. <el-input v-model="bindFire.mapUrl" placeholder="输入名称搜索" clearable disabled>
  39. <template #append>
  40. <el-button :icon="Search" @click="searchAMapAddress" />
  41. </template>
  42. </el-input>
  43. </el-form-item>
  44. </div>
  45. </el-form>
  46. <!-- fire 编辑(原有) -->
  47. <el-form v-else ref="form" label-width="100px" class="camera-from">
  48. <div class="all-content">
  49. <!-- <div class="form-title">案件信息</div> -->
  50. <el-form-item label="项目编号" class="mandatory">
  51. <el-input
  52. v-model="bindFire.projectSn"
  53. maxlength="18"
  54. placeholder="请输入项目编号"
  55. />
  56. </el-form-item>
  57. <el-form-item label="起火对象" class="mandatory">
  58. <el-input
  59. v-model="bindFire.projectName"
  60. maxlength="50"
  61. placeholder="请输入起火对象"
  62. />
  63. </el-form-item>
  64. <el-form-item label="详细地址">
  65. <el-input
  66. v-model="bindFire.mapUrl"
  67. placeholder="输入名称搜索"
  68. clearable
  69. readonly
  70. disabled
  71. class="mandatory"
  72. >
  73. <template #append>
  74. <el-button :icon="Search" @click="searchAMapAddress" />
  75. </template>
  76. </el-input>
  77. </el-form-item>
  78. <el-form-item label="起火地址" class="mandatory">
  79. <el-input
  80. v-model="bindFire.projectAddress"
  81. maxlength="50"
  82. placeholder="请输入起火地址"
  83. />
  84. </el-form-item>
  85. <el-form-item label="起火场所" class="mandatory">
  86. <el-cascader
  87. style="width: 100%"
  88. v-model="projectSite"
  89. placeholder="起火场所"
  90. :options="place"
  91. :props="{ expandTrigger: 'hover' }"
  92. />
  93. </el-form-item>
  94. <el-form-item label="承办单位" class="mandatory">
  95. <companySelect v-model="bindFire.deptId" hideAll :notUpdate="true" disabled />
  96. </el-form-item>
  97. <el-form-item label="承办人员" class="mandatory" placeholder="请输入承办人员">
  98. <el-input v-model="bindFire.organizerUsers" maxlength="50" />
  99. </el-form-item>
  100. <el-form-item label="事故日期" class="mandatory" placeholder="请选择事故日期">
  101. <el-date-picker
  102. type="date"
  103. v-model="accidentDate"
  104. style="width: 100%"
  105. :disabled-date="(date) => date.getTime() > new Date().getTime()"
  106. />
  107. </el-form-item>
  108. <el-form-item label="火灾原因" class="mandatory">
  109. <el-cascader
  110. style="width: 100%"
  111. v-model="fireReason"
  112. placeholder="火灾原因:"
  113. :options="reason"
  114. :props="{ expandTrigger: 'hover' }"
  115. />
  116. </el-form-item>
  117. </div>
  118. </el-form>
  119. </template>
  120. </div>
  121. <!-- 地图弹窗仅 fire 使用 -->
  122. <!-- 地图弹窗改为统一使用,无论路由来源 -->
  123. <creatMap v-model="showMapDialog" :caseId="caseId" @confirm="handleMapConfirm" />
  124. </template>
  125. <script setup lang="ts">
  126. import companySelect from "@/components/company-select/index.vue";
  127. import { ref, watch, toRef, onMounted, onUnmounted, onBeforeUnmount } from "vue";
  128. import { Fire, setFire, addFire } from "@/app/fire/store/fire";
  129. import { reason, place } from "@/app/fire/constant/fire";
  130. import { ElMessage } from "element-plus";
  131. import { dateFormat, debounce } from "@/util";
  132. import { genCascaderValue, getCode } from "@/helper/cascader";
  133. import { QuiskExpose } from "@/helper/mount";
  134. import { user } from "@/store/user";
  135. import { Search } from "@element-plus/icons-vue";
  136. // 旧的选择地图逻辑已统一为 creatMap 弹窗,移除 selectMapImage
  137. import { Example, setExample as setCriminalExample, addExample as addCriminalExample } from "@/app/criminal/store/example";
  138. import { getCaseInfoOffline as getCaseInfo } from "@/store/editCsae";
  139. import creatMap from "./creatMap.vue";
  140. const props = defineProps<{ fire?: Fire, caseId?: number, editOrShow?: string, fromRoute?: string }>();
  141. const caseId = toRef(props, 'caseId');
  142. let bindFire = ref<Fire>( props.fire ? { ...props.fire } : ({ deptId: user.value.info.deptId} as Fire));
  143. const accidentDate = ref(
  144. bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
  145. );
  146. watch(() => props.fire, (newVal, oldVal) => {
  147. bindFire.value = newVal ? { ...newVal } : ({ deptId: user.value.info.deptId} as Fire)
  148. accidentDate.value = bindFire.value.accidentDate ? new Date(bindFire.value.accidentDate) : new Date()
  149. })
  150. const dispatchRecordUpdate = () => {
  151. try {
  152. const title = props.fromRoute === 'criminal' ? (bindFire.value as any).caseTitle : (bindFire.value as any).projectName;
  153. const mapUrl = (bindFire.value as any).mapUrl;
  154. const latAndLong = (bindFire.value as any).latAndLong;
  155. window.dispatchEvent(
  156. new CustomEvent('fireDetails:updateTitle', {
  157. detail: {
  158. title,
  159. mapUrl,
  160. latAndLong,
  161. fire: props.fromRoute === 'fire' ? { ...(bindFire.value as any) } : undefined,
  162. criminal: props.fromRoute === 'criminal' ? { ...(bindFire.value as any) } : undefined,
  163. },
  164. })
  165. );
  166. } catch (e) {}
  167. };
  168. const showMapDialog = ref(false)
  169. const fireReason = genCascaderValue(bindFire, "fireReason");
  170. const projectSite = genCascaderValue(bindFire, "projectSite");
  171. const firstReload = ref(0)
  172. // ========== 自动保存(有变化就保存,无需按钮) ==========
  173. // 仅在数据满足基本必填项时才触发保存;使用防抖避免频繁请求
  174. const isValidForAutoSave = () => {
  175. const v = bindFire.value;
  176. // 自动保存不弹错误,只在必填项齐备时保存
  177. return (
  178. !!v.latAndLong && !!v.projectAddress && !!v.projectSn && !!v.projectName &&
  179. !!v.projectSite && !!v.deptId && !!v.organizerUsers && !!fireReason.value && !!accidentDate.value
  180. );
  181. };
  182. // 用于避免重复保存同一状态
  183. let lastSavedSnapshot = "";
  184. const getSnapshot = () => {
  185. const v = { ...bindFire.value } as any;
  186. v.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
  187. v.projectSiteCode = getCode(place, bindFire.value.projectSite);
  188. return JSON.stringify({
  189. projectSn: v.projectSn,
  190. projectName: v.projectName,
  191. mapUrl: v.mapUrl,
  192. projectAddress: v.projectAddress,
  193. latAndLong: v.latAndLong,
  194. deptId: v.deptId,
  195. organizerUsers: v.organizerUsers,
  196. projectSite: v.projectSite,
  197. projectSiteCode: v.projectSiteCode,
  198. fireReason: v.fireReason,
  199. accidentDate: v.accidentDate,
  200. caseId: v.caseId,
  201. id: v.id,
  202. });
  203. };
  204. const autoSave = async () => {
  205. // criminal 路由不触发 fire 自动保存
  206. if (props.fromRoute === 'criminal') return;
  207. if (props.editOrShow === 'show') return;
  208. // if (!isValidForAutoSave()) return;
  209. const snapshot = getSnapshot();
  210. if (snapshot === lastSavedSnapshot) return;
  211. // 写入派生字段
  212. bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
  213. bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
  214. try {
  215. if (bindFire.value.id) {
  216. console.log('auto-save update', bindFire.value);
  217. await setFire(bindFire.value);
  218. if (firstReload.value != 0) {
  219. ElMessage.success('保存成功');
  220. }
  221. firstReload.value += 1;
  222. } else {
  223. await addFire(bindFire.value as any);
  224. ElMessage.success('新增成功');
  225. }
  226. lastSavedSnapshot = snapshot;
  227. // 保存成功后派发更新事件,供父组件同步 currentRecord
  228. dispatchRecordUpdate();
  229. // 自动保存成功后不刷新页面,也不打扰用户
  230. } catch (e) {
  231. // 自动保存失败不打断填写,可在控制台查看错误
  232. console.error("auto-save error", e);
  233. }
  234. };
  235. // 防抖触发:用户停顿一会儿再保存
  236. const triggerAutoSave = debounce(() => autoSave(), 800);
  237. // ========== criminal 自动保存(与 edit.vue 保存逻辑一致) ==========
  238. const isValidForAutoSaveCriminal = () => {
  239. const v: any = bindFire.value;
  240. return !!v.caseTitle && !!String(v.caseTitle).trim() && !!v.latAndLong && !!String(v.latAndLong).trim();
  241. };
  242. let lastSavedSnapshotCriminal = "";
  243. const getSnapshotCriminal = () => {
  244. const v: any = bindFire.value;
  245. return JSON.stringify({
  246. caseTitle: v.caseTitle,
  247. mapUrl: v.mapUrl,
  248. latAndLong: v.latAndLong,
  249. caseId: v.caseId,
  250. id: v.id,
  251. });
  252. };
  253. const autoSaveCriminal = async () => {
  254. if (props.fromRoute !== 'criminal') return;
  255. if (props.editOrShow === 'show') return;
  256. // if (!isValidForAutoSaveCriminal()) return;
  257. const snapshot = getSnapshotCriminal();
  258. if (snapshot === lastSavedSnapshotCriminal) return;
  259. try {
  260. if ((bindFire.value as any).caseId) {
  261. await setCriminalExample(bindFire.value as any);
  262. if (firstReload.value != 0) {
  263. ElMessage.success('保存成功');
  264. }
  265. firstReload.value += 1;
  266. } else {
  267. await addCriminalExample(bindFire.value as any);
  268. ElMessage.success('新增成功');
  269. }
  270. lastSavedSnapshotCriminal = snapshot;
  271. // 保存成功后派发标题更新事件,附带地图信息,供父组件同步 currentRecord
  272. dispatchRecordUpdate();
  273. } catch (e) {
  274. console.error('criminal auto-save error', e);
  275. }
  276. };
  277. const triggerAutoSaveCriminal = debounce(() => autoSaveCriminal(), 800);
  278. // 深度监听表单对象;日期单独监听
  279. watch(bindFire, () => {
  280. if (props.editOrShow === 'show') return;
  281. if (props.fromRoute === 'criminal') {
  282. triggerAutoSaveCriminal();
  283. } else {
  284. triggerAutoSave();
  285. }
  286. }, { deep: true });
  287. watch(accidentDate, () => {
  288. if (props.editOrShow === 'show') return;
  289. if (props.fromRoute !== 'criminal') triggerAutoSave();
  290. });
  291. onBeforeUnmount(() => {
  292. if (props.editOrShow === 'show') return;
  293. dispatchRecordUpdate();
  294. });
  295. // 监听来自 header 的重命名事件,并直接触发保存以更新标题
  296. onMounted(() => {
  297. const renameHandler = (evt: any) => {
  298. const title = evt?.detail?.title || '';
  299. if (!title || !String(title).trim()) return;
  300. if (props.fromRoute === 'criminal') {
  301. (bindFire.value as any).caseTitle = title;
  302. if (props.editOrShow !== 'show') {
  303. autoSaveCriminal();
  304. }
  305. } else {
  306. (bindFire.value as any).projectName = title;
  307. if (props.editOrShow !== 'show') {
  308. autoSave();
  309. }
  310. }
  311. };
  312. window.addEventListener('fireDetails:renameTitle', renameHandler as any);
  313. onUnmounted(() => {
  314. window.removeEventListener('fireDetails:renameTitle', renameHandler as any);
  315. });
  316. });
  317. defineExpose<QuiskExpose>({
  318. async submit() {
  319. if (props.editOrShow === 'show') return;
  320. if (props.fromRoute === 'criminal') {
  321. if (!bindFire.value.caseTitle || !bindFire.value.caseTitle.trim()) {
  322. ElMessage.error("案件名称不能为空");
  323. throw "案件名称不能为空";
  324. } else if (!bindFire.value.latAndLong || !bindFire.value.latAndLong.trim()) {
  325. ElMessage.error("详细地址不能为空");
  326. throw "详细地址不能为空!";
  327. }
  328. await (bindFire.value.caseId ? setCriminalExample(bindFire.value as any) : addCriminalExample(bindFire.value as any));
  329. // 保存成功后刷新页面数据
  330. window.location.reload()
  331. } else {
  332. if (!bindFire.value.latAndLong || !bindFire.value.latAndLong.trim()) {
  333. ElMessage.error("详细地址不能为空");
  334. throw "详细地址不能为空!";
  335. } else if (!bindFire.value.projectAddress || !bindFire.value.projectAddress.trim()) {
  336. ElMessage.error("起火地址不能为空!");
  337. throw "起火地址不能为空!";
  338. } else if (!bindFire.value.projectSn || !bindFire.value.projectSn.trim()) {
  339. ElMessage.error("项目编号不能为空!");
  340. throw "项目编号不能为空!";
  341. } else if (!bindFire.value.projectName || !bindFire.value.projectName.trim()) {
  342. ElMessage.error("起火对象不能为空!");
  343. throw "起火对象不能为空!";
  344. } else if (!bindFire.value.projectSite || !bindFire.value.projectSite.trim()) {
  345. ElMessage.error("起火场所不能为空!");
  346. throw "起火场所不能为空!";
  347. } else if (!bindFire.value.deptId || !bindFire.value.deptId.trim()) {
  348. ElMessage.error("承办单位不能为空!");
  349. throw "承办单位不能为空!";
  350. } else if (!bindFire.value.organizerUsers || !bindFire.value.organizerUsers.trim()) {
  351. ElMessage.error("承办人员不能为空!");
  352. throw "承办人员不能为空!";
  353. } else if (!accidentDate) {
  354. ElMessage.error("事故日期不能为空!");
  355. throw "事故日期不能为空!";
  356. } else if (!bindFire.value.fireReason || !bindFire.value.fireReason.trim()) {
  357. ElMessage.error("火灾原因不能为空!");
  358. throw "火灾原因不能为空!";
  359. }
  360. bindFire.value.accidentDate = dateFormat(accidentDate.value, "yyyy-MM-dd");
  361. bindFire.value.projectSiteCode = getCode(place, bindFire.value.projectSite);
  362. // 保存数据
  363. if (bindFire.value.id) {
  364. await setFire(bindFire.value);
  365. } else {
  366. await addFire(bindFire.value as any);
  367. }
  368. // 保存成功后,刷新fireDetails页面的数据
  369. window.location.reload()
  370. }
  371. },
  372. });
  373. // 打开地图/选择地址
  374. const searchAMapAddress = async () => {
  375. // 统一使用 creatMap 弹窗进行位置选择
  376. showMapDialog.value = true;
  377. };
  378. // 处理地图确认选择
  379. const handleMapConfirm = (LocationInfo: any) => {
  380. console.log(LocationInfo, 666)
  381. const {cityname, adname, address, name, location} = LocationInfo;
  382. bindFire.value.mapUrl = cityname + adname + address + name;
  383. bindFire.value.latlng = bindFire.value.latAndLong = `${location.lat},${location.lng}`;
  384. showMapDialog.value = false;
  385. }
  386. </script>
  387. <style scoped lang="scss">
  388. .basic-info{
  389. width: 100%;
  390. height: calc(100% - 174px);
  391. background: #f5f7fa;
  392. padding: 24px 0;
  393. .show-view-content{
  394. height: calc(100% - 20px);
  395. }
  396. .caseTitle{
  397. width: 60%;
  398. text-align: center;
  399. background: #FFFFFF;
  400. margin: 0 auto;
  401. padding: 48px 140px 0px 140px;
  402. font-size: 24px;
  403. }
  404. }
  405. .camera-from {
  406. width: 60%;
  407. height: calc(100% - 185px);
  408. background: #FFFFFF;
  409. margin: 0 auto;
  410. padding: 40px 140px 48px 140px;
  411. .all-content{
  412. height: calc(100% - 0px);
  413. padding-right: 60px;
  414. overflow: auto;
  415. }
  416. .no-scrollbar {
  417. overflow-y: scroll; /* 允许垂直滚动 */
  418. scrollbar-width: none; /* Firefox */
  419. -ms-overflow-style: none; /* Internet Explorer 10+ */
  420. }
  421. .no-scrollbar::-webkit-scrollbar {
  422. /* Chrome, Safari, Opera */
  423. display: none;
  424. }
  425. .form-title{
  426. text-align: center;
  427. font-family: Microsoft YaHei, Microsoft YaHei;
  428. font-weight: 400;
  429. font-size: 24px;
  430. color: rgba(0,0,0,0.85);
  431. line-height: 36px;
  432. margin-bottom: 44px;
  433. }
  434. .mandatory{
  435. .el-input-group__append button.el-button{
  436. border-left: 0;
  437. }
  438. }
  439. }
  440. </style>
  441. <style scoped lang="scss">
  442. .show-view {
  443. .info-row {
  444. display: flex;
  445. // align-items: center;
  446. margin-bottom: 40px;
  447. .label {
  448. width: 84px;
  449. color: rgba(0, 0, 0, 0.85);
  450. }
  451. .value {
  452. flex: 1;
  453. text-align: left;
  454. color: rgba(0, 0, 0, 0.85);
  455. }
  456. }
  457. }
  458. .camera-from{
  459. :deep(.el-form-item){
  460. margin-bottom: 40px;
  461. }
  462. :deep(.el-form-item__label){
  463. padding-right: 24px;
  464. }
  465. }
  466. </style>