소스 검색

fix: 搭建创建方位图框架

bill 2 년 전
부모
커밋
d98dfbaa59

+ 8 - 0
craco.config.js

@@ -1,6 +1,14 @@
 const CracoLessPlugin = require('craco-less');
 
 module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.svg$/i,
+        use: 'raw-loader',
+      },
+    ],
+  },
   plugins: [
     {
       plugin: CracoLessPlugin,

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "mitt": "^3.0.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
+    "react-input-color": "^4.0.1",
     "react-redux": "^8.0.2",
     "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",

+ 194 - 0
pnpm-lock.yaml

@@ -29,6 +29,7 @@ specifiers:
   mitt: ^3.0.0
   react: ^18.2.0
   react-dom: ^18.2.0
+  react-input-color: ^4.0.1
   react-redux: ^8.0.2
   react-router-dom: ^6.3.0
   react-scripts: 5.0.1
@@ -65,6 +66,7 @@ dependencies:
   mitt: 3.0.0
   react: 18.2.0
   react-dom: 18.2.0_react@18.2.0
+  react-input-color: 4.0.1_biqbaboplfbrettd7655fr4n2y
   react-redux: 8.0.5_moha6x5fbqoiok2ot63p7hwafm
   react-router-dom: 6.4.3_biqbaboplfbrettd7655fr4n2y
   react-scripts: 5.0.1_vrhrsyowp6vs4524xuw7v67ume
@@ -1700,6 +1702,75 @@ packages:
     engines: {node: '>=10'}
     dev: false
 
+  /@emotion/cache/10.0.29:
+    resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==}
+    dependencies:
+      '@emotion/sheet': 0.9.4
+      '@emotion/stylis': 0.8.5
+      '@emotion/utils': 0.11.3
+      '@emotion/weak-memoize': 0.2.5
+    dev: false
+
+  /@emotion/core/10.3.1_react@18.2.0:
+    resolution: {integrity: sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==}
+    peerDependencies:
+      react: '>=16.3.0'
+    dependencies:
+      '@babel/runtime': 7.20.1
+      '@emotion/cache': 10.0.29
+      '@emotion/css': 10.0.27
+      '@emotion/serialize': 0.11.16
+      '@emotion/sheet': 0.9.4
+      '@emotion/utils': 0.11.3
+      react: 18.2.0
+    dev: false
+
+  /@emotion/css/10.0.27:
+    resolution: {integrity: sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==}
+    dependencies:
+      '@emotion/serialize': 0.11.16
+      '@emotion/utils': 0.11.3
+      babel-plugin-emotion: 10.2.2
+    dev: false
+
+  /@emotion/hash/0.8.0:
+    resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
+    dev: false
+
+  /@emotion/memoize/0.7.4:
+    resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+    dev: false
+
+  /@emotion/serialize/0.11.16:
+    resolution: {integrity: sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==}
+    dependencies:
+      '@emotion/hash': 0.8.0
+      '@emotion/memoize': 0.7.4
+      '@emotion/unitless': 0.7.5
+      '@emotion/utils': 0.11.3
+      csstype: 2.6.21
+    dev: false
+
+  /@emotion/sheet/0.9.4:
+    resolution: {integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==}
+    dev: false
+
+  /@emotion/stylis/0.8.5:
+    resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==}
+    dev: false
+
+  /@emotion/unitless/0.7.5:
+    resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
+    dev: false
+
+  /@emotion/utils/0.11.3:
+    resolution: {integrity: sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==}
+    dev: false
+
+  /@emotion/weak-memoize/0.2.5:
+    resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==}
+    dev: false
+
   /@eslint/eslintrc/1.3.3:
     resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2338,6 +2409,26 @@ packages:
       - supports-color
     dev: false
 
+  /@swiftcarrot/color-fns/3.2.0:
+    resolution: {integrity: sha512-6SCpc4LwmGGqWHpBY9WaBzJwPF4nfgvFfejOX7Ub0kTehJysFkLUAvGID8zEx39n0pGlfr9pTiQE/7/buC7X5w==}
+    dependencies:
+      '@babel/runtime': 7.20.1
+    dev: false
+
+  /@swiftcarrot/react-hooks/0.1.4:
+    resolution: {integrity: sha512-5sLrCIh5x47lIbiv9kZsSDWvGsMfDOvbq25VWKj0n3yLMo20aLlTXBZb7h42Fjcg6cLXqzbwbEQi650/UCJfjg==}
+    dependencies:
+      '@babel/runtime': 7.20.1
+    dev: false
+
+  /@swiftcarrot/utils/0.1.2:
+    resolution: {integrity: sha512-4wsnz0YPEebWeUNm2pvZeZVXQ7ukY5FOReMEKpZi0hs6bj19fWQi4m+BXUw6tzt6hPtHpdCy+izI56+4/7B/xA==}
+    dependencies:
+      '@babel/runtime': 7.20.1
+      lodash: 4.17.21
+      qss: 2.0.3
+    dev: false
+
   /@testing-library/dom/8.19.0:
     resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==}
     engines: {node: '>=12'}
