basicInfo.vue 18 KB

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