Forráskód Böngészése

添加绘制相关类

bill 2 éve
szülő
commit
245d92710a

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "mitt": "^3.0.0",
     "sass": "^1.62.0",
     "sass-loader": "^13.2.2",
+    "stateshot": "^1.3.5",
     "vue": "^3.2.47",
     "vue-cropper": "1.0.2",
     "vue-i18n": "^9.2.2",

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
server/test/SS-t-P1d6CwREny2/attach/sceneStore


+ 1 - 2
src/components/base/assets/scss/components/_input.scss

@@ -653,10 +653,9 @@
   
   list-style: none;
   max-height: 288px;
-  background: rgba(26, 26, 26, 0.8);
+  background: #161A1A;
   box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3),
     inset 0 0 1px rgb(255 255 255 / 90%);
-  backdrop-filter: blur(4px);
   border-radius: 4px;
   overflow-y: auto;
   color: var(--colors-content-color);

+ 4 - 1
src/components/fill-slide/index.vue

@@ -12,7 +12,10 @@
           show-ctrl
       >
         <template v-slot="{raw}">
-          <img :src="getStaticFile(raw.url)" class="image" />
+          <template v-if="$slots.default">
+            <slot :data="raw" />
+          </template>
+          <img :src="getStaticFile(raw.url)" class="image" v-else />
         </template>
       </ui-slide>
     </div>

+ 4 - 1
src/components/group-button/index.vue

@@ -8,7 +8,10 @@
       :class="{ active: activeKey === menu.key, dire }"
       @click="menu.onClick && menu.onClick(menu)"
     >
-      <ui-icon :type="menu.icon || 'close'" class="icon"/>
+      <template v-if="$slots.default">
+        <slot :data="menu" />
+      </template>
+      <ui-icon :type="menu.icon || 'close'" class="icon" v-else/>
       <p v-if="menu.text">{{ menu.text }}</p>
     </div>
   </ButtonPane>

+ 6 - 1
src/graphic/CanvasStyle/default.js