@@ -2949,6 +3040,18 @@ packages:
       '@xtuc/long': 4.2.2
     dev: false
 
+  /@xkit/popover/0.1.24_react@18.2.0:
+    resolution: {integrity: sha512-oKB5EmGSCa4c4T6ByXzRpn7hetYXuGXGYlMIUcQBRBgnUD/d5IE81F5UHOgmsVNqsnTSt6nEXvnkhUERJu3F5Q==}
+    dependencies:
+      '@babel/runtime': 7.20.1
+      '@emotion/core': 10.3.1_react@18.2.0
+      '@swiftcarrot/react-hooks': 0.1.4
+      '@swiftcarrot/utils': 0.1.2
+      popper.js: 1.16.1
+    transitivePeerDependencies:
+      - react
+    dev: false
+
   /@xtuc/ieee754/1.2.0:
     resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
     dev: false
@@ -3390,6 +3493,21 @@ packages:
       webpack: 5.75.0
     dev: false
 
+  /babel-plugin-emotion/10.2.2:
+    resolution: {integrity: sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==}
+    dependencies:
+      '@babel/helper-module-imports': 7.18.6
+      '@emotion/hash': 0.8.0
+      '@emotion/memoize': 0.7.4
+      '@emotion/serialize': 0.11.16
+      babel-plugin-macros: 2.8.0
+      babel-plugin-syntax-jsx: 6.18.0
+      convert-source-map: 1.9.0
+      escape-string-regexp: 1.0.5
+      find-root: 1.1.0
+      source-map: 0.5.7
+    dev: false
+
   /babel-plugin-istanbul/6.1.1:
     resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
     engines: {node: '>=8'}
@@ -3413,6 +3531,14 @@ packages:
       '@types/babel__traverse': 7.18.2
     dev: false
 
+  /babel-plugin-macros/2.8.0:
+    resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==}
+    dependencies:
+      '@babel/runtime': 7.20.1
+      cosmiconfig: 6.0.0
+      resolve: 1.22.1
+    dev: false
+
   /babel-plugin-macros/3.1.0:
     resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
     engines: {node: '>=10', npm: '>=6'}
@@ -3466,6 +3592,10 @@ packages:
       - supports-color
     dev: false
 
+  /babel-plugin-syntax-jsx/6.18.0:
+    resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
+    dev: false
+
   /babel-plugin-transform-react-remove-prop-types/0.4.24:
     resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==}
     dev: false
@@ -4282,6 +4412,10 @@ packages:
       cssom: 0.3.8
     dev: false
 
+  /csstype/2.6.21:
+    resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
+    dev: false
+
   /csstype/3.1.1:
     resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
     dev: false
@@ -5311,6 +5445,10 @@ packages:
       pkg-dir: 4.2.0
     dev: false
 
+  /find-root/1.1.0:
+    resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
+    dev: false
+
   /find-up/3.0.0:
     resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
     engines: {node: '>=6'}
@@ -7686,6 +7824,11 @@ packages:
       find-up: 3.0.0
     dev: false
 
+  /popper.js/1.16.1:
+    resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==}
+    deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1
+    dev: false
+
   /postcss-attribute-case-insensitive/5.0.2_postcss@8.4.19:
     resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==}
     engines: {node: ^12 || ^14 || >=16}
@@ -8560,6 +8703,11 @@ packages:
       side-channel: 1.0.4
     dev: false
 
+  /qss/2.0.3:
+    resolution: {integrity: sha512-j48ZBT5IZbSqJiSU8EX4XrN8nXiflHvmMvv2XpFc31gh7n6EpSs75bNr6+oj3FOLWyT8m09pTmqLNl34L7/uPQ==}
+    engines: {node: '>=4'}
+    dev: false
+
   /querystringify/2.2.0:
     resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
     dev: false
@@ -9197,6 +9345,47 @@ packages:
     resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==}
     dev: false
 
