tabulation.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <template>
  2. <MainPanel>
  3. <template v-slot:header>
  4. <Header title="现场绘图 | 制表" :on-back="onBack" type="return">
  5. <ui-button type="primary" @click="saveHandler" width="96px"> 完成 </ui-button>
  6. </Header>
  7. </template>
  8. <div class="tab-layout" v-if="roadPhoto" :class="{ downMode }">
  9. <div class="content" ref="layoutRef">
  10. <table>
  11. <tr>
  12. <td class="value title" colspan="6" height="46">
  13. <span v-if="downMode">{{ roadPhoto.title }}</span>
  14. <ui-input
  15. v-else
  16. type="text"
  17. @input="input"
  18. v-model="roadPhoto.title"
  19. @blur="history.push"
  20. />
  21. </td>
  22. </tr>
  23. <tr>
  24. <td class="label" width="218" height="40">到达事故现场时间</td>
  25. <td class="value">
  26. <span v-if="downMode">{{ history.value.arrivalTime }}</span>
  27. <ui-input
  28. v-else
  29. type="text"
  30. @input="input"
  31. v-model="history.value.arrivalTime"
  32. @blur="history.push"
  33. />
  34. </td>
  35. <td class="label" width="100">天气</td>
  36. <td class="value" width="116">
  37. <span v-if="downMode">{{ history.value.weather }}</span>
  38. <ui-input
  39. v-else
  40. type="text"
  41. @input="input"
  42. v-model="history.value.weather"
  43. @blur="history.push"
  44. />
  45. </td>
  46. <td class="label" width="100">路面性质</td>
  47. <td class="value" width="150">
  48. <span v-if="downMode">{{ history.value.conditions }}</span>
  49. <ui-input
  50. v-else
  51. type="text"
  52. @input="input"
  53. v-model="history.value.conditions"
  54. @blur="history.push"
  55. />
  56. </td>
  57. </tr>
  58. <tr>
  59. <td class="label" height="40">事故发生地点</td>
  60. <td class="value" colspan="5">
  61. <span v-if="downMode">{{ history.value.location }}</span>
  62. <ui-input
  63. v-else
  64. type="text"
  65. @input="input"
  66. v-model="history.value.location"
  67. @blur="history.push"
  68. />
  69. </td>
  70. </tr>
  71. <tr>
  72. <td class="image" colspan="6" height="569">
  73. <div class="photo-layout">
  74. <ui-icon type="reset" class="reset" @click="resetHandler" />
  75. <img
  76. :src="useStaticUrl(roadPhoto.url).value"
  77. @blur="history.push"
  78. class="photo"
  79. :style="{ transform: photoCSSMatrix }"
  80. ref="photoRef"
  81. />
  82. <img
  83. src="/static/compass.png"
  84. class="compass"
  85. :style="{ transform: compassCSSMatrix }"
  86. ref="compassRef"
  87. />
  88. <!-- <p class="compass-info">比例1 : {{ proportion }}</p> -->
  89. </div>
  90. </td>
  91. </tr>
  92. <tr>
  93. <td class="value textarea-layout" colspan="6" height="62">
  94. <span v-if="downMode">{{ history.value.illustrate }}</span>
  95. <ui-input
  96. class="textarea"
  97. v-else
  98. type="textarea"
  99. @input="input"
  100. v-model="history.value.illustrate"
  101. @blur="history.push"
  102. :maxlength="120"
  103. />
  104. </td>
  105. </tr>
  106. <tr>
  107. <td class="value date" colspan="6" height="44">
  108. 绘图时间:{{ formatDate(new Date(), "yyyy年MM月dd日hh时mm分") }}
  109. </td>
  110. </tr>
  111. </table>
  112. <div class="signatures">
  113. <p class="signature">勘察员:</p>
  114. <p class="signature">绘图员:</p>
  115. <p class="signature">当事人签字:</p>
  116. <p class="signature">见证人签字:</p>
  117. </div>
  118. </div>
  119. </div>
  120. </MainPanel>
  121. </template>
  122. <script setup lang="ts">
  123. import { router, writeRouteName } from "@/router";
  124. import { formatDate } from "@/utils";
  125. import { computed, nextTick, onDeactivated, ref, watchEffect } from "vue";
  126. import { useHistory } from "@/hook/useHistory";
  127. import { roadPhotos, RoadPhoto, getDefaultTable } from "@/store/roadPhotos";
  128. import { useStaticUrl } from "@/hook/useStaticUrl";
  129. import html2canvas from "html2canvas";
  130. import UiButton from "@/components/base/components/button/index.vue";
  131. import UiInput from "@/components/base/components/input/index.vue";
  132. import { HandMode, useHand } from "@/hook/useHand";
  133. import Header from "@/components/photos/header.vue";
  134. import MainPanel from "@/components/main-panel/index.vue";
  135. import { downloadImage, uploadImage } from "@/store/sync";
  136. import { Mode } from "@/views/graphic/menus";
  137. import Message from "@/components/base/components/message/message.vue";
  138. import matruces from "@/utils/matruces";
  139. import { genUseLoading } from "@/hook";
  140. const roadPhoto = computed<RoadPhoto>(() => {
  141. let route, params, data;
  142. if (
  143. (route = router.currentRoute.value).name === writeRouteName.tabulation &&
  144. (params = route.params).id &&
  145. (data = roadPhotos.value.find((data) => data.id === params.id))
  146. ) {
  147. return data;
  148. } else {
  149. // router.back();
  150. }
  151. });
  152. const history = computed(
  153. () => roadPhoto.value && useHistory(getDefaultTable(roadPhoto.value))
  154. );
  155. const input = () => (history.value.state.hasRedo = false);
  156. const compassRef = ref<HTMLImageElement>();
  157. const { cssMatrix: compassCSSMatrix, matrix: compassMatrix } = useHand(
  158. compassRef,
  159. HandMode.Angle,
  160. () => {
  161. history.value.value.compassAngle = compassMatrix.value;
  162. history.value.push();
  163. },
  164. history.value.value.compassAngle
  165. );
  166. const photoRef = ref<HTMLImageElement>();
  167. const { cssMatrix: photoCSSMatrix, matrix: photoMatrix } = useHand(
  168. photoRef,
  169. HandMode.MoveAndScale,
  170. () => {
  171. history.value.value.imageTransform = photoMatrix.value;
  172. history.value.push();
  173. },
  174. history.value.value.imageTransform
  175. );
  176. const resetHandler = () => {
  177. photoMatrix.value = matruces.translateMatrix(0, 0, 0);
  178. history.value.push();
  179. console.log("?????");
  180. };
  181. onDeactivated(() => (photoLoaded.value = false));
  182. const proportion = ref(1);
  183. const photoLoaded = ref(false);
  184. watchEffect(() => {
  185. if (!roadPhoto.value || !photoRef.value) {
  186. return;
  187. }
  188. if (!photoLoaded.value) {
  189. photoRef.value.onload = () => (photoLoaded.value = true);
  190. return;
  191. }
  192. const scale = roadPhoto.value.data.scale / window.devicePixelRatio || 1;
  193. const martrixScale = photoMatrix.value[0];
  194. const photoWidth = photoRef.value.naturalWidth;
  195. const prop = ((photoWidth / photoRef.value.offsetWidth) * scale) / martrixScale;
  196. proportion.value = Math.ceil(1 / prop);
  197. });
  198. const onBack = () => {
  199. router.back();
  200. // router.replace({
  201. // name: writeRouteName.graphic,
  202. // params: { mode: Mode.Road, id: roadPhoto.value.id, action: "update" },
  203. // });
  204. };
  205. const downMode = ref(false);
  206. // const downMode = ref(true);
  207. const layoutRef = ref<HTMLDivElement>();
  208. const getLayoutImage = async () => {
  209. downMode.value = true;
  210. await nextTick();
  211. const canvas = await html2canvas(layoutRef.value);
  212. Message.success({ msg: "已保存至相册", time: 2000 });
  213. downMode.value = false;
  214. const blob = await new Promise<Blob>((resolve) =>
  215. canvas.toBlob(resolve, "image/jpeg", 0.95)
  216. );
  217. await downloadImage(blob, roadPhoto.value.id + ".jpg");
  218. return await uploadImage(blob);
  219. };
  220. const saveHandler = genUseLoading(async () => {
  221. let index = 1;
  222. let prex = "未命名";
  223. while (true) {
  224. if (roadPhotos.value.some((road) => road.title === `${prex}${index}`)) {
  225. index++;
  226. } else {
  227. break;
  228. }
  229. }
  230. roadPhoto.value.title = roadPhoto.value.title.trim() || `${prex}${index}`;
  231. // const repeatIndex = roadPhotos.value.findIndex(
  232. // (road) => road !== roadPhoto.value && road.title === roadPhoto.value.title
  233. // );
  234. // if (~repeatIndex && !(await useConfirm("检测到您有相同文件名的文件,确定要覆盖吗?"))) {
  235. // return;
  236. // }
  237. // if (~repeatIndex) {
  238. // roadPhotos.value.splice(repeatIndex, 1);
  239. // }
  240. roadPhoto.value.table = {
  241. ...history.value.value,
  242. url: await getLayoutImage(),
  243. };
  244. router.replace({ name: writeRouteName.roads });
  245. });
  246. </script>
  247. <style lang="scss" scoped>
  248. .tab-layout {
  249. position: absolute;
  250. top: calc(var(--header-top) + var(--editor-head-height));
  251. bottom: 0;
  252. overflow-y: auto;
  253. left: 0;
  254. right: 0;
  255. font-family: sr, st;
  256. color: #000;
  257. font-size: 20px;
  258. }
  259. .content {
  260. width: 100%;
  261. padding: 20px 20px 60px;
  262. margin: 0 auto;
  263. }
  264. .image {
  265. position: relative;
  266. .photo-layout {
  267. position: absolute;
  268. left: 0px;
  269. right: 0px;
  270. bottom: 0px;
  271. top: 0px;
  272. overflow: hidden;
  273. display: flex;
  274. justify-content: center;
  275. .photo {
  276. max-width: 100%;
  277. max-height: 100%;
  278. align-items: center;
  279. }
  280. .compass {
  281. top: 20px;
  282. }
  283. .compass,
  284. .compass-info {
  285. position: absolute;
  286. right: 20px;
  287. width: 128px;
  288. height: 128px;
  289. top: 20px;
  290. }
  291. .compass-info {
  292. text-align: center;
  293. color: #000;
  294. font-size: 16px;
  295. margin-top: 128px;
  296. pointer-events: none;
  297. white-space: nowrap;
  298. }
  299. }
  300. }
  301. .content table {
  302. width: calc(100% - 2px);
  303. // height: 800px;
  304. border-collapse: collapse;
  305. tr:not(:first-child) {
  306. &:nth-child(2) td {
  307. border-top: 2px solid #000;
  308. }
  309. td:first-child {
  310. border-left: 2px solid #000;
  311. }
  312. td {
  313. border-right: 2px solid #000;
  314. border-bottom: 2px solid #000;
  315. }
  316. }
  317. .label {
  318. text-align: center;
  319. }
  320. .value {
  321. height: 43px;
  322. background-color: #d4e8ff;
  323. }
  324. .title {
  325. padding-bottom: 7px;
  326. position: relative;
  327. &:after {
  328. content: "";
  329. position: absolute;
  330. bottom: 0;
  331. left: 0;
  332. right: 0;
  333. height: 8px;
  334. background-color: #fff;
  335. }
  336. }
  337. .date {
  338. text-align: right;
  339. padding-right: 44px;
  340. }
  341. .reset {
  342. position: absolute;
  343. left: 24px;
  344. top: 24px;
  345. z-index: 99;
  346. width: 32px;
  347. height: 32px;
  348. background: #ffffff;
  349. color: #000;
  350. box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.5);
  351. border-radius: 55px 55px 55px 55px;
  352. opacity: 1;
  353. display: flex;
  354. align-items: center;
  355. justify-content: center;
  356. font-size: 16px;
  357. color: #000;
  358. }
  359. }
  360. .downMode {
  361. .reset {
  362. display: none !important;
  363. }
  364. .title {
  365. span {
  366. height: 52px !important;
  367. font-size: 46px !important;
  368. font-weight: 400;
  369. color: #000000;
  370. line-height: 52px;
  371. letter-spacing: 10px;
  372. }
  373. }
  374. .content {
  375. width: 1485px;
  376. height: 1050px;
  377. padding: 125px 100px 75px 100px;
  378. // padding: 30px 28px 26px;
  379. overflow: hidden;
  380. }
  381. .content table .textarea-layout {
  382. height: 60px;
  383. span {
  384. padding: 5px 0px;
  385. display: block;
  386. height: 100%;
  387. }
  388. }
  389. .content table {
  390. .value {
  391. background: none;
  392. }
  393. tr:not(:first-child) {
  394. td {
  395. border-right: 1px solid #000;
  396. border-bottom: 1px solid #000;
  397. }
  398. &:nth-child(2) td {
  399. border-top: 2px solid #000;
  400. }
  401. td:first-child {
  402. border-left: 2px solid #000;
  403. }
  404. td:last-child {
  405. border-right: 2px solid #000;
  406. }
  407. &:last-child td {
  408. border-bottom: 2px solid #000;
  409. }
  410. }
  411. }
  412. }
  413. .signatures {
  414. margin-top: 10px;
  415. display: flex;
  416. font-size: 16px;
  417. .signature {
  418. flex: 1;
  419. }
  420. }
  421. </style>
  422. <style lang="scss">
  423. .value {
  424. box-sizing: border-box;
  425. // padding: 8px 10px;
  426. padding: 0 10px;
  427. input,
  428. .ui-input {
  429. width: 100%;
  430. height: 32px !important;
  431. outline: none !important;
  432. color: #000 !important;
  433. border: 1px #000 dotted !important;
  434. font-size: 16px !important;
  435. line-height: 32px !important;
  436. vertical-align: middle !important;
  437. }
  438. }
  439. .title {
  440. span {
  441. display: block;
  442. font-size: 32px !important;
  443. height: 48px !important;
  444. font-weight: 400;
  445. text-align: center;
  446. line-height: 48px !important;
  447. }
  448. input,
  449. .ui-input {
  450. font-size: 32px !important;
  451. height: 48px !important;
  452. font-weight: bold;
  453. text-align: center;
  454. line-height: 48px !important;
  455. }
  456. }
  457. .textarea {
  458. textarea {
  459. color: #000 !important;
  460. font-size: 16px !important;
  461. line-height: 1.2em;
  462. }
  463. }
  464. .textarea-layout {
  465. .textarea {
  466. height: 100% !important;
  467. }
  468. .retouch {
  469. justify-content: flex-end !important;
  470. .len {
  471. color: #000 !important;
  472. }
  473. }
  474. }
  475. </style>