@@ -22,10 +22,15 @@ const Arrow = {
 }
 
 const Magnifier = {
-  strokeStyle: "#000",
+  strokeStyle: "#2F8FFF",
   lineWidth: 1,
   fillStyle: "rgba(0,0,0,0)",
   radius: 10,
+  target: {
+    radius: 100,
+    strokeStyle: "#2F8FFF",
+    lineWidth: 3
+  }
 }
 
 const CurveRoad = {

+ 6 - 5
src/graphic/CanvasStyle/focus.js

@@ -7,10 +7,7 @@ const Road = {
 };
 
 const Magnifier = {
-  strokeStyle: "#3290FF",
-  lineWidth: 1,
-  fillStyle: "rgba(0,0,0,0)",
-  radius: 10,
+  ...def.Magnifier
 }
 
 const CurveRoad = {
@@ -55,11 +52,15 @@ const CurveRoadEdge = {
   lineWidth: 2,
   strokeStyle: "#3290FF",
 };
-
+const Arrow = {
+  lineWidth: 2,
+  strokeStyle: "red",
+}
 export default {
   Road,
   Text,
   Point,
+  Arrow,
   RoadPoint,
   CurveRoadPoint,
   ControlPoint,

+ 5 - 0
src/graphic/CanvasStyle/select.js

@@ -6,6 +6,10 @@ const Road = {
   strokeStyle: "#3290FF",
 };
 
+const Arrow = {
+  lineWidth: 2,
+  strokeStyle: "red",
+}
 const CurveRoad = {
   ...def.CurveRoad,
   ...Road,
@@ -59,4 +63,5 @@ export default {
   CurveRoad,
   RoadEdge,
   CurveRoadEdge,
+  Arrow
 };

+ 81 - 16
src/graphic/Renderer/Draw.js

@@ -7,6 +7,7 @@ import { mathUtil } from "../Util/MathUtil.js";
 import ElementEvents from "../enum/ElementEvents.js";
 import { elementService } from "../Service/ElementService.js";
 
+const imgCache = {}
 const help = {
   getVectorStyle(vector, geoType = vector.geoType) {
     const geoId = vector?.vectorId;
@@ -23,7 +24,7 @@ const help = {
     return itemsEntry.reduce((prev, [item, attr]) => {
       if (
         item &&
-        item.type === VectorType[geoType] &&
+        // item.type === VectorType[geoType] &&
         geoId === item.vectorId
       ) {
         if (Style[attr] && Style[attr][geoType]) {
@@ -77,18 +78,36 @@ const help = {
       ctx.stroke();
     }
   },
+  getReal(data) {
+    return (data * coordinate.ratio * coordinate.zoom) / coordinate.defaultZoom;
+  },
+  getImage(src) {
+    if (imgCache[src]) {
+      return imgCache[src];
+    }
+    const img = new Image()
+    img.src = src
+    return imgCache[src] = new Promise(resolve => {
+      img.onload = () => {
+        resolve(img)
+      }
+    })
+  }
 };
 
 export default class Draw {
   constructor() {
+    this.canvas = null
     this.context = null;
   }
 
   initContext(canvas) {
     if (canvas) {
+      this.canvas = canvas
       this.context = canvas.getContext("2d");
     } else {
       this.context = null;
+      this.canvas = null
     }
   }
 
@@ -102,9 +121,23 @@ export default class Draw {
   }
 
   drawBackGroundImg(vector) {
-    this.context.save();
+    help.getImage(vector.src)
+      .then(img => {
+        const width = help.getReal(img.width)
+        const height = help.getReal(img.height)
+        const center = coordinate.getScreenXY({x: 0, y: 0});
+        this.context.save();
+        this.context.drawImage(
+          img,
+          center.x - width / 2,
+          center.y - height / 2,
+          width,
+          height
+        )
+        this.context.restore();
+      })
+
 
-    this.context.restore();
   }
 
   drawGrid(startX, startY, w, h, step1, step2) {
@@ -140,7 +173,6 @@ export default class Draw {
   }
 
   drawRoad(vector, isTemp) {
-    console.log(vector);
     if (!isTemp && vector.display && vector.way !== "oneWay") {
       const ctx = this.context;
       const draw = (midDivide) => {
@@ -357,10 +389,10 @@ export default class Draw {
   }
 
   drawArrow(vector) {
-    let start = dataService.getPoint(vector.startId);
-    start = coordinate.getScreenXY(start);
-    let end = dataService.getPoint(vector.endId);
-    end = coordinate.getScreenXY(end);
+    const startReal = dataService.getPoint(vector.startId);
+    const start = coordinate.getScreenXY(startReal);
+    const endReal = dataService.getPoint(vector.endId);
+    const end = coordinate.getScreenXY(endReal);
     const ctx = this.context
 
     const ange = 30
@@ -372,7 +404,7 @@ export default class Draw {
     let yD = end.y - L * Math.sin(a - ange * Math.PI/180);
     ctx.save()
 
-    help.setVectorStyle(
+    const style = help.setVectorStyle(
       this.context,
       vector,
       'Arrow'
@@ -388,20 +420,29 @@ export default class Draw {
     ctx.lineTo(xD, yD);
     ctx.stroke();
     ctx.restore();
+
+    if ([Style.Focus.Arrow, Style.Select.Arrow].includes(style)) {
+      this.drawPoint(startReal)
+      this.drawPoint(endReal)
+    }
   }
 
   drawMagnifier(vector) {
     const ctx = this.context
     this.drawPoint({
+      ...vector,
       ...vector.position,
       radius: Style.Magnifier.radius,
     })
     const pt = coordinate.getScreenXY(vector.position)
     const target = coordinate.getScreenXY(vector.popPosition)
     const style = help.setVectorStyle(ctx, vector)
-    const offset = style.radius / 2
+    const radius =
+      ((vector.radius || style.radius) * coordinate.ratio * coordinate.zoom) /
+      coordinate.defaultZoom;
+    const offset = radius / 2
     const targetPts = style === Style.Focus.Magnifier ? [
-      mathUtil.translate(pt, target, pt, style.radius),
+      mathUtil.translate(pt, target, pt, radius),
       target
     ] : null
 
@@ -414,11 +455,36 @@ export default class Draw {
     ctx.moveTo(pt.x, pt.y - offset)
     ctx.lineTo(pt.x, pt.y + offset)
     ctx.stroke();
+
     if (targetPts) {
       ctx.beginPath()
       ctx.moveTo(targetPts[0].x, targetPts[0].y)
       ctx.lineTo(targetPts[1].x, targetPts[1].y)
       ctx.stroke();
+
+      imgCache[Object.keys(imgCache)[0]].then(img => {
+        const size = help.getReal(style.target.radius);
+        ctx.save();
+        ctx.beginPath()
+        ctx.arc(target.x, target.y, size, 0, 2*Math.PI);
+        ctx.clip()
+        ctx.drawImage(
+          img,
+          vector.position.x - style.target.radius,
+          vector.position.y - style.target.radius,
+          style.target.radius*2,
+          style.target.radius*2,
+          target.x - size,
+          target.y - size,
+          size*2,
+          size*2
+        );
+        ctx.strokeStyle = style.target.strokeStyle
+        ctx.lineWidth = style.target.lineWidth
+        ctx.stroke()
+        ctx.restore();
+      })
+
     }
     ctx.restore();
   }
@@ -429,6 +495,8 @@ export default class Draw {
       geoType: 'Circle',
       ...element.center
     });
+
+    console.log(element)
   }
 
   drawPoint(vector) {
@@ -438,9 +506,7 @@ export default class Draw {
     if (vector.color) {
       ctx.strokeStyle = vector.color
     }
-    const radius =
-      ((vector.radius || style.radius) * coordinate.ratio * coordinate.zoom) /
-      coordinate.defaultZoom;
+    const radius = help.getReal(vector.radius || style.radius)
     ctx.save();
     ctx.beginPath();
     ctx.arc(pt.x, pt.y, radius, 0, Math.PI * 2, true);
@@ -483,8 +549,7 @@ export default class Draw {
   }
 
   drawLine(vector) {
-    console.log(vector)
-    if (vector.category === "MeasureLine") {
+    if (vector.category === "ArrowLine") {
       return this.drawArrow(vector);
     }
     let start = dataService.getPoint(vector.startId);

+ 6 - 4
src/graphic/Renderer/Render.js

@@ -17,6 +17,12 @@ export default class Render {
       return;
     }
     switch (vector.geoType) {
+      case VectorType.BackgroundImg:
+        draw.drawBackGroundImg(vector);
+        break;
+      case VectorType.Img:
+        draw.drawBackGroundImg(vector);
+        break;
       case VectorType.Road:
         draw.drawRoad(vector, false);
         return;
@@ -38,9 +44,6 @@ export default class Render {
       case VectorType.Circle:
         draw.drawCircle(vector);
         break;
-      case VectorType.BackgroundImg:
-        draw.drawBackGroundImg();
-        break;
       case VectorType.Magnifier:
         draw.drawMagnifier(vector);
         break;
@@ -49,7 +52,6 @@ export default class Render {
 
   //绘制交互的元素
   drawElement(vector) {
-    console.log(vector);
     if (draw.context == null) {
       return;
     }

+ 1 - 0
src/graphic/enum/VectorType.js

@@ -7,6 +7,7 @@ const VectorType = {
   CurvePoint: "CurvePoint",
   CurveLine: "CurveLine",
   MeasureArrow: "MeasureArrow",
+  ArrowLine: "ArrowLine",
   Road: "Road",
   RoadEdge: "RoadEdge",
   RoadPoint: "RoadPoint",

+ 4 - 3
src/hook/useGraphic.ts

@@ -4,13 +4,14 @@ import VectorType from '@/graphic/enum/VectorType'
 import UIType from '@/graphic/enum/UIEvents'
 import type {RoadPhoto} from '@/store/roadPhotos'
 import type {AccidentPhoto} from '@/store/accidentPhotos'
+import {getStaticFile} from "@/dbo/main";
 
 export type UITypeT = typeof UIType
 export type VectorTypeT = typeof VectorType
 
 const newsletter = ref<{
   selectUI?: UITypeT[keyof UITypeT]
-  focusVector?: { type: VectorTypeT[keyof VectorTypeT], vectorId: string }
+  focusVector?: { type: VectorTypeT[keyof VectorTypeT], category?: VectorTypeT[keyof VectorTypeT], vectorId: string }
 }>({ selectUI: null, focusVector: null });
 
 export const graphicState = ref({
@@ -29,7 +30,7 @@ export const setCanvas = async (canvas: HTMLCanvasElement, data: Ref<AccidentPho
           oldId && drawRef.value.load.clear()
           drawRef.value.load.load(data.value.data, {
             ...data.value.sceneData,
-            backImage: data.value.photoUrl
+            backImage: getStaticFile(data.value.photoUrl)
           })
         } else {
           drawRef.value.load.clear()
@@ -44,7 +45,7 @@ export const drawRef = ref<ReturnType<typeof structureDraw>>();
 
 export const uiType = reactive({
   current: computed(() => newsletter.value.selectUI),
-  change: (type: typeof UIType) => {
+  change: (type: UITypeT[keyof UITypeT]) => {
     drawRef.value.uiControl.selectUI = type
   }
 })

+ 43 - 0
src/hook/useHistory.ts

@@ -0,0 +1,43 @@
+import { History } from 'stateshot'
+import {reactive, Ref, ref} from "vue";
+
+export const useHistory = <T>(initialState: T) => {
+  const history = new History<T>({ initialState })
+  const value = ref({...initialState}) as Ref<T>
+  const state = ref({
+    hasUndo: false,
+    hasRedo: false
+  })
+  const setHistoryState = () => {
+    state.value.hasUndo = history.hasUndo
+    state.value.hasRedo = history.hasRedo
+  }
+
+  const undo = () => {
+    history.undo()
+    setHistoryState()
+    value.value = history.get()
+  }
+
+  const redo = () => {
+    history.redo()
+    setHistoryState()
+    value.value = history.get()
+  }
+
+  const push = () => {
+    const pushStateStr = JSON.stringify(value.value)
+    if (pushStateStr !== JSON.stringify(history.get())) {
+      history.pushSync(JSON.parse(pushStateStr))
+      setHistoryState()
+    }
+  }
+
+  return reactive({
+    push,
+    redo,
+    undo,
+    value,
+    state
+  })
+}

+ 8 - 2
src/router/constant.ts

@@ -9,7 +9,10 @@ export const readyRouteName = {
   measure: "measure",
   graphic: "graphic",
   scene: "scene",
-  photos: "photos"
+  photos: "photos",
+  accidents: "accidents",
+  roads: "roads",
+  tabulation: "tabulation"
 } as const;
 
 export const writeRouteName = {
@@ -41,7 +44,10 @@ export const readyRouteMeta: RouteMetaRaw = {
   [readyRouteName.measure]: { title: ui18n.t("measure.name") },
   [readyRouteName.graphic]: { title: "绘图" },
   [readyRouteName.scene]: { title: "绘图" },
-  [readyRouteName.photos]: {title: "相册"}
+  [readyRouteName.photos]: {title: "相册"},
+  [readyRouteName.accidents]: {title: "事故照片"},
+  [readyRouteName.roads]: {title: "道路照片"},
+  [readyRouteName.tabulation]: {title: "制表"}
 };
 
 export const writeRouteMeta: RouteMetaRaw<typeof modeFlags.LOGIN> = {

+ 18 - 0
src/router/info.ts

@@ -43,6 +43,24 @@ export const writeRoutesRaw: RoutesRaw<typeof modeFlags.LOGIN> = [
     meta: readyRouteMeta.photos,
     component: () => import("@/views/photos/index.vue"),
   },
+  {
+    path: "/accidents",
+    name: readyRouteName.accidents,
+    meta: readyRouteMeta.accidents,
+    component: () => import("@/views/accidents/index.vue"),
+  },
+  {
+    path: "/roads",
+    name: readyRouteName.roads,
+    meta: readyRouteMeta.roads,
+    component: () => import("@/views/roads/index.vue"),
+  },
+  {
+    path: "/tabulation/:id",
+    name: readyRouteName.tabulation,
+    meta: readyRouteMeta.tabulation,
+    component: () => import("@/views/roads/tabulation.vue"),
+  },
 ];
 
 export type RoutesRef<T extends ModeFlag = any> = ComputedRef<{

+ 11 - 3
src/store/accidentPhotos.ts

@@ -1,13 +1,14 @@
-import {ref} from "vue";
+import {ref, watch, watchEffect} from "vue";
 import type { PhotoRaw } from './photos'
-import {Pos} from "@/sdk";
 
 export type AccidentPhoto = {
   id: string
   photoUrl: string
+  title: string
   url: string
   time: number,
   data: any,
+  type?: string
   sceneData: {
     measures: PhotoRaw['measures'],
     fixPoints: PhotoRaw['fixPoints'],
@@ -16,4 +17,11 @@ export type AccidentPhoto = {
   }
 }
 
-export const accidentPhotos = ref<AccidentPhoto[]>([])
+export const types = [
+  "概览照片",
+  "中心照片",
+  "细目照片",
+  "方位照片"
+]
+
+export const accidentPhotos = ref<AccidentPhoto[]>([])

+ 25 - 0
src/store/roadPhotos.ts

@@ -1,11 +1,24 @@
 import {ref} from "vue";
 import {PhotoRaw} from "@/store/photos";
 
+export type RoadPhotoTable = {
+  arrivalTime: string,
+  weather: string,
+  conditions: string,
+  location: string,
+  illustrate: string,
+  other: string,
+  url?: string,
+  compassAngle: number
+
+}
 export type RoadPhoto = {
   id: string
   photoUrl: string
   url: string
+  title: string
   time: number,
+  table?: RoadPhotoTable,
   data: any
   sceneData: {
     measures: PhotoRaw['measures'],
@@ -15,4 +28,16 @@ export type RoadPhoto = {
   }
 }
 
+export const getDefaultTable = (road): RoadPhotoTable => {
+  return road.table ? { ...road.table } : {
+    arrivalTime: "2020-10-12",
+    weather: "晴天",
+    conditions: "普通",
+    location: "",
+    compassAngle: 0,
+    illustrate: "说明:绘图比例为1:215,单位为米。车辆甲为小轿车,无车号。车辆乙为小轿车,无车号。选取道路边缘线为基准线,井盖为基准点。",
+    other: "",
+  }
+}
+
 export const roadPhotos = ref<RoadPhoto[]>([])

+ 119 - 0
src/views/accidents/index.vue

@@ -0,0 +1,119 @@
+<template>
+  <MainPanel>
+    <template v-slot:header>
+      标注照片({{accidentPhotos.length}})
+    </template>
+
+    <div class="photos-layout">
+      <div class="type-photos" v-for="typePhoto in typePhotos" :key="typePhoto.type">
+        <p>{{ typePhoto.type }}</p>
+        <div class="photos" >
+          <div v-for="photo in typePhoto.photos" :key="photo.id" class="photo" @click="active = photo">
+            <p>{{ photo.title || '默认标题' }}</p>
+            <img :src="getStaticFile(photo.photoUrl)"  />
+          </div>
+        </div>
+      </div>
+    </div>
+  </MainPanel>
+
+  <FillSlide :data="sortPhotos" v-model:active="active" @quit="active = null" v-if="active">
+    <template v-slot:header>
+      <div class="btns">
+        <ui-button width="100px" @click="gotoDraw">修改</ui-button>
+      </div>
+    </template>
+    <template v-slot:foot>
+      <div class="foot">
+        <ui-icon
+            type="close"
+            @click="delPhoto"
+            ctrl
+        />
+      </div>
+    </template>
+  </FillSlide>
+</template>
+
+<script setup lang="ts">
+import MainPanel from '@/components/main-panel/index.vue'
+import FillSlide from '@/components/fill-slide/index.vue'
+import {types, accidentPhotos, AccidentPhoto} from '@/store/accidentPhotos'
+import {getStaticFile} from "@/dbo/main";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import {router, writeRouteName} from '@/router'
+import {computed, ref} from "vue";
+import {Mode} from '@/views/graphic/menus'
+import UiButton from "@/components/base/components/button/index.vue";
+
+const sortPhotos = computed(() => {
+  const photos = [...accidentPhotos.value]
+  return photos.sort((a, b) =>
+    types.indexOf(a.type) - types.indexOf(b.type)
+  )
+})
+const typePhotos = computed(() =>
+  types
+    .map(type => ({
+      type,
+      photos: sortPhotos.value.filter(data => data.type === type)
+    }))
+    .filter(data => data.photos.length)
+)
+const active = ref<AccidentPhoto>()
+const delPhoto = () => {
+  const index = accidentPhotos.value.indexOf(active.value)
+  if (~index) {
+    accidentPhotos.value.splice(index, 1)
+  }
+}
+
+const gotoDraw = () => {
+  router.push({
+    name: writeRouteName.graphic,
+    params: {mode: Mode.Photo, id: active.value.id, action: 'update'}
+  })
+}
+
+</script>
+
+<style scoped lang="scss">
+.photos-layout {
+  position: absolute;
+  top: calc(var(--header-top) + var(--editor-head-height));
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow-y: auto;
+}
+.type-photos p {
+  color: #000;
+}
+
+.photos {
+  display: grid;
+  grid-gap: 10px;
+  padding: 10px;
+  //grid-template-rows: auto;
+  grid-template-columns: repeat(5, 1fr);
+}
+.photo img {
+  width: 100%;
+}
+
+
+.btns {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  justify-content: center;
+}
+
+.foot {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  height: 100%;
+}
+</style>

+ 1 - 0
src/views/graphic/container.vue

@@ -23,6 +23,7 @@ const setCanvasSize = () => {
 
 
 onMounted(() => {
+  console.log(useData())
   setCanvasSize();
   setCanvas(drawCanvasRef.value, useData());
 });

+ 5 - 3
src/views/graphic/data.ts

@@ -1,6 +1,6 @@
 import {computed, ref, watch} from "vue";
 import {Mode} from "@/views/graphic/menus";
-import {accidentPhotos, AccidentPhoto} from "@/store/accidentPhotos";
+import {accidentPhotos, types, AccidentPhoto} from "@/store/accidentPhotos";
 import {roadPhotos, RoadPhoto} from "@/store/roadPhotos";
 import {router} from '@/router'
 import {getId} from "@/utils";
@@ -17,6 +17,8 @@ export const useData = () => {
         const photo = photos.value.find(data => data.id === params.id)
         data.value = {
           data: null,
+          title: "",
+          type: types[0],
           sceneData: {
             measures: photo.measures,
             basePoints: photo.basePoints,
@@ -31,9 +33,9 @@ export const useData = () => {
       } else {
         const mode = Number(params.mode) as Mode
         if (mode === Mode.Photo) {
-          return accidentPhotos.value.find(data => data.id === params.id)
+          return data.value = accidentPhotos.value.find(data => data.id === params.id)
         } else {
-          return roadPhotos.value.find(data => data.id === params.id)
+          return data.value = roadPhotos.value.find(data => data.id === params.id)
         }
       }
     },

+ 75 - 0
src/views/graphic/geos/arrow.vue

@@ -0,0 +1,75 @@
+<template>
+  <GeoTeleport :menus="menus" class="geo-teleport-use">
+    <template v-slot="{ data }">
+      <template v-if="data.key === 'color'">
+        <ui-input type="color" class="geo-input" v-model="color" />
+        <span class="color" :style="{backgroundColor: color}"></span>
+      </template>
+      <ui-icon type="del" class="icon" v-if="data.key === 'del'" />
+      <ui-icon type="del" class="icon" v-if="data.key === 'copy'" />
+    </template>
+
+  </GeoTeleport>
+</template>
+
+<script setup lang="ts">
+import GeoTeleport from "@/views/graphic/geos/geo-teleport.vue";
+import UiInput from "@/components/base/components/input/index.vue";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import { uiType, UIType } from '@/hook/useGraphic'
+import {ref} from "vue";
+
+const props = defineProps<{geo: any}>()
+
+const color = ref("#000000")
+const menus = [
+  {
+    key: 'color',
+    text: "颜色"
+  },
+  {
+    key: 'copy',
+    text: "复制",
+    onClick: () => {
+      uiType.change(UIType.Copy)
+    }
+  },
+  {
+    key: 'del',
+    text: "删除",
+    onClick: () => {
+      uiType.change(UIType.Delete)
+    }
+  }
+]
+
+</script>
+
+<style scoped lang="scss">
+.color {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+}
+
+.icon {
+  font-size: 16px;
+}
+
+.geo-input {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1;
+  opacity: 0;
+}
+</style>
+
+<style lang="scss">
+.select-floating.select-float.dire-top {
+  margin-top: -10px;
+}
+</style>

+ 67 - 0
src/views/graphic/geos/circle.vue

@@ -0,0 +1,67 @@
+<template>
+  <GeoTeleport :menus="menus" class="geo-teleport-use">
+    <template v-slot="{ data }">
+      <template v-if="data.key === 'color'">
+        <ui-input type="color" class="geo-input" v-model="color" />
+        <span class="color" :style="{backgroundColor: color}"></span>
+      </template>
+      <ui-icon type="del" class="icon" v-if="data.key === 'del'" />
+    </template>
+
+  </GeoTeleport>
+</template>
+
+<script setup lang="ts">
+import GeoTeleport from "@/views/graphic/geos/geo-teleport.vue";
+import UiInput from "@/components/base/components/input/index.vue";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import { uiType, UIType } from '@/hook/useGraphic'
+import {ref} from "vue";
+
+const props = defineProps<{geo: any}>()
+
+const color = ref("#000000")
+const menus = [
+  {
+    key: 'color',
+    text: "颜色"
+  },
+  {
+    key: 'del',
+    text: "删除",
+    onClick: () => {
+      uiType.change(UIType.Delete)
+    }
+  }
+]
+
+</script>
+
+<style scoped lang="scss">
+.color {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+}
+
+.icon {
+  font-size: 16px;
+}
+
+.geo-input {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1;
+  opacity: 0;
+}
+</style>
+
+<style lang="scss">
+.select-floating.select-float.dire-top {
+  margin-top: -10px;
+}
+</style>

+ 103 - 0
src/views/graphic/geos/geo-teleport.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="geo-teleport">
+    <ActionMenus
+        class="menus "
+        :menus="menus"
+        dire="row"
+    >
+      <template v-slot="{data}">
+        <div class="menu-layout">
+          <slot :data="data" />
+        </div>
+      </template>
+    </ActionMenus>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ActionMenus from '@/components/group-button/index.vue'
+import {onMounted} from "vue";
+
+type Menu =  {
+  key: any,
+  text?: string,
+  icon?: string,
+  onClick?: (menu: Menu) => void
+}
+
+defineProps<{ menus: Menu[] }>()
+</script>
+
+<style scoped lang="scss">
+.geo-teleport {
+  position: absolute;
+  bottom: var(--boundMargin);
+  left: 0;
+  right: 0;
+  display: flex;
+  justify-content: center;
+}
+
+.menus {
+  position: static;
+}
+.menu {
+
+  min-width: 56px;
+  display: flex;
+  flex-direction: column;
+  cursor: pointer;
+  height: 100%;
+  text-align: center;
+  transition: color .3s ease;
+  color: #fff;
+  border-radius: 4px;
+
+  &.active,
+  &:hover {
+    color: var(--colors-primary-base);
+  }
+
+  &.active {
+    background: rgba(255, 255, 255, 0.1);;
+  }
+
+  .icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 20px;
+    flex: 1;
+  }
+
+  p {
+    line-height: 17px;
+    font-size: 12px;
+    white-space:nowrap;
+  }
+}
+
+
+.menu-layout {
+  height: 22px;
+  margin-bottom: 3px;
+  margin-top: 7px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow-y: hidden;
+}
+
+</style>
+
+<style lang="scss">
+.geo-teleport .menu {
+  position: relative;
+
+  .input,
+  input {
+    width: 100% !important;
+    height: 100% !important;
+  }
+}
+</style>

+ 12 - 0
src/views/graphic/geos/index.ts

@@ -0,0 +1,12 @@
+import { VectorType } from '@/hook/useGraphic'
+import Arrow from './arrow.vue'
+import Text from './text.vue'
+import Circle from './circle.vue'
+import magnifier from './magnifier.vue'
+
+export default {
+  [VectorType.ArrowLine]: Arrow,
+  [VectorType.Text]: Text,
+  [VectorType.Circle]: Circle,
+  [VectorType.Magnifier]: magnifier
+}

+ 70 - 0
src/views/graphic/geos/magnifier.vue

@@ -0,0 +1,70 @@
+<template>
+  <GeoTeleport :menus="menus" class="geo-teleport-use">
+    <template v-slot="{ data }">
+      <template v-if="data.key === 'file'">
+        <ui-input type="file" class="geo-input" v-model="file" />
+        <ui-icon type="del" class="icon" />
+      </template>
+      <ui-icon type="del" class="icon" v-if="data.key === 'del'" />
+    </template>
+
+  </GeoTeleport>
+</template>
+
+<script setup lang="ts">
+import GeoTeleport from "@/views/graphic/geos/geo-teleport.vue";
+import UiInput from "@/components/base/components/input/index.vue";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import { uiType, UIType } from '@/hook/useGraphic'
+import {ref, watchEffect} from "vue";
+
+const props = defineProps<{geo: any}>()
+const file = ref<Blob>()
+const menus = [
+  {
+    key: 'file',
+    text: "拍照"
+  },
+  {
+    key: 'del',
+    text: "删除",
+    onClick: () => {
+      uiType.change(UIType.Delete)
+    }
+  }
+]
+
+watchEffect(() => {
+  console.log(file)
+})
+
+</script>
+
+<style scoped lang="scss">
+.color {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+}
+
+.icon {
+  font-size: 16px;
+}
+
+.geo-input {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1;
+  opacity: 0;
+}
+</style>
+
+<style lang="scss">
+.select-floating.select-float.dire-top {
+  margin-top: -10px;
+}
+</style>

+ 96 - 0
src/views/graphic/geos/text.vue

@@ -0,0 +1,96 @@
+<template>
+  <GeoTeleport :menus="menus" class="geo-teleport-use">
+    <template v-slot="{ data }">
+      <template v-if="data.key === 'color'">
+        <ui-input type="color" class="geo-input" v-model="color" />
+        <span class="color" :style="{backgroundColor: color}"></span>
+      </template>
+      <template v-if="data.key === 'fontSize'">
+        <ui-input
+            type="select"
+            :options="sizeOption"
+            class="geo-input"
+            dire="top"
+            floatingClass="select-floating"
+            v-model="size"
+        />
+        <span class="font-size">{{ size }}</span>
+      </template>
+      <ui-icon type="del" class="del-icon" v-if="data.key === 'del'" />
+
+    </template>
+
+  </GeoTeleport>
+</template>
+
+<script setup lang="ts">
+import GeoTeleport from "@/views/graphic/geos/geo-teleport.vue";
+import UiInput from "@/components/base/components/input/index.vue";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import { uiType, UIType } from '@/hook/useGraphic'
+import {ref} from "vue";
+
+const props = defineProps<{geo: any}>()
+
+const sizeOption = [];
+for (let i = 10; i < 30; i++) {
+  sizeOption.push({label: i, value: i});
+}
+
+
+const color = ref("#000000")
+const size = ref(18)
+const menus = [
+  {
+    key: 'color',
+    text: "颜色"
+  },
+  {
+    key: 'fontSize',
+    text: "文字大小"
+  },
+  {
+    key: 'del',
+    text: "删除",
+    onClick: () => {
+      uiType.change(UIType.Delete)
+    }
+  }
+]
+
+</script>
+
+<style scoped lang="scss">
+.color {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+}
+
+.font-size {
+  font-size: 16px;
+  font-weight: bold;
+  color: #fff;
+}
+
+.del-icon {
+  font-size: 16px;
+}
+
+.geo-input {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1;
+  opacity: 0;
+}
+</style>
+
+<style lang="scss">
+.select-floating.select-float.dire-top {
+  margin-top: -10px;
+}
+</style>

+ 77 - 35
src/views/graphic/header.vue

@@ -1,8 +1,9 @@
 <template>
-  <div class="graphic-header">
+  <div class="graphic-header" v-if="data">
+
     <div class="title">
       <ui-icon type="close" @click="router.back" />
-      <p>{{ mode === Mode.Road ? '现场绘图' : '事故照片' }}</p>
+      <p>{{ isRoad ? '现场绘图' : '事故照片' }}</p>
     </div>
 
     <div class="actions">
@@ -10,7 +11,7 @@
           v-for="menu in menus"
           :key="menu.text"
           class="action fun-ctrl"
-          :class="{disabled: !menu.enable}"
+          :class="{disabled: menu.disable}"
           @click="menu.onClick"
       >
         <ui-icon type="close" />
@@ -18,8 +19,18 @@
       </div>
     </div>
 
-    <div class="table" v-if="mode === Mode.Road">
-      <ui-button width="100px" type="primary">制表</ui-button>
+    <div class="table">
+      <ui-input
+          width="120px"
+          height="32px"
+          type="select"
+          :options="options"
+          v-model="(data as AccidentPhoto).type"
+          v-if="options"
+      />
+      <ui-button width="100px" type="primary" class="save" @click="saveHandler">
+        {{ isRoad ? '制表' : '保存' }}
+      </ui-button>
     </div>
   </div>
 </template>
@@ -27,38 +38,66 @@
 import UiIcon from "@/components/base/components/icon/index.vue";
 import UiButton from "@/components/base/components/button/index.vue";
 import {Mode} from './menus'
-import {drawRef, graphicState, uiType, UIType,} from '@/hook/useGraphic'
-import {computed} from "vue";
-import {router} from '@/router'
-
-const menus = computed(() => [
-  {
-    key: UIType.GoBack,
-    text: "回退",
-    enable: graphicState.value.canRevoke,
-    onClick: () => drawRef.value.uiControl.menu_revoke()
-  },
-  {
-    key: UIType.GoAhead,
-    text: "前进",
-    enable: graphicState.value.canRecovery,
-    onClick: () => drawRef.value.uiControl.menu_recovery()
-  },
-  {
-    key: UIType.Clear,
-    text: "清除",
-    enable: true,
-    onClick: () => drawRef.value.uiControl.menu_clear()
-  },
-  {
-    key: UIType.BackImageChange,
-    text: `底图${graphicState.value.showBackImage ? '关' : '开'}`,
-    enable: true,
-    onClick: () => drawRef.value.uiControl.menu_backgroundImg(!graphicState.value.showBackImage)
-  }
-])
+import {drawRef, graphicState} from '@/hook/useGraphic'
+import {computed, watchEffect} from "vue";
+import {router, writeRouteName} from '@/router'
+import {types, AccidentPhoto, accidentPhotos} from '@/store/accidentPhotos'
+import {useData} from './data'
+import UiInput from "@/components/base/components/input/index.vue";
+import {roadPhotos} from "@/store/roadPhotos";
 
+const data = useData()
 const mode = computed(() => Number(router.currentRoute.value.params.mode) as Mode)
+const isRoad = computed(() => mode.value === Mode.Road)
+const options = computed(() =>
+  !isRoad.value ? types.map(t => ({ label: t, value: t })) : null
+)
+const menus = computed<{disable?: boolean, text: string, onClick: () => void}[]>(() => {
+  const menus = [
+    {
+      text: "回退",
+      disable: !graphicState.value.canRevoke,
+      onClick: () => drawRef.value.uiControl.menu_revoke()
+    },
+    {
+      text: "前进",
+      disable: !graphicState.value.canRecovery,
+      onClick: () => drawRef.value.uiControl.menu_recovery()
+    },
+    {
+      text: "清除",
+      onClick: () => drawRef.value.uiControl.menu_clear()
+    }
+  ]
+
+  if (isRoad.value) {
+    menus.push({
+      text: `底图${graphicState.value.showBackImage ? '关' : '开'}`,
+      onClick: () => drawRef.value.uiControl.menu_backgroundImg(!graphicState.value.showBackImage)
+    }, {
+      text: `保存`,
+      onClick: () => drawRef.value.uiControl.menu_backgroundImg(!graphicState.value.showBackImage)
+    })
+  }
+  return menus
+})
+
+const saveHandler = () => {
+  const newData = {
+    ...data.value,
+    data: drawRef.value.load.save()
+  }
+  const origin = isRoad.value ? roadPhotos.value : accidentPhotos.value
+  const index = origin.indexOf(data.value)
+  if (~index) {
+    origin[index] = newData
+  } else {
+    origin.push(newData)
+  }
+  router.replace({name: isRoad.value ? writeRouteName.roads : writeRouteName.accidents})
+}
+
+
 </script>
 
 <style scoped lang="scss">
@@ -107,6 +146,9 @@ const mode = computed(() => Number(router.currentRoute.value.params.mode) as Mod
   right: 0;
 }
 
+.save {
+  margin-left: 24px;
+}
 
 
 </style>

+ 6 - 3
src/views/graphic/index.vue

@@ -18,6 +18,7 @@
       />
     </GraphicAction>
     <VectorMenus :menus="focusMenus" v-if="focusMenus" />
+    <Component :is="geoComponent as any" v-if="geoComponent" :geo="currentVector"/>
   </MainPanel>
 </template>
 
@@ -35,6 +36,7 @@ import {computed} from "vue";
 import {customMap} from '@/hook'
 import {focusMenuRaw, generateMixMenus, mainMenusRaw, photoMenusRaw, Mode, UITypeExtend} from './menus'
 import {currentVector} from "@/hook/useGraphic";
+import geos from "./geos/index";
 
 const menusRaws = computed(() => {
   const mode = Number(router.currentRoute.value.params.mode) as Mode
@@ -53,8 +55,9 @@ const store = computed(() => generateMixMenus(
 ))
 
 
-const focusMenus = computed(() => {
-  return focusMenuRaw[currentVector.value?.type]
+const focusMenus = computed(() => focusMenuRaw[currentVector.value?.type])
+const geoComponent = computed(() => {
+  return geos[currentVector.value?.type] || geos[currentVector.value?.category]
 })
 const isFull = computed(() => customMap.sysView === 'full' )
 </script>
@@ -62,7 +65,7 @@ const isFull = computed(() => customMap.sysView === 'full' )
 <style lang="scss" scoped>
 .full-action {
   right: 24px;
-  bottom: 26px;
+  bottom: var(--boundMargin);
   width: 64px;
   font-size: 22px;
   justify-content: center;

+ 2 - 0
src/views/photos/index.vue

@@ -7,6 +7,7 @@
     <div class="photos-layout">
       <div class="photos">
         <div v-for="photo in sortPhotos" :key="photo.id" class="photo" @click="active = photo">
+
           <img :src="getStaticFile(photo.url)"  />
         </div>
       </div>
@@ -76,6 +77,7 @@ const gotoDraw = (mode: Mode) => {
   left: 0;
   right: 0;
   bottom: 0;
+  overflow-y: auto;
 }
 .photos {
   display: grid;

+ 98 - 0
src/views/roads/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <MainPanel>
+    <template v-slot:header>
+      现场图管理({{sortPhotos.length}})
+    </template>
+
+    <div class="photos-layout">
+      <div class="photos">
+        <div v-for="photo in sortPhotos" :key="photo.id" class="photo" @click="active = photo">
+          <img :src="getStaticFile(photo.photoUrl)"  />
+          <p>{{ photo.title || '默认标题' }}</p>
+        </div>
+      </div>
+    </div>
+  </MainPanel>
+
+  <FillSlide :data="sortPhotos" v-model:active="active" @quit="active = null" v-if="active">
+    <template v-slot:header>
+      <div class="btns">
+        <ui-button width="100px" @click="gotoDraw()">修改</ui-button>
+      </div>
+    </template>
+    <template v-slot:foot>
+      <div class="foot">
+        <ui-icon type="close" @click="delPhoto" ctrl />
+      </div>
+    </template>
+  </FillSlide>
+</template>
+
+<script setup lang="ts">
+import MainPanel from '@/components/main-panel/index.vue'
+import FillSlide from '@/components/fill-slide/index.vue'
+import {roadPhotos, RoadPhoto} from '@/store/roadPhotos'
+import {getStaticFile} from "@/dbo/main";
+import UiIcon from "@/components/base/components/icon/index.vue";
+import {router, writeRouteName} from '@/router'
+import {computed, ref} from "vue";
+import {Mode} from '@/views/graphic/menus'
+import UiButton from "@/components/base/components/button/index.vue";
+
+const sortPhotos = computed(() => roadPhotos.value.reverse())
+const active = ref<RoadPhoto>()
+const delPhoto = () => {
+  const index = roadPhotos.value.indexOf(active.value)
+  if (~index) {
+    roadPhotos.value.splice(index, 1)
+  }
+}
+
+const gotoDraw = () => {
+  router.push({
+    name: writeRouteName.graphic,
+    params: {mode: Mode.Road, id: active.value.id, action: 'update'}
+  })
+}
+
+</script>
+
+<style scoped lang="scss">
+.photos-layout {
+  position: absolute;
+  top: calc(var(--header-top) + var(--editor-head-height));
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+.photos {
+  display: grid;
+  grid-gap: 10px;
+  padding: 10px;
+  //grid-template-rows: auto;
+  grid-template-columns: repeat(5, 1fr);
+}
+.photo img {
+  width: 100%;
+}
+
+.photo p {
+  color: #000;
+}
+
+
+.btns {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  justify-content: center;
+}
+
+.foot {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  height: 100%;
+}
+</style>

+ 243 - 0
src/views/roads/tabulation.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="layout" v-if="roadPhoto">
+    <ui-button @click="downLayoutImage">下载</ui-button>
+    <ui-button @click="history.undo" type="primary" :class="{disabled: !history.state.hasUndo}">撤销</ui-button>
+    <ui-button @click="history.redo" type="primary" :class="{disabled: !history.state.hasRedo}">恢复</ui-button>
+    <ui-button type="primary" @click="roadPhoto.table = history.value">保存</ui-button>
+    <div class="content" ref="layoutRef">
+      <h2>{{ roadPhoto.title || "默认名称" }}</h2>
+      <table>
+        <tr>
+          <td class="label" width="150">到达事故现场时间</td>
+          <td class="value">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.arrivalTime"
+                @blur="history.push"
+            />
+          </td>
+          <td class="label" width="100">天气</td>
+          <td class="value" width="80">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.weather"
+                @blur="history.push"
+            />
+          </td>
+          <td class="label" width="100">路面性质</td>
+          <td class="value" width="150">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.conditions"
+                @blur="history.push"
+            />
+          </td>
+        </tr>
+        <tr>
+          <td class="label">事故发生地点</td>
+          <td class="value" colspan="5">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.location"
+                @blur="history.push"
+            />
+          </td>
+        </tr>
+        <tr>
+          <td class="image" colspan="6">
+            <div>
+              <img :src="getStaticFile(roadPhoto.photoUrl)" @blur="history.push" class="photo" />
+              <img
+                  src="/static/compass.png"
+                  :style="{transform: `rotateZ(${history.value.compassAngle}deg)`}"
+                  class="compass"
+                  @mousedown="downHandler"
+              />
+            </div>
+          </td>
+        </tr>
+        <tr>
+          <td class="value" colspan="6">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.illustrate"
+                @blur="history.push"
+            />
+          </td>
+        </tr>
+        <tr>
+          <td class="value" colspan="6">
+            <ui-input
+                type="text"
+                @input="input"
+                v-model="history.value.other"
+                @blur="history.push"
+            />
+          </td>
+        </tr>
+      </table>
+      <div class="signatures">
+        <p class="signature">绘图员:</p>
+        <p class="signature">当事人签名:</p>
+        <p class="signature">勘察员:</p>
+        <p class="signature">见证人签名:</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { router, writeRouteName } from '@/router'
+import {computed, ref} from "vue";
+import { useHistory } from '@/hook/useHistory'
+import {roadPhotos, RoadPhoto, getDefaultTable} from "@/store/roadPhotos";
+import {getStaticFile} from "@/dbo/main";
+import html2canvas from 'html2canvas'
+import UiButton from "@/components/base/components/button/index.vue";
+import UiInput from "@/components/base/components/input/index.vue";
+import { mathUtil } from '@/graphic/Util/MathUtil'
+import {getPostionByTarget} from "@/components/base/utils";
+
+const roadPhoto = computed<RoadPhoto>(() => {
+  const route = router.currentRoute.value;
+  const params = route.params
+  let data
+  if (route.name !== writeRouteName.tabulation) {
+    return null
+  } else if (!params.id || !(data = roadPhotos.value.find(data => data.id === params.id))) {
+    router.back();
+    return null;
+  }
+  return data
+})
+const history = computed(
+  () => roadPhoto.value && useHistory(getDefaultTable(roadPhoto.value))
+)
+
+const input = () => {
+  history.value.state.hasRedo = false
+}
+
+const downHandler = (ev: MouseEvent) => {
+  const target = (ev.target as HTMLImageElement)
+  const page = getPostionByTarget(target, document.documentElement)
+  const start = { x: page.x + target.offsetWidth / 2, y: page.y }
+  const center = { x: page.x + target.offsetWidth / 2, y: page.y + target.offsetHeight / 2 }
+  let angle
+  const moveHandler = (ev: MouseEvent) => {
+    const move = {
+      x: ev.pageX,
+      y: ev.pageY
+    }
+    angle = mathUtil.Angle(center, start, move)
+    angle = move.x < start.x ? -angle : angle
+    target.style.transform = `rotateZ(${angle}deg)`
+    ev.stopPropagation();
+    ev.preventDefault();
+  }
+  const upHandler = (ev:MouseEvent) => {
+    document.documentElement.removeEventListener("mousemove", moveHandler)
+    document.documentElement.removeEventListener("mouseup", upHandler);
+    ev.stopPropagation();
+    ev.preventDefault();
+    history.value.value.compassAngle = angle
+    history.value.push()
+  }
+  document.documentElement.addEventListener("mousemove", moveHandler)
+  document.documentElement.addEventListener("mouseup", upHandler)
+
+  ev.stopPropagation();
+  ev.preventDefault();
+}
+
+const layoutRef = ref<HTMLDivElement>()
+const downLayoutImage = async () => {
+  const canvas = await html2canvas(layoutRef.value)
+  const blob = await new Promise<Blob>(resolve => canvas.toBlob(resolve, "image/jpeg", 0.95))
+  window.open(URL.createObjectURL(blob))
+}
+</script>
+
+<style lang="scss" scoped>
+.layout {
+  overflow-y: auto;
+  height: 100%;
+}
+
+.content {
+  box-sizing: content-box;
+  width: 980px;
+  height: 1300px;
+  padding: 20px;
+  padding-bottom: 60px;
+  margin: 0 auto;
+}
+
+.image {
+  position: relative;
+  div {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    .photo {
+      max-width: 100%;
+      max-height: 100%;
+    }
+    .compass {
+      position: absolute;
+      right: 20px;
+      top: 20px;
+      width: 60px;
+    }
+  }
+}
+
+.content table {
+  width: 980px;
+  height: 800px;
+  border: 2px solid #000;
+  border-collapse: collapse;
+
+  td:not(:last-child) {
+    border-right: 2px solid #000;
+  }
+  tr:not(:last-child) td {
+    border-bottom: 2px solid #000;
+  }
+
+  .value {
+    height: 43px;
+  }
+}
+
+.signatures {
+  display: flex;
+
+  .signature {
+    flex: 1;
+  }
+}
+</style>
+
+<style lang="scss">
+.value {
+  box-sizing: border-box;
+  padding: 8px 10px;
+  input,
+  .ui-input {
+    width: 100%;
+    height: 100%;
+    padding: 0 !important;
+    outline: none !important;
+    border: none !important;
+    color: #000 !important;
+  }
+}
+</style>

+ 14 - 1
yarn.lock

@@ -809,7 +809,7 @@ he@^1.2.0:
   resolved "http://192.168.0.47:4873/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
+html2canvas@^1.0.0-rc.5:
   version "1.4.1"
   resolved "http://192.168.0.47:4873/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
   integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
@@ -817,6 +817,14 @@ html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
     css-line-break "^2.1.0"
     text-segmentation "^1.0.3"
 
+html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 http-errors@2.0.0:
   version "2.0.0"
   resolved "http://192.168.0.47:4873/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
@@ -1218,6 +1226,11 @@ stackblur-canvas@^2.0.0:
   resolved "http://192.168.0.47:4873/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz#aa87bbed1560fdcd3138fff344fc6a1c413ebac4"
   integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==
 
+stateshot@^1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/stateshot/-/stateshot-1.3.5.tgz#9d91b1ae5057739abe5ecebcff551c30af95953a"
+  integrity sha512-A/I230vCzTBDHAc2wzCXrH3ofcNnMd9Cs/HhRrxjWJ1YI90cOklljX9XATTdU45T4W/c/+g+jBtS/oQLs+Wkdw==
+
 statuses@2.0.1:
   version "2.0.1"
   resolved "http://192.168.0.47:4873/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"