+  /react-input-color/4.0.1_biqbaboplfbrettd7655fr4n2y:
+    resolution: {integrity: sha512-ev1e0IbB/Chc4wWjGu7/UYsxMrSpz9eI/Ix60BBLCxsANzsfQwcw37lno/7v0ih04ug/B9HEBfTcs2GtsfsIrQ==}
+    peerDependencies:
+      react: ^16.8.4 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.4 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@babel/runtime': 7.20.1
+      '@emotion/core': 10.3.1_react@18.2.0
+      '@swiftcarrot/color-fns': 3.2.0
+      '@xkit/popover': 0.1.24_react@18.2.0
+      react: 18.2.0
+      react-dom: 18.2.0_react@18.2.0
+      react-input-number: 5.0.19_biqbaboplfbrettd7655fr4n2y
+      react-input-slider: 5.1.7_biqbaboplfbrettd7655fr4n2y
+    dev: false
+
+  /react-input-number/5.0.19_biqbaboplfbrettd7655fr4n2y:
+    resolution: {integrity: sha512-Aa9RmOoOgzCn6b/RYyrKRkBtIWGum6v9sA73rJFtl9N0O3kjWrAdj9lk09n9QoOaxvyEHc3GQZwLcUrBEIH2Cw==}
+    peerDependencies:
+      react: ^16.8.4
+      react-dom: ^16.8.4
+    dependencies:
+      '@babel/runtime': 7.20.1
+      '@emotion/core': 10.3.1_react@18.2.0
+      lodash: 4.17.21
+      react: 18.2.0
+      react-dom: 18.2.0_react@18.2.0
+    dev: false
+
+  /react-input-slider/5.1.7_biqbaboplfbrettd7655fr4n2y:
+    resolution: {integrity: sha512-w8ciwQCJcY5qKygQQTKTvnxR1CYiTE6fd1M0w29sHqzXPq+44SiWqPJxg7esuejFzRAluHaus7BEgRSJcQcpLg==}
+    peerDependencies:
+      react: ^16.8.4
+      react-dom: ^16.8.4
+    dependencies:
+      '@babel/runtime': 7.20.1
+      '@emotion/core': 10.3.1_react@18.2.0
+      react: 18.2.0
+      react-dom: 18.2.0_react@18.2.0
+    dev: false
+
   /react-is/16.13.1:
     resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
     dev: false
@@ -9940,6 +10129,11 @@ packages:
       source-map: 0.6.1
     dev: false
 
+  /source-map/0.5.7:
+    resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
   /source-map/0.6.1:
     resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
     engines: {node: '>=0.10.0'}

+ 26 - 23
src/api/board.ts

@@ -23,90 +23,92 @@ export interface Pos {
   y: number
 }
 
-interface Shape {
+interface ShapeData {
   color: string
 }
 
-interface CurrencyShape extends Shape {
+interface CurrencyShapeData extends ShapeData {
   pos: Pos,
   width: number,
   height: number
 }
 
-export interface BrokenLineShape extends Shape {
+export interface BrokenLineShapeData extends ShapeData {
   type: typeof brokenLine
   points: Pos[]
 }
 
-export interface TextShape extends Shape {
+export interface TextShapeData extends ShapeData {
   type: typeof text
+  text: string
   fontSize: number
 }
 
-export interface TableShape extends CurrencyShape {
+export interface TableShapeData extends CurrencyShapeData {
   type: typeof table
+  fontSize: number
   content: string[][]
 }
 
-export interface RectShape extends CurrencyShape {
+export interface RectShapeData extends CurrencyShapeData {
   type: typeof rect
 }
 
-export interface CircularShape extends CurrencyShape {
+export interface CircularShapeData extends CurrencyShapeData {
   type: typeof circular
 }
 
-export interface ArrowShape extends CurrencyShape {
+export interface ArrowShapeData extends CurrencyShapeData {
   type: typeof arrow
   direction: number
 }
 
-export interface IconShape extends CurrencyShape {
+export interface IconShapeData extends CurrencyShapeData {
   type: typeof icon
   index: number
 }
 
-export interface CigaretteShape extends CurrencyShape {
+export interface CigaretteShapeData extends CurrencyShapeData {
   type: typeof cigarette
 }
 
-export interface FireointShape extends CurrencyShape {
+export interface FireointShapeData extends CurrencyShapeData {
   type: typeof fireoint
 }
 
-export interface FootPrintShape extends CurrencyShape {
+export interface FootPrintShapeData extends CurrencyShapeData {
   type: typeof footPrint
 }
 
-export interface ShoePrintShape extends CurrencyShape {
+export interface ShoePrintShapeData extends CurrencyShapeData {
   type: typeof shoePrint
 }
 
-export interface FingerPrintShape extends CurrencyShape {
+export interface FingerPrintShapeData extends CurrencyShapeData {
   type: typeof fingerPrint
 }
 
-export interface CorpseShape extends CurrencyShape {
+export interface CorpseShapeData extends CurrencyShapeData {
   type: typeof corpse
 }
 
-export interface TheBloodShape extends CurrencyShape {
+export interface TheBloodShapeData extends CurrencyShapeData {
   type: typeof theBlood
 }
 
-export interface FootPrintShape extends CurrencyShape {
+export interface FootPrintShapeData extends CurrencyShapeData {
   type: typeof footPrint
 }
 
-export type BoardShape = BrokenLineShape | TextShape | TableShape | RectShape | CircularShape 
-  | ArrowShape | IconShape | CigaretteShape | FireointShape | FootPrintShape | ShoePrintShape 
-  | FingerPrintShape | CorpseShape | TheBloodShape
+export type BoardShapeData = BrokenLineShapeData | TextShapeData | TableShapeData | RectShapeData | CircularShapeData 
+  | ArrowShapeData | IconShapeData | CigaretteShapeData | FireointShapeData | FootPrintShapeData | ShoePrintShapeData 
+  | FingerPrintShapeData | CorpseShapeData | TheBloodShapeData
 
 
 export interface BoardData {
   id: number
-  bgImage?: string
-  shapes: BoardShape[]
+  bgImage: string | null
+  shapes: BoardShapeData[]
 }
 
 export enum BoardType {
@@ -123,7 +125,8 @@ export const getBoardById = (params: { id: number, type: BoardType }) => {
   axios.get<BoardData>(GET_DRAW_FILE, { params: params })
   return {
     id: params.id,
-    shapes: []
+    shapes: [],
+    bgImage: null
   }
 }
 

+ 3 - 0
src/react-app-env.d.ts

@@ -1,5 +1,7 @@
 /// <reference types="react-scripts" />
 
+type OmitBasic<T, U> = T extends U ? never : T
+
 type ExcludeObject<T, U> = {
   [key in keyof T]: Exclude<T[key], U>
 }
@@ -22,6 +24,7 @@ type ExtractRouteParams<T> = {
   [key in ExtractRouteParamsKey<T>]: string
 }
 
+type IntersectionFromUnion<T> = [T extends object ? (arg: T) => void : never] extends [(arg: infer P) => any] ? P : any
 
 declare module 'canvas-nest.js' {
   export default any

+ 3 - 3
src/store/board.ts

@@ -1,10 +1,11 @@
 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
 import { getBoardById, setBoard, addBoard } from 'api'
 
+
 import type { BoardData } from 'api'
 import type { StoreState } from 'store'
 
-export type { BoardData, BoardShape } from 'api'
+export * from 'api/board'
 export { BoardType, BoardTypeDesc } from 'api'
 
 export interface BoardState {
@@ -13,7 +14,7 @@ export interface BoardState {
 }
 
 const initialState: BoardState = { 
-  list: [{ shapes: [], id: -1 }], 
+  list: [{ shapes: [], id: -1, bgImage: null  }], 
   currentIndex: 0
 }
 
@@ -44,7 +45,6 @@ const boardSlice = createSlice({
     backoff(state, { payload = 1 }: { payload: number }) {
       let move = state.currentIndex - payload
       state.currentIndex = move < 0 ? 0 : move
-      console.log('backoff')
       return state
     },
     push: pushState,

+ 19 - 4
src/views/draw-file/board/index.d.ts

@@ -1,6 +1,7 @@
 import { metas as fMetas } from './shape'
-import type { BoardData } from 'store'
-import type {  } from 'mime'
+import type { BoardData, BoardShapeData } from 'store'
+import type { Emitter } from 'mitt'
+
 
 type Metas = typeof fMetas
 export type ShapeType = keyof Metas
@@ -9,14 +10,27 @@ export interface Pos {
   y: number
 }
 
+// 形状通用对象
+export type BoardShape<T = BoardShapeData, K = keyof T> = IntersectionFromUnion<
+  K extends 'type'
+    ? { data: { [key in K]: T[K]} }
+    : { data: { [key in K]: T[K] } } & { [key in `set${Capitalize<K>}`]: (val: T[K]) => void }
+>
+
+export type ExtractShape<T extends string, D = BoardShapeData> = BoardShape<
+  D extends object
+    ? T extends keyof D ? D : never
+    : never
+>
+
 export type Board = {
   el: HTMLCanvasElement
   bus: Emitter<{
     storeChange: undefined
+    selectShape: BoardShape | null
   }>
   setImage(url: string): void
-  addShape(type: ShapeType, pos: Pos): void
-  getPosByScreen(screen: Pos): Pos
+  readyAddShape(type: ShapeType, onFinish?: () => void): () => void
   getCurrentStore(): BoardData
   drawStore(store: BoardData): void
   export(): Promise<Blob>
@@ -25,6 +39,7 @@ export type Board = {
 
 function create (store: BoardData, canvas: HTMLCanvasElement): Board
 
+export { BoardShapeData }
 export const metas: Metas
 export const images: ShapeType[]
 export const labels: ShapeType[]

+ 91 - 21
src/views/draw-file/board/index.js

@@ -1,37 +1,107 @@
 import mitt from 'mitt'
+import {
+  text,
+  table
+} from './shape'
+
+const createShape = (refs, shapeData) => {
+  console.log('创建', shapeData)
+  const shape = {
+    data: shapeData,
+    setColor(color) {
+      console.log('set color', color)
+    },
+    destory() {
+      console.log('删除此数据')
+      const currentIndex = refs.shapes.indexOf(shape)
+      if (currentIndex !== -1) {
+        refs.shapes.splice(currentIndex, 1)
+      }
+    }
+  }
+
+  switch (shapeData.type) {
+    case text:
+      shape.setText = (text) => {
+        console.log('设置文字', text)
+      }
+    case table:
+      shape.setFontSize = (text) => {
+        console.log('设置文字大小', text)
+      }
+      break;
+    default:
+      console.log('完成')
+  }
+  refs.shapes.push(shape)
+
+  return shape
+}
+
+const createBasemap = (refs, url) => {
+  const baseMap = {
+    data: url,
+    changeImage(url) {
+      baseMap.data = url
+      console.log('更换底图')
+    }
+  }
+  return baseMap
+}
+
+const toStore = (refs) => {
+  return {
+    bgImage: refs.baseMap?.data || null,
+    shapes: refs.shapes.map(shape => shape.data)
+  }
+}
 
 export const create = (store, canvas) => {
-  const bus = mitt()
+  const refs = {
+    ctx: canvas.getContext('2d'),
+    bus: mitt(),
+    shapes: [],
+    baseMap: null
+  }
+  const generateRefs = (store) => {
+    store.shapes.forEach(shapeData => createShape(refs, shapeData))
+    createBasemap(store.bgImage)
+  }
+  generateRefs(store)
+
+  canvas.addEventListener('click', () => {
+    // refs.bus.emit('selectShape', null)
+  })
+
   const board = {
-    el: canvas,
-    bus,
-    getCurrentStore() {
-      return store
-    },
+    bus: refs.bus,
+    getCurrentStore:() => ({ ...store, ...toStore(refs) }),
     drawStore(newStore) {
-      store = newStore
-    },
-    getPosByScreen(screen) {
-      return screen
+      refs.ctx.clearRect(0,0, canvas.width, canvas.height)
+      refs.shapes = []
+      refs.baseMap = null
+      generateRefs(newStore)
     },
-    addShape(shapeType, pos) {
-      store.shapes.push({
-        type: shapeType,
-        pos
-      })
-      bus.emit('storeChange')
-      console.log('添加', shapeType, pos)
+    readyAddShape(shapeType, onFine) {
+      const definePosition = ev => {
+        const shape = createShape(refs, { type: shapeType, pos: { x: ev.offsetX, y: ev.offsetY } })
+        cleaup()
+        onFine()
+        refs.bus.emit('storeChange')
+        refs.bus.emit('selectShape', shape)
+      }
+      canvas.addEventListener('click',definePosition)
+      const cleaup = () => canvas.removeEventListener('click',definePosition)
+      return cleaup
     },
     setImage(url) {
-      console.log('设置底图', url)
-      store.bgImage = url
-      bus.emit('storeChange')
+      refs.baseMap.changeImage(url)
+      refs.bus.emit('storeChange')
     },
     export() {
       return new Promise(resolve => canvas.toBlob(resolve))
     },
     destroy() {
-      
     }
   }
 

+ 88 - 0
src/views/draw-file/eshape.tsx

@@ -0,0 +1,88 @@
+import { useEffect, useState } from "react"
+import { Form, Input, Select } from 'antd'
+import { CloseOutlined } from '@ant-design/icons'
+import InputColor from 'react-input-color';
+import style from './style.module.scss'
+
+import type { Board, BoardShape, ShapeType, ExtractShape } from "./board"
+import type { RefObject, ComponentType } from 'react'
+
+
+const ColorInput = ({ shape }: { shape: BoardShape }) => (
+  <Form.Item label="颜色" className={style['def-color-item']}>
+    <InputColor 
+      initialValue={shape.data.color || '#000'} 
+      onChange={(color) => shape.setColor(color.rgba)}
+    />
+  </Form.Item>
+)
+
+const sizeOptions = [6,7,8,9,10,11,12,13,14,16,18,20,28,36,48,72]
+  .map(size => ({ label: `${size}px`, value: size }))
+const FontSizeInput = ({ shape }: { shape: ExtractShape<'fontSize'> }) => (
+  <Form.Item label="字号">
+    <Select
+      defaultValue={shape.data.fontSize}
+      style={{ width: 80 }}
+      onChange={(size) => shape.setFontSize(size)}
+      options={sizeOptions}
+    />
+  </Form.Item>
+)
+
+
+const TextInput = ({ shape }: { shape: ExtractShape<'text'> }) => (
+  <Form.Item label="内容">
+    <Input 
+      style={{ width: 120 }}
+      defaultValue={shape.data.text} 
+      onChange={(ev) => shape.setText(ev.target.value)} 
+    />
+  </Form.Item>
+)
+
+const shapeCompontes: { [key in ShapeType]: ComponentType<{ shape: any }> } = {
+  broken: ColorInput,
+  text: (props) => <><ColorInput {...props} /><FontSizeInput {...props} /><TextInput {...props} /> </>,
+  table: (props) => <><ColorInput {...props} /><FontSizeInput {...props} /> </>,
+  rect: ColorInput,
+  circular: ColorInput,
+  arrow: ColorInput,
+  icon: ColorInput,
+  cigarette: ColorInput,
+  fireoint: ColorInput,
+  footPrint: ColorInput,
+  footPrintRever: ColorInput,
+  shoePrint: ColorInput,
+  shoePrintRever: ColorInput,
+  fingerPrint: ColorInput,
+  corpse: ColorInput,
+  theBlood: ColorInput
+}
+
+export type EShapeProps = {
+  board: RefObject<Board>
+}
+export const EShape = ({ board }: EShapeProps) => {
+  const [shape, setShape] = useState<BoardShape | null>(null)
+
+  useEffect(() => {
+    if (board.current) {
+      const boardRef = board.current
+      boardRef.bus.on('selectShape', setShape)
+      return () => boardRef.bus.off('selectShape', setShape)
+    }
+  }, [board])
+
+  const Edit = shape && shapeCompontes[shape.data.type]
+  return Edit && (
+    <div className={style['def-shape-edit']}>
+      <Edit shape={shape} />
+      <div className={`ant-form-item ${style['def-close-shape-edit']}`} onClick={() => setShape(null)}>
+        <CloseOutlined className={`${style['icon']}`} />
+      </div>
+    </div>
+  )
+}
+
+export default EShape

+ 12 - 12
src/views/draw-file/header.tsx

@@ -1,4 +1,4 @@
-import { ArrowLeftOutlined, ArrowRightOutlined, DoubleLeftOutlined } from "@ant-design/icons"
+import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons"
 import { Button } from "antd"
 import { useNavigate, fillRoutePath, RoutePath, usePathData } from "router"
 import { saveAs } from 'utils'
@@ -48,27 +48,27 @@ const Header = ({ board, type }: HeaderProps) => {
   return (
     <>
       <div className={style['df-header-left']}>
-        <span className={style['df-header-back']} onClick={() => navigate(-1)}>
-          <DoubleLeftOutlined />
+        <span className={`${style['df-header-back']} ${style['icon']}`} onClick={() => navigate(-1)}>
+          <ArrowLeftOutlined />
           返回
         </span>
+      </div>
+      <h1 className={style['df-header-center']}>
+        创建{ BoardTypeDesc[type] }
+      </h1>
+      <div className={style['df-header-right']}>
         <div className={style['df-header-action']}>
           <ArrowLeftOutlined 
-            className={!status.canBack ? 'disabled': ''} 
+            className={!status.canBack ? 'disabled':  style['icon']} 
             onClick={() => dispatch({ type: 'board/backoff' })} 
           />
           <ArrowRightOutlined 
-            className={!status.canForward ? 'disabled': ''} 
+            className={!status.canForward ? 'disabled': style['icon']} 
             onClick={() => dispatch({ type: 'board/forward' })} 
           />
         </div>
-      </div>
-      <h1 className={style['df-header-center']}>
-        创建{ BoardTypeDesc[type] }
-      </h1>
-      <div className={style['df-header-right']}>
-        <Button type="primary" size="large" onClick={exportPng}>导出</Button>
-        <Button type="primary" size="large" onClick={save}>保存</Button>
+        <Button type="primary" size="middle" onClick={save}>保存</Button>
+        <Button type="default" size="middle" onClick={exportPng}>导出</Button>
       </div>
     </>
   )

+ 3 - 1
src/views/draw-file/index.tsx

@@ -6,6 +6,7 @@ import style from './style.module.scss'
 import boardFactory from './board'
 import DfSlider from './slider'
 import DfHeader from './header'
+import EShape from './eshape'
 import { 
   currentBoard, 
   useDispatch, 
@@ -84,7 +85,8 @@ export const DrawFile = () => {
         <Sider className={style['df-sider']}>
           <DfSlider board={ board } type={ pathType } caseId={ caseId } />
         </Sider>
-        <Content>
+        <Content className={style['def-content']}>
+          { <EShape board={ board } /> }
           { pathId === boardData.id && <DfBoard ref={board} /> }
         </Content>
       </Layout>

+ 40 - 0
src/views/draw-file/shapes/index.tsx

@@ -0,0 +1,40 @@
+import { ReactComponent as brokenLineSVG} from 'assets/svg/brokenLine.svg'
+import { ReactComponent as textSVG} from 'assets/svg/text.svg'
+import { ReactComponent as tableSVG} from 'assets/svg/table.svg'
+import { ReactComponent as rectSVG} from 'assets/svg/rect.svg'
+import { ReactComponent as circularSVG} from 'assets/svg/circular.svg'
+import { ReactComponent as arrowSVG} from 'assets/svg/arrow.svg'
+import { ReactComponent as iconSVG} from 'assets/svg/icon.svg'
+import { ReactComponent as cigaretteSVG} from 'assets/svg/cigarette.svg'
+import { ReactComponent as fireointSVG} from 'assets/svg/fireoint.svg'
+import { ReactComponent as footPrintSVG} from 'assets/svg/footPrint.svg'
+import { ReactComponent as footPrintReverSVG} from 'assets/svg/footPrintRever.svg'
+import { ReactComponent as shoePrintSVG} from 'assets/svg/shoePrint.svg'
+import { ReactComponent as shoePrintReverSVG} from 'assets/svg/shoePrintRever.svg'
+import { ReactComponent as fingerPrintSVG} from 'assets/svg/fingerPrint.svg'
+import { ReactComponent as corpseSVG} from 'assets/svg/corpse.svg'
+import { ReactComponent as theBloodSVG} from 'assets/svg/theBlood.svg'
+
+import type { ShapeType } from '../board'
+import type { ComponentType } from 'react'
+
+export const shapes: { [key in ShapeType]: ComponentType } = {
+  broken: brokenLineSVG,
+  text: textSVG,
+  table: tableSVG,
+  rect: rectSVG,
+  circular: circularSVG,
+  arrow: arrowSVG,
+  icon: iconSVG,
+  cigarette: cigaretteSVG,
+  fireoint: fireointSVG,
+  footPrint: footPrintSVG,
+  footPrintRever: footPrintReverSVG,
+  shoePrint: shoePrintSVG,
+  shoePrintRever: shoePrintReverSVG,
+  fingerPrint: fingerPrintSVG,
+  corpse: corpseSVG,
+  theBlood: theBloodSVG
+}
+
+export default shapes

+ 20 - 17
src/views/draw-file/slider.tsx

@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
 import { metas, labels, images } from './board'
 import { BoardType, BoardTypeDesc } from 'store'
 import { SelectMap, SelectFuse } from './modal'
+import shapes from './shapes'
 import style from './style.module.scss'
 
 import type { ShapeType, Board } from './board'
@@ -16,16 +17,19 @@ type SliderProps = {
 export const DfSlider = ({ board, type, caseId }: SliderProps) => {
   const [selectMode, setSelectMode] = useState(false)
   const [currentShape, setCurrentShape] = useState<ShapeType>()
-  const getEle = (shapeType: ShapeType) => (
-    <div 
-      key={shapeType}
-      className={style['df-slide-shape'] + (currentShape === shapeType ? ` ${style['active']}` : '')} 
-      onClick={() => setCurrentShape(shapeType)}
-    >
-      <img src={metas[shapeType].icon} />
-      <p>{metas[shapeType].desc}</p>
-    </div>
-  )
+  const getEle = (shapeType: ShapeType) => {
+    const Shape = shapes[shapeType]
+    return (
+      <div 
+        key={shapeType}
+        className={style['df-slide-shape'] + (currentShape === shapeType ? ` ${style['active']}` : '')} 
+        onClick={() => setCurrentShape(shapeType)}
+      >
+        <Shape />
+        <p>{metas[shapeType].desc}</p>
+      </div>
+    )
+  }
   const setBoardImage = async (blob: Blob) => {
     const url = URL.createObjectURL(blob)
     board.current?.setImage(url)
@@ -40,21 +44,20 @@ export const DfSlider = ({ board, type, caseId }: SliderProps) => {
   useEffect(() => {
     const boardRef = board.current
     if (currentShape && boardRef) {
-      const clickHandler = (ev: MouseEvent) => {
-        const pos = boardRef.getPosByScreen({ x: ev.offsetX, y: ev.offsetY })
-        boardRef.addShape(currentShape, pos)
-        setCurrentShape(void 0)
-      }
+      console.log('read add', currentShape)
+      const cleaup = boardRef.readyAddShape(
+        currentShape,
+        () => setCurrentShape(void 0)
+      )
       const keyupHandler = (ev: KeyboardEvent) => {
         if (ev.key === 'Escape') {
           setCurrentShape(void 0)
+          cleaup()
         }
       }
-      boardRef.el.addEventListener('click', clickHandler)
       document.documentElement.addEventListener('keyup', keyupHandler)
 
       return () => {
-        boardRef.el.removeEventListener('click', clickHandler)
         document.documentElement.removeEventListener('keyup', keyupHandler)
       }
     }

+ 78 - 10
src/views/draw-file/style.module.scss

@@ -2,8 +2,9 @@
   color: #fff;
   display: flex;
   align-items: center;
+  justify-content: space-between;
   width: 100%;
-  height: 64px;
+  height: 60px;
   line-height: inherit;
   padding: 0 20px;
 }
@@ -14,9 +15,12 @@
 }
 
 .df-header-center {
-  flex: 1;
+  position: absolute;
+  left: 0;
+  right: 0;
   color: inherit;
   text-align: center;
+  font-size: 24px;
   margin: 0;
 }
 
@@ -25,22 +29,35 @@
   align-items: center;
 }
 
-.df-header-back,
-.df-header-action span {
+.df-header-back span {
+  margin-right: 18px;
+}
+
+.icon {
   cursor: pointer;
   transition: color .3s ease;
   &:hover {
     color: #26559B;
   }
+}
+
+.df-header-left,
+.df-header-right {
+  z-index: 1;
+}
 
+.df-header-right {
+  display: flex;
+  align-items: center;
 }
 
 .df-header-action {
-  margin-left: 20px;
+  margin-right: 60px;
   span {
-    font-size: 1.5em;
-    margin-left: 10px;
-    margin-right: 10px;
+    font-size: 15px;
+    &:not(:first-child) {
+      margin-left: 32px;
+    }
   }
 }
 
@@ -59,12 +76,16 @@
 .df-board {
   margin: 30px;
   height: calc(100% - 30px);
+  padding: 10px;
   overflow: auto;
   display: flex;
   align-items: center;
   justify-content: center;
 
   canvas {
+    box-sizing: content-box;
+    border: 1px solid #000;
+    outline: 10px solid #fff;
     background: #fff;
     margin: auto;
     width: 940px;
@@ -93,7 +114,6 @@
   width: 88px;
   height: 88px;
   text-align: center;
-  line-height: 60px;
   color: rgba(0,0,0,0.85);;
   cursor: pointer;
   transition: all .3s;
@@ -102,8 +122,13 @@
   justify-content: center;
   align-items: center;
 
+  :global(svg) {
+    width: 40px;
+    height: 40px;
+    flex: none;
+  }
   p {
-    margin: 0;
+    margin: 10px 0 0;
   }
   &.active,
   &:hover {
@@ -111,6 +136,49 @@
   }
 }
 
+.def-content {
+  position: relative;
+}
+
+.def-shape-edit {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  right: 0;
+  background: #fff;
+  box-shadow: 0 0 5px rgba(0,0,0,0.2);
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 15px;
+  width: fit-content;
+
+  > :global(.ant-form-item) {
+    margin-bottom: 0;
+    &:not(:last-child) {
+      margin-right: 15px;
+    }
+  }
+}
+
+.def-close-shape-edit {
+  align-self: start;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.85)
+}
+
+.def-color-item :global(.ant-form-item-control-input-content) {
+  display: flex;
+  align-items: center;
+
+  > span {
+    height: 32px;
+    width: 60px;
+  }
+}
+
 .def-select-map {
   height: 500px;
 }