344 Achegas ca81492ae3 ... 1e80cb5e7c

Autor SHA1 Mensaxe Data
  bill 1e80cb5e7c custom hai 2 semanas
  bill a28619b08a Merge branch 'v2.0.0-ga' into v2.0.0-jm-local hai 2 semanas
  bill e1d6bd3a26 fix: 1 hai 2 semanas
  bill f7c2896907 fix: 1 hai 2 semanas
  bill 58b4a57911 fix: 1 hai 2 semanas
  bill 94fb7d7fbf Merge branch 'v2.0.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v2.0.0-ga hai 2 semanas
  bill 7e79b2ee84 fix: 1 hai 2 semanas
  xzw 361e9456f8 fix: done后transformChanged发送时间调整 hai 2 semanas
  bill 3ae4e425bd fix: 1 hai 2 semanas
  bill b30cf0d00c fix: 1 hai 3 semanas
  bill f56af4c492 fix: 1 hai 3 semanas
  bill be2fc037b1 fix: 1 hai 3 semanas
  bill c273802139 fix: 1 hai 3 semanas
  bill a7314c5d3d Merge branch 'v2.0.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v2.0.0-ga hai 3 semanas
  bill 1c4b1008bf fix: 1 hai 3 semanas
  xzw d0a845d7c3 fix: 1 hai 3 semanas
  bill f2df4a2fee fix: 1 hai 3 semanas
  bill b84c0b5fde fix: 1 hai 3 semanas
  bill 54504991ae fix: 1 hai 1 mes
  bill 12a5d817ae fix: 1 hai 1 mes
  bill 18d5aa35a5 Merge branch 'v2.0.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v2.0.0-ga hai 1 mes
  bill 662c792128 fix: 1 hai 1 mes
  xzw 51c8f6f80d fix: 0 hai 1 mes
  bill db48a9d801 fix: 1 hai 1 mes
  bill 642483b05f fix: unset修正 hai 1 mes
  bill b24c0b1f7e fix: 1 hai 1 mes
  bill 79237ce027 fix: 1 hai 1 mes
  bill fb447575a2 fix: 1 hai 1 mes
  bill 6160a8e725 fix: 1 hai 1 mes
  bill 6e55a1b8b6 feat: 制作全屏需求 hai 1 mes
  bill f038e4abe8 fix: 1 hai 1 mes
  bill 4d790a6b91 fix: 1 hai 2 meses
  bill 254efb32ff fix: 1 hai 2 meses
  xzw b0d72e2c20 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 2 meses
  xzw da74390cd3 fix: monitor hai 2 meses
  bill 6ec9df244d Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 2 meses
  xzw 2d4eb12125 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 2 meses
  xzw a476f4b4e1 fix: 圆周率场景分为0和4两种场景 hai 2 meses
  bill 8d455b7307 fix: 1 hai 2 meses
  bill 5bc4371de4 fix: 1 hai 3 meses
  xzw bd84843c38 fix: update code hai 3 meses
  bill 5113e1cf13 fix: 1 hai 3 meses
  bill 29b1bd3c8a fix: 1 hai 3 meses
  xzw 2b3a2397e7 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 3 meses
  bill aaa70a0f27 fix: 1 hai 3 meses
  xzw 60d4c79801 fix: defaultMapProps hai 3 meses
  xzw 30aabc65e3 fix: queryCloudLonLatUrl hai 4 meses
  xzw 603e8e2fe3 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 4 meses
  xzw 5800c04d80 fix: 1 hai 4 meses
  xzw 0fdaf97d5a fix: showscope hai 4 meses
  bill 214567ca24 fix: 1 hai 4 meses
  bill d2a087ff29 fix: 1 hai 4 meses
  xzw 8fcda3cab4 fix: update lessCurvePoints hai 4 meses
  xzw 9d2438f170 fix: queryCloudLonLatUrl hai 4 meses
  xzw c7a40184f7 fix: fileStorage hai 4 meses
  xzw 0915e3cc95 fix: overlay hai 4 meses
  xzw dba72b99da fix: 只有动作播放或改变时mixer才更新 hai 4 meses
  xzw 822f3a7b9b fix: map3d hai 4 meses
  xzw ccb1f07d69 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 4 meses
  xzw 78b12fcac2 fix: 测试替换3dtiles 带坐标 hai 4 meses
  bill 78dceabcf0 fix: 1 hai 4 meses
  bill dcc54b6b8b fix: 1 hai 5 meses
  bill a3cbdb0608 fix: 1 hai 5 meses
  bill 5ba2159a64 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  bill cca559389a fix: 1 hai 5 meses
  xzw 2467b336c8 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw f584cc4c19 fix: 0 hai 5 meses
  bill 9e80e3ed95 fix: 1 hai 5 meses
  xzw e8de639adf fix: 1 hai 5 meses
  xzw 529c697429 fix: 1 hai 5 meses
  xzw a905ecc9bd fix: 1 hai 5 meses
  xzw 75104d56f2 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw a61cd05f96 fix: 1 hai 5 meses
  bill c2b8f3d69b fix: 1 hai 5 meses
  xzw a82f3913b6 fix: webgl1 shader err hai 5 meses
  xzw 9adf45e379 fix: 1 hai 5 meses
  xzw b169cf8076 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 2f353b240b fix: 1 hai 5 meses
  bill 5e95a77b3e fix: 1 hai 5 meses
  xzw fbc8388039 fix: 1 hai 5 meses
  bill 64d2e406a0 fix: 1 hai 5 meses
  xzw 3be7a0a0d3 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw bd17de0eb9 fix: 1 hai 5 meses
  bill a424a72086 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  bill 7f0f62ba81 fix: 1 hai 5 meses
  xzw e52b166e5d fix: 1 hai 5 meses
  xzw cdbe7ae1aa Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 6f6f061a71 fix: 1 hai 5 meses
  bill f431bc5744 fix: 1 hai 5 meses
  bill 8cb0cc2cc8 fix: 1 hai 5 meses
  bill 7d0ff6532e fix: 1 hai 5 meses
  xzw 12adca03b9 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 14058cf512 fix: 1 hai 5 meses
  bill 9547c1e794 fix: 1 hai 5 meses
  bill a31b19a343 fix: 1 hai 5 meses
  xzw bdcdb24532 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw f66267510b fix: 1 hai 5 meses
  bill f652466786 fix: 1 hai 5 meses
  bill 8e0c2f59b0 fix: 1 hai 5 meses
  xzw 69763c888b fix: 1 hai 5 meses
  xzw 4b272acb09 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 8b4da213d0 fix: ani action hai 5 meses
  bill ab7c839dfd fix: 1 hai 5 meses
  bill 05bc1cea82 fix: 1 hai 5 meses
  xzw 685fc56bfc Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 5e95d5127d fix: 1 hai 5 meses
  bill 721f872808 fix: 1 hai 5 meses
  bill 249f0d501d fix: 1 hai 5 meses
  bill 2ea910e8d7 fix: 1 hai 5 meses
  bill ccffa519f1 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  bill 702a197b61 fix: 1 hai 5 meses
  xzw c63507e628 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 4e308752f0 fix: 1 hai 5 meses
  bill 87e294a35c fix: 1 hai 5 meses
  bill b35f2f5bd9 fix: 1 hai 5 meses
  bill 8db269bce0 fix: 1 hai 5 meses
  bill c8ea599a89 fix: 1 hai 5 meses
  bill 301d686e2f fix: 1 hai 5 meses
  xzw 9347247aec fix: 1 hai 5 meses
  bill 94a1112e9f fix: 1 hai 5 meses
  bill 2c5e50adaa fix: 1 hai 5 meses
  bill 47f86644f5 fix: 1 hai 5 meses
  bill 7433d36e1d fix: 1 hai 5 meses
  bill b74047458b fix: 1 hai 5 meses
  bill c5079dc009 fix: 1 hai 5 meses
  xzw 5d2aeae19b fix: 1 hai 5 meses
  xzw f5ffde7db4 fix: 1 hai 5 meses
  bill 802eb061a5 fix: 1 hai 5 meses
  xzw 85e36c40bb fix: 1 hai 5 meses
  xzw a3a3677946 fix: 1 hai 5 meses
  xzw 68c5a49d59 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 6ff357749b fix: quaAtPath hai 5 meses
  bill 46a1c1b3bf fix: 1 hai 5 meses
  bill 5da11aeb13 fix: 1 hai 5 meses
  bill d52150712d fix: 1 hai 5 meses
  bill a9fb87d05d fix: 1 hai 5 meses
  bill 0875f31284 fix: 1 hai 5 meses
  xzw 818a52d975 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 65b59f274e fix: 1 hai 5 meses
  bill b5e7be8496 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  bill 83b1048492 fix: 1 hai 5 meses
  xzw 03c29ab6e0 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw b82f8dadfb fix: 1 hai 5 meses
  bill 9674c3c8df fix: 1 hai 5 meses
  bill 43f3732a5e fix: 1 hai 5 meses
  bill b14db1b542 fix: 1 hai 5 meses
  bill 358ea80b65 fix: 1 hai 5 meses
  bill 3c81d47093 fix: 1 hai 5 meses
  bill 3ee1547f57 fix: 1 hai 5 meses
  bill 6202794422 fix: 1 hai 5 meses
  xzw 82c8e03200 fix: 1 hai 5 meses
  bill 65d0965649 fix: 1 hai 5 meses
  bill c01076d6c3 fix: 1 hai 5 meses
  bill e356f6f130 fix: 1 hai 5 meses
  xzw c9c1c129ea fix: 1 hai 5 meses
  bill 015506d01e fix: 1 hai 5 meses
  bill a6b75a1582 fix: 1 hai 5 meses
  bill 2d73c52da7 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  bill 1914da55c2 fix: 1 hai 5 meses
  xzw e9b5a8f1bc fix: 1 hai 5 meses
  bill 614d98c600 fix: 1 hai 5 meses
  xzw c9112bd1cd fix: 1 hai 5 meses
  bill 8a4417d378 fix: 1 hai 5 meses
  bill 5adcbb3fdf fix: 1 hai 5 meses
  bill 98b7bd2386 fix: 1 hai 5 meses
  xzw 33e32e9b41 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 5 meses
  xzw 1bcb5fe9fd fix: 1 hai 5 meses
  bill 8c0d6751a9 fix: 1 hai 5 meses
  xzw d728707a50 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 37e45ff450 fix: 动画调整 hai 6 meses
  bill ba454520fe fix: 1 hai 6 meses
  bill 17eb2fde84 fix: 1 hai 6 meses
  bill 7f6f45e253 fix: 1 hai 6 meses
  bill ce5e45ca25 fix: 1 hai 6 meses
  bill e0b1ac0ed4 fix: 1 hai 6 meses
  bill 65538be5b1 fix: 1 hai 6 meses
  bill 970e98207b fix: 1 hai 6 meses
  bill 40690eadb0 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill cf49957c89 fix: 1 hai 6 meses
  xzw ea07c9648f Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 22c3c59467 fix: 1 hai 6 meses
  bill 9752353547 fix: 1 hai 6 meses
  bill ec6df839ff Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill f6bb81baf5 fix: 1 hai 6 meses
  xzw 4f9e3de8fc fix: 1 hai 6 meses
  xzw 39da6aa418 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill d65249e9ee fix: 1 hai 6 meses
  xzw 0e3391cbfd Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill 72ce95fa0b fix: 1 hai 6 meses
  xzw 06b0c8aa85 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 3c4e3fd341 fix: 1 hai 6 meses
  bill 94c418c7d6 fix: 1 hai 6 meses
  xzw 53a9eebd2f Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw ad0219e4c9 fix: 1 hai 6 meses
  bill e02452fee2 fix: 1 hai 6 meses
  xzw c0ce406495 fix: 1 hai 6 meses
  xzw 4c45c8bd54 fix: ani hai 6 meses
  xzw 23861de25a Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw ed9e5bc3f6 fix: 1 hai 6 meses
  bill 6754808c0a fix: 1 hai 6 meses
  bill f366bf503c fix: 1 hai 6 meses
  bill 31136f21d8 fix: 1 hai 6 meses
  xzw 5e3d6e1f99 fix: speed action hai 6 meses
  xzw 80a674769a Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw becb60fe08 fix: 1 hai 6 meses
  bill 8c20583d0e fix: 1 hai 6 meses
  bill 7340af2130 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill a26d76b472 fix: 1 hai 6 meses
  xzw 554315ed20 fix: monitor hide measure hai 6 meses
  xzw abd807ec5e Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 3be6097796 fix: 1 hai 6 meses
  bill 1f81323b25 fix: 1 hai 6 meses
  xzw c8227aff6a fix: 1 hai 6 meses
  xzw 4ce4718dc5 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw f9833ed2d5 fix: comeToByLatLng hai 6 meses
  bill 913afd427c fix: 1 hai 6 meses
  bill 3af99be6bf fix: 1 hai 6 meses
  bill 7133096be3 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill 6d71aea5ab fix: 1 hai 6 meses
  xzw 30c240df0d Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 36026a3382 fix: animation almost done hai 6 meses
  bill 5ebd9d04e4 fix: 1 hai 6 meses
  xzw e46844b12a Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 8ad388556a fix: 1 hai 6 meses
  bill a780ecfa6d fix: 1 hai 6 meses
  bill 9391b6d75b Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill dbd378abc4 fix: 1 hai 6 meses
  xzw 347173c8c1 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 4dc8fa8536 fix: 1 hai 6 meses
  bill 448bc0e41c fix: 1 hai 6 meses
  bill 5e42d2946e Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill 7974c03578 fix: 1 hai 6 meses
  xzw f87c6833f4 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw a3648eec62 fix: 1 hai 6 meses
  bill 2eb6eb2e51 fix: 1 hai 6 meses
  xzw 857d786586 fix: 1 hai 6 meses
  bill 775aa4341a fix: 1 hai 6 meses
  bill c5122c62c7 fix: 1 hai 6 meses
  xzw f0adfa83c9 fix: 1 hai 6 meses
  bill 38813997d4 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill f939559c15 fix: 1 hai 6 meses
  xzw 06b6637203 fix: 1 hai 6 meses
  bill 30dc6b0d1c fix: 1 hai 6 meses
  bill 4d4cbb48f8 fix: 1 hai 6 meses
  bill 2721f13a75 fix: 1 hai 6 meses
  bill fdc956b7ab fix: 1 hai 6 meses
  bill 4784721f17 fix: 1 hai 6 meses
  bill d3808eea9c fix: 1 hai 6 meses
  bill 80fdc27ef9 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill d0b13a4edd fix: 1 hai 6 meses
  xzw b420081834 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw d89303440c fix: 1 hai 6 meses
  bill 03c09950ab fix: 1 hai 6 meses
  bill 4adf62fcf7 fix: 1 hai 6 meses
  bill 5a9d47b7d9 fix: 1 hai 6 meses
  bill ba24e1ca05 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw d2ed242a5f fix: 1 hai 6 meses
  xzw 4ab530eae3 fix: 1 hai 6 meses
  bill 7b911645ac fix: 1 hai 6 meses
  bill 0c34af1535 fix: 1 hai 6 meses
  bill 773f5b92fa fix: 1 hai 6 meses
  xzw 596f182194 fix: 1 hai 6 meses
  bill fdb775ea4b fix: 1 hai 6 meses
  bill 47964a5ae9 fix: 1 hai 6 meses
  xzw ccb1231a26 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 6ca9749318 fix: 1 hai 6 meses
  xzw 95a3da0b35 fix: 1 hai 6 meses
  bill f41a71efbe fix: 1 hai 6 meses
  bill dbcba7266c fix: 1 hai 6 meses
  bill 83581eaea0 fix: 1 hai 6 meses
  xzw 6669e2ebdd 1 hai 6 meses
  bill 396fd3a9e9 fix: 1 hai 6 meses
  xzw a5ee25642e fix: 1 hai 6 meses
  bill 0640f3eb41 fix: 1 hai 6 meses
  xzw db8b95fb00 fix: hls hai 6 meses
  xzw f1bedf91c0 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw d91649ab6b fix: panoRoot hai 6 meses
  bill 017900bdc0 fix: 1 hai 6 meses
  bill 81a7763902 fix: 1 hai 6 meses
  xzw 3bb8f07a70 fix: 1 hai 6 meses
  bill f0cc1dc73f fix: 1 hai 6 meses
  bill 29f0ce2bc6 fix: 1 hai 6 meses
  bill b95aaea222 fix: 1 hai 6 meses
  bill ac5c3769bc fix: 1 hai 6 meses
  xzw 84b29eb740 fix: 1 hai 6 meses
  bill 35509916d9 fix: 1 hai 6 meses
  bill 02ee6d30dc fix: 1 hai 6 meses
  xzw cbb481ae42 fix: 1 hai 6 meses
  bill 1f3d51d42a fix: 1 hai 6 meses
  bill 919b8fcc58 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill c7457b81d7 fix: 1 hai 6 meses
  xzw e84cbaa1c0 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 42fb284a41 fix: 字幕 hai 6 meses
  bill 0e62d67b35 fix: 1 hai 6 meses
  bill 5ab6d6c6d9 fix: 1 hai 6 meses
  bill 8665e109d3 fix: 1 hai 6 meses
  bill e6e189ff1c Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill be89931947 fix: 1 hai 6 meses
  xzw e8c1e0d73f Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 784ec5a2de fix: 1 hai 6 meses
  bill 4036983580 fix: 1 hai 6 meses
  xzw f2281b5c87 fix: 1 hai 6 meses
  bill 54778d5e4b fix: 1 hai 6 meses
  bill fc2d00a3e0 fix: 1 hai 6 meses
  bill 2dfbafa73d fix: 1 hai 6 meses
  bill 0c358903b1 fix: 1 hai 6 meses
  bill 026764618f fix: 1 hai 6 meses
  bill c669ef2382 fix: 1 hai 6 meses
  xzw 21cb6b79cd fix: 1 hai 6 meses
  bill 8de9e5a356 fix: 1 hai 6 meses
  xzw 7438008ca7 fix: 1 hai 6 meses
  bill 080b8dac2b fix: 1 hai 6 meses
  bill 8f55f15915 fix: 1 hai 6 meses
  bill 1c0586a5e5 fix: 1 hai 6 meses
  xzw 787bf24462 fix: 1 hai 6 meses
  bill 9abb14c022 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  bill 0e584e0e96 fix: 1 hai 6 meses
  xzw 053ec54a73 fix: 0 hai 6 meses
  xzw 437a83c6a5 fix: 1 hai 6 meses
  bill db0c2ec4b1 fix: 1 hai 6 meses
  xzw d0f02763a9 fix: 1 hai 6 meses
  xzw 30d1721393 Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga hai 6 meses
  xzw 7a90293628 fix: 0 hai 6 meses
  bill 86b218e1d9 fix: 1 hai 6 meses
  bill 0b5f77880a feat: 制作展示页面 hai 6 meses
  bill 96c7c32bb2 feat: 制作搜索功能 hai 6 meses
  bill 8209513811 feat: 制作新功能 hai 6 meses
  bill a147272554 feat: 制作1.2新功能 hai 6 meses
  bill 7afb5d2c1d fix: 导览支持配置 hai 6 meses
  bill fd8236c144 fix: 1 hai 6 meses
  bill b429f02e46 fix: 1 hai 6 meses
  xzw 83dd245237 fix: 还没改好,动画 hai 6 meses
  bill d8ae79f85e fix: 1 hai 6 meses
  bill 7a0387ee22 fix: 1 hai 6 meses
  bill b828c8b147 fix: 1 hai 6 meses
  bill f00538fb88 fix: 1 hai 6 meses
  bill 751b7b6456 fix: 1 hai 6 meses
  bill 79378df21e fix: 1 hai 6 meses
  bill 4fec91d1b4 fix: 1 hai 7 meses
  bill de01339a15 fix: 编写文档 hai 7 meses
  bill d854b56852 fix: 制作demo hai 7 meses
  bill 36808a8834 fix: 1 hai 7 meses
  bill 767cf2b8e9 feat: 制作动画模块 hai 7 meses
  xzw c6fd3df7a1 1 hai 7 meses
Modificáronse 100 ficheiros con 47335 adicións e 14818 borrados
  1. 2 2
      .env
  2. 5 0
      .env.development
  3. 9 2
      package.json
  4. 19 7
      pnpm-lock.yaml
  5. BIN=BIN
      public/animation/Man.glb
  6. BIN=BIN
      public/animation/Soldier.glb
  7. BIN=BIN
      public/animation/Xbot.glb
  8. BIN=BIN
      public/animation/dog.glb
  9. BIN=BIN
      public/animation/kid.glb
  10. BIN=BIN
      public/animation/man--running.glb
  11. BIN=BIN
      public/animation/man--walk.glb
  12. BIN=BIN
      public/images/chrome.png
  13. BIN=BIN
      public/images/download.png
  14. BIN=BIN
      public/images/eg.png
  15. BIN=BIN
      public/images/err.png
  16. BIN=BIN
      public/images/ff.png
  17. BIN=BIN
      public/images/login-backimage.png
  18. BIN=BIN
      public/images/safar.png
  19. 1 1
      public/lib/Cesium/Cesium.js
  20. 26911 0
      public/lib/other/hls.js
  21. 14594 14317
      public/lib/potree/potree.js
  22. 1 1
      public/lib/potree/potree.js.map
  23. BIN=BIN
      public/lib/potree/resources/models/glb/monitor.glb
  24. 16 35
      public/lib/three.js/loaders/draco/draco_decoder.js
  25. 3 3
      public/lib/three.js/loaders/draco/draco_wasm_wrapper.js
  26. 150 0
      src/api/animation.ts
  27. 34 5
      src/api/constant.ts
  28. 7 5
      src/api/fuse-model.ts
  29. 5 1
      src/api/guide-path.ts
  30. 17 2
      src/api/guide.ts
  31. 3 1
      src/api/index.ts
  32. 6 6
      src/api/instance.ts
  33. 24 0
      src/api/map-tile.ts
  34. 94 30
      src/api/material.ts
  35. 65 0
      src/api/monitor.ts
  36. 3 3
      src/api/path.ts
  37. 2 2
      src/api/record.ts
  38. 40 33
      src/api/scene.ts
  39. 20 8
      src/api/setting.ts
  40. 2 0
      src/api/setup.ts
  41. 15 40
      src/api/sys.ts
  42. 15 1
      src/api/tagging-position.ts
  43. 71 21
      src/api/tagging-style.ts
  44. 35 6
      src/api/tagging.ts
  45. 2 2
      src/api/view.ts
  46. 69 5
      src/app.vue
  47. 97 0
      src/below.vue
  48. 90 0
      src/components/actions-merge/index.vue
  49. 52 44
      src/components/actions/index.vue
  50. 1 1
      src/components/bill-ui/assets/scss/_base-vars.scss
  51. 1 1
      src/components/bill-ui/assets/scss/editor/_toolbar.scss
  52. 739 3
      src/components/bill-ui/components/icon/iconfont/demo_index.html
  53. 131 3
      src/components/bill-ui/components/icon/iconfont/iconfont.css
  54. 1 1
      src/components/bill-ui/components/icon/iconfont/iconfont.js
  55. 224 0
      src/components/bill-ui/components/icon/iconfont/iconfont.json
  56. BIN=BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.ttf
  57. BIN=BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff
  58. BIN=BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff2
  59. 26 15
      src/components/bill-ui/components/input/checkbox.vue
  60. 4 3
      src/components/bill-ui/components/message/index.js
  61. 94 93
      src/components/bill-ui/components/slide/index.vue
  62. 94 0
      src/components/drawing-time-line/action.vue
  63. 99 0
      src/components/drawing-time-line/check.ts
  64. 22 0
      src/components/drawing-time-line/empty.vue
  65. 61 0
      src/components/drawing-time-line/frame.vue
  66. 125 0
      src/components/drawing-time-line/index.vue
  67. 108 0
      src/components/drawing-time/current.vue
  68. 120 0
      src/components/drawing-time/time.vue
  69. 12 0
      src/components/drawing/dec.d.ts
  70. 749 0
      src/components/drawing/hook.ts
  71. 10 0
      src/components/drawing/install-lib.ts
  72. 606 0
      src/components/drawing/math.ts
  73. 218 0
      src/components/drawing/operate.vue
  74. 128 0
      src/components/drawing/renderer.vue
  75. 174 0
      src/components/drawing/viewer.ts
  76. 11 0
      src/components/global-search/guide.vue
  77. 264 0
      src/components/global-search/index.vue
  78. 16 0
      src/components/global-search/map.vue
  79. 16 0
      src/components/global-search/measure.vue
  80. 11 0
      src/components/global-search/model.vue
  81. 10 0
      src/components/global-search/monitor.vue
  82. 10 0
      src/components/global-search/path.vue
  83. 11 0
      src/components/global-search/tagging.vue
  84. 19 0
      src/components/global-search/view.vue
  85. 23 20
      src/components/list/index.vue
  86. 135 55
      src/components/materials/index.vue
  87. 4 0
      src/components/materials/quisk.ts
  88. 29 0
      src/components/monitor-exit/index.vue
  89. 12 11
      src/components/path/list.vue
  90. 1 1
      src/components/path/sign.vue
  91. 36 0
      src/components/right-menu/index.ts
  92. 44 0
      src/components/right-menu/index.vue
  93. 70 0
      src/components/subtitle/index.vue
  94. 3 1
      src/components/tagging/list.vue
  95. 47 21
      src/components/tagging/sign-new.vue
  96. 90 0
      src/components/view-setting/index.vue
  97. 32 6
      src/env/index.ts
  98. 66 0
      src/hook/ids.ts
  99. 149 0
      src/hook/use-fly.ts
  100. 0 0
      src/hook/use-pixel.ts

+ 2 - 2
.env

@@ -1,5 +1,5 @@
 VITE_LASER_HOST=
 VITE_LASER_OSS=/laser-data
 VITE_OSS=/oss
-VITE_PANO_OSS=/laser-data
-
+VITE_PANO_OSS=/oss
+VITE_MAP_PLATFORM=jm

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+VITE_LASER_HOST=
+VITE_LASER_OSS=/laser-data
+VITE_OSS=/oss
+VITE_PANO_OSS=/oss
+VITE_MAP_PLATFORM=gaode

+ 9 - 2
package.json

@@ -13,23 +13,30 @@
   },
   "dependencies": {
     "@ant-design/icons-vue": "^7.0.1",
-    "ant-design-vue": "^4.2.6",
+    "@types/three": "^0.169.0",
+    "ant-design-vue": "4.1.0",
     "axios": "^0.27.2",
     "body-parser": "^1.20.3",
     "coordtransform": "^2.1.2",
     "express": "^4.21.2",
     "i18n": "^0.15.1",
+    "dayjs": "^1.11.13",
+    "js-base64": "^3.7.8",
+    "konva": "^9.3.18",
     "less": "^4.1.3",
     "mitt": "^3.0.0",
     "simaqcore": "^1.2.0",
     "swiper": "^11.1.15",
+    "three": "^0.169.0",
+    "uuid": "^11.0.2",
     "vite-plugin-mkcert": "^1.10.1",
     "vue": "3.2.47",
     "vue-cropper": "1.0.2",
     "vue-i18n": "^11.1.1",
     "vue-router": "^4.1.3",
     "vuedraggable": "^4.1.0",
-    "xlsx": "^0.18.5"
+    "xlsx": "^0.18.5",
+    "vue-konva": "3.2.0"
   },
   "devDependencies": {
     "@types/node": "^18.6.5",

+ 19 - 7
pnpm-lock.yaml

@@ -3,8 +3,9 @@ lockfileVersion: 5.4
 specifiers:
   '@ant-design/icons-vue': ^7.0.1
   '@types/node': ^18.6.5
+  '@types/three': ^0.169.0
   '@vitejs/plugin-vue': ^3.0.0
-  ant-design-vue: ^4.2.6
+  ant-design-vue: 4.1.0
   axios: ^0.27.2
   body-parser: ^1.20.3
   coordtransform: ^2.1.2
@@ -15,7 +16,9 @@ specifiers:
   sass: ^1.54.3
   simaqcore: ^1.2.0
   swiper: ^11.1.15
+  three: ^0.169.0
   typescript: ^4.6.4
+  uuid: ^11.0.2
   vite: ^3.0.0
   vite-plugin-mkcert: ^1.10.1
   vue: 3.2.47
@@ -28,7 +31,8 @@ specifiers:
 
 dependencies:
   '@ant-design/icons-vue': 7.0.1_vue@3.2.47
-  ant-design-vue: 4.2.6_vue@3.2.47
+  '@types/three': 0.169.0
+  ant-design-vue: 4.1.0_vue@3.2.47
   axios: 0.27.2
   body-parser: 1.20.3
   coordtransform: 2.1.2
@@ -317,8 +321,8 @@ packages:
       '@parcel/watcher-win32-x64': 2.5.1
     optional: true
 
-  /@simonwep/pickr/1.8.2:
-    resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
+  /@types/three/0.169.0:
+    resolution: {integrity: sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==}
     dependencies:
       core-js: 3.42.0
       nanopop: 2.4.2
@@ -1280,6 +1284,10 @@ packages:
   /is-what/3.14.1:
     resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
 
+  /js-base64/3.7.8:
+    resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
+    dev: false
+
   /js-tokens/4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
     dev: false
@@ -1321,10 +1329,10 @@ packages:
     dependencies:
       sourcemap-codec: 1.4.8
 
-  /magic-string/0.30.17:
-    resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+  /magic-string/0.30.18:
+    resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.5.0
+      '@jridgewell/sourcemap-codec': 1.5.5
     dev: true
 
   /make-dir/2.1.0:
@@ -1722,6 +1730,10 @@ packages:
     engines: {node: '>= 4.7.0'}
     dev: false
 
+  /three/0.169.0:
+    resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==}
+    dev: false
+
   /throttle-debounce/5.0.2:
     resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
     engines: {node: '>=12.22'}

BIN=BIN
public/animation/Man.glb


BIN=BIN
public/animation/Soldier.glb


BIN=BIN
public/animation/Xbot.glb


BIN=BIN
public/animation/dog.glb


BIN=BIN
public/animation/kid.glb


BIN=BIN
public/animation/man--running.glb


BIN=BIN
public/animation/man--walk.glb


BIN=BIN
public/images/chrome.png


BIN=BIN
public/images/download.png


BIN=BIN
public/images/eg.png


BIN=BIN
public/images/err.png


BIN=BIN
public/images/ff.png


BIN=BIN
public/images/login-backimage.png


BIN=BIN
public/images/safar.png


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
public/lib/Cesium/Cesium.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 26911 - 0
public/lib/other/hls.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 14594 - 14317
public/lib/potree/potree.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
public/lib/potree/potree.js.map


BIN=BIN
public/lib/potree/resources/models/glb/monitor.glb


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 16 - 35
public/lib/three.js/loaders/draco/draco_decoder.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 3 - 3
public/lib/three.js/loaders/draco/draco_wasm_wrapper.js


+ 150 - 0
src/api/animation.ts

@@ -0,0 +1,150 @@
+import axios from "./instance";
+import { params } from "@/env";
+import {
+  AM_MODEL_LIST,
+  INSERT_AM_MODEL,
+  UPDATE_AM_MODEL,
+  DELETE_AM_MODEL,
+} from "./constant";
+
+type ServiceAnimationModel = {
+  key?: string
+  id: string;
+  title: string;
+  url: string;
+  showTitle: boolean;
+  fontSize: number;
+  globalVisibility: boolean;
+  visibilityRange: number;
+  frames: string
+  actions: string
+  subtitles: string
+  paths: string
+  mat?: string
+}
+
+export type AnimationModelAction = {
+  amplitude: number;
+  speed: number;
+  time: number;
+  duration: number;
+  id: string;
+  key: string;
+  name: string;
+};
+export type AnimationModelSubtitle = {
+  content: string;
+  duration: number;
+  time: number;
+  id: string;
+  background: string;
+  name: string;
+};
+export type AnimationModelFrame = {
+  time: number;
+  id: string;
+  name: string;
+  mat?: {
+    position?: SceneLocalPos;
+    scale?: number;
+    rotation?: SceneLocalPos;
+    originPosition?: SceneLocalPos;
+  };
+  duration?: number;
+};
+export type AnimationModelPath = {
+  reverse: boolean;
+  pathId?: string;
+  time: number;
+  duration: number;
+  id: string;
+  name: string;
+};
+
+export interface AnimationModel {
+  key?: string
+  id: string;
+  title: string;
+  url: string;
+  showTitle: boolean;
+  fontSize: number;
+  globalVisibility: boolean;
+  visibilityRange: number;
+  frames: AnimationModelFrame[];
+  actions: AnimationModelAction[];
+  subtitles: AnimationModelSubtitle[];
+  paths: AnimationModelPath[];
+  mat?: {
+    position?: SceneLocalPos;
+    quaAtPath?: any
+    scale?: number;
+    rotation?: SceneLocalPos & {w: number};
+    quaternion?: SceneLocalPos & {w: number};
+    originPosition?: SceneLocalPos;
+  };
+}
+
+export type AnimationModels = AnimationModel[];
+
+const serviceToLocal = (serviceAM: ServiceAnimationModel): AnimationModel => ({
+  ...serviceAM,
+  frames: JSON.parse(serviceAM.frames),
+  actions: JSON.parse(serviceAM.actions),
+  subtitles: JSON.parse(serviceAM.subtitles),
+  paths: JSON.parse(serviceAM.paths),
+  mat: serviceAM.mat && JSON.parse(serviceAM.mat)
+});
+
+const localToService = (am: AnimationModel): ServiceAnimationModel => ({
+  ...am,
+  frames: JSON.stringify(am.frames),
+  actions: JSON.stringify(am.actions),
+  subtitles: JSON.stringify(am.subtitles),
+  paths: JSON.stringify(am.paths),
+  mat: am.mat ? JSON.stringify(am.mat) : undefined
+});
+
+export const fetchAnimationModels = async () => {
+  const ams = await axios.get<ServiceAnimationModel[]>(AM_MODEL_LIST, {
+    params: { fusionId: params.caseId },
+  });
+  return ams.map(serviceToLocal);
+};
+
+export const fetchAnimationActions = async () => {
+  return [
+    { id: "1", action: "Walk", title: "走", url: "" },
+    { id: "2", action: "Run", title: "跑", url: "" },
+    { id: "3", action: "Climb", title: "爬", url: "" },
+    { id: "2", action: "JumpUp", title: "向上跳", url: "" },
+    { id: "3", action: "JumpDown", title: "向下跳", url: "" },
+    { id: "2", action: "TurnLeft", title: "左转", url: "" },
+    { id: "3", action: "TurnRight", title: "右转", url: "" },
+    { id: "3", action: "FallForward", title: "向前倒地", url: "" },
+    { id: "3", action: "FallBackward", title: "向后倒地", url: "" },
+  ];
+};
+
+export const postInsertAnimationModel = async (am: AnimationModel) => {
+  const addData = {
+    ...localToService(am),
+    fusionId: params.caseId,
+    id: undefined,
+  };
+  console.log('add', addData)
+  const serviceData = await axios.post<ServiceAnimationModel>(
+    INSERT_AM_MODEL,
+    addData
+  );
+  return serviceToLocal(serviceData);
+};
+
+export const postUpdateAnimationModel = async (guide: AnimationModel) => {
+  console.log('set', guide)
+  const data = await axios.post<ServiceAnimationModel>(UPDATE_AM_MODEL, { ...localToService(guide) });
+  return {...guide, id: data.id}
+};
+
+export const postDeleteAnimationModel = (id: AnimationModel["id"]) => {
+  return axios.post<undefined>(DELETE_AM_MODEL, { id: Number(id) });
+};

+ 34 - 5
src/api/constant.ts

@@ -3,7 +3,8 @@ import { lang, ui18n } from '@/lang';
 
 export enum ResCode {
   TOKEN_INVALID = 4008,
-  UN_AUTH = 4010,
+  UN_AUTH = 40111,
+  UN_EDIT_AUTH = 40110,
   SUCCESS = 0
 }
 
@@ -17,8 +18,9 @@ export const UPLOAD_HEADS = {
 
 export const USER_INFO = `${namespace}/web/user/getUserInfo`;
 
-export const CASE_INFO = `${namespace}/case/getInfo`
-export const CASE_FIRE_INFO = `${namespace}/caseInquestInfo/info`
+export const CASE_INFO = `${namespace}/caseFusion/info`
+export const UPDATE_CASE_INFO = `${namespace}/caseFusion/updateInfo`
+// export const CASE_FIRE_INFO = `${namespace}/caseInquestInfo/info`
 
 // 校验密码
 export const AUTH_PWD = `${namespace}/web/fireProject/getDetailWithoutAuth`
@@ -29,7 +31,7 @@ export const FUSE_INSERT_MODEL = `${namespace}/caseFusion/add`
 export const FUSE_UPDATE_MODEL = `${namespace}/caseFusion/update`
 export const FUSE_DELETE_MODEL = `${namespace}/caseFusion/delete`
 // 场景列表
-export const SCENE_LIST_ALL = `${namespace}/api/scene/list`
+export const SCENE_LIST_ALL = `/service/manage/scene/list`
 export const MODEL_LIST = `${namespace}/case/sceneList`
 export const MODEL_SIGN = `${namespace}/model/getInfo`
 export const SYNC_INFO = `${namespace}/caseLive/getTakeLookRoom`;
@@ -55,7 +57,9 @@ export const DELETE_TAGGING_POINT = `${namespace}/caseTagPoint/delete`
 
 // 标签样式类型列表
 export const TAGGING_STYLE_LIST = `${namespace}/edit/hotIcon/list`
+export const TAGGING_STYLE_TREE = `${namespace}/edit/hotIcon/treeList`
 export const INSERT_TAGGING_STYLE = `${namespace}/edit/hotIcon/add`
+
 export const DELETE_TAGGING_STYLE = `${namespace}/edit/hotIcon/delete`
 
 // 测量线
@@ -76,6 +80,13 @@ export const INSERT_GUIDE_PATH = `${namespace}/fusionGuidePath/add`
 export const UPDATE_GUIDE_PATH = `${namespace}/fusionGuidePath/update`
 export const DELETE_GUIDE_PATH = `${namespace}/fusionGuidePath/delete`
 
+
+// 监控
+export const GUIDE_MONITOR_LIST = `${namespace}/monitor/allList`
+export const UPDATE_MONITOR = `${namespace}/monitor/update`
+export const INSERT_MONITOR = `${namespace}/monitor/update`
+export const DELETE_MONITOR = `${namespace}/monitor/delete`
+
 // 屏幕录制
 export const RECORD_LIST = `${namespace}/caseVideoFolder/allList`
 export const RECORD_STATUS = `${namespace}/caseVideo/uploadAddVideoProgress`
@@ -99,6 +110,8 @@ export const UPDATE_SETTING = `${namespace}/caseSettings/saveOrUpdate`
 
 // 卷宗类型
 export const FOLDER_TYPE_LIST = `${namespace}/caseFilesType/allList`
+// 卷宗类型
+export const TAGGING_TYPE_LIST = `${namespace}/caseFilesType/getByParentId?parentId=49`
 
 // 卷宗
 // export const FLODER_LIST = `${namespace}/caseFiles/allList`
@@ -112,4 +125,20 @@ export const UPLOAD_FILE = `${namespace}/upload/file`
 export const MATERIAL_PAG = `${namespace}/dictFile/pageList/media-library`
 export const ADD_MATERIAL = `${namespace}/upload/fileNew`
 export const DEL_MATERIAL = `${namespace}/dictFile/del/media-library`
-export const MATERIAL_GROUP_LIST = `${namespace}/dict/getByKey/media-library`
+export const MATERIAL_GROUP_LIST = `${namespace}/dict/getByKey/media-library`
+// export const MATERIAL_PAG = `/service/manage/dictFile/pageList/media-library`
+// export const ADD_MATERIAL = `/service/manage/common/upload/fileNew`
+// export const DEL_MATERIAL = `/service/manage/dictFile/del/media-library`
+// export const MATERIAL_GROUP_LIST = `/service/manage/dict/getByKey/media-library`
+export const MATERIAL_TA_GROUP_LIST = `/service/manage/dict/getByUseType/trace_evidence`
+export const SYNC_MATERIAL = `/service/manage/caseFusion/refreshTraceEvidenceInfoList/`
+
+
+// 动画模块
+export const AM_MODEL_LIST = `${namespace}/caseAnimation/list`
+export const INSERT_AM_MODEL = `${namespace}/caseAnimation/addOrUpdate`
+export const UPDATE_AM_MODEL = `${namespace}/caseAnimation/addOrUpdate`
+export const DELETE_AM_MODEL = `${namespace}/caseAnimation/delete`
+
+
+export const MAP_TILE_LIST = `${namespace}/notAuth/getMapConfig`

+ 7 - 5
src/api/fuse-model.ts

@@ -141,25 +141,27 @@ const localToService = (
 export type FuseModels = FuseModel[];
 
 export const fetchFuseModels = async () => {
-  const serviceModels = await axios.get<ServiceFuseModel[]>(FUSE_MODEL_LIST, {
-    params: { caseId: params.caseId },
+  let serviceModels = await axios.get<ServiceFuseModel[]>(FUSE_MODEL_LIST, {
+    params: { fusionId: params.caseId },
   });
-  console.log('===>', serviceModels.map((item, index) => serviceToLocal(item, index == 0)))
+  // serviceModels = [serviceModels[4]]
+
   return serviceModels.map((item, index) => serviceToLocal(item, index == 0));
 };
 
-export const postAddFuseModel = async (model: FuseModel) => {
+export const postAddFuseModel = async (model: FuseModel, attach: any = {}) => {
   const upload = localToService(model);
   const serviceModel = await axios<ServiceFuseModel>({
     url: FUSE_INSERT_MODEL,
     method: "POST",
     data: {
-      caseId: params.caseId,
+      fusionId: params.caseId,
       modelId: model.modelId,
       hide: upload.hide,
       transform: upload.transform,
       opacity: upload.opacity,
       bottom: upload.bottom,
+      ...attach,
     },
   });
   return serviceToLocal(serviceModel);

+ 5 - 1
src/api/guide-path.ts

@@ -19,6 +19,8 @@ interface ServiceGuidePath {
   speed: number
   panoInfo?: string
   cover: string
+
+  playAnimation?: boolean
 }
 
 export interface GuidePath {
@@ -36,6 +38,8 @@ export interface GuidePath {
   sort: number
   speed: number
   cover: string
+
+  playAnimation?: boolean
 }
 
 export type GuidePaths = GuidePath[]
@@ -64,7 +68,7 @@ export const fetchGuidePaths = async (guideId: Guide['id']) => {
 }
 
 export const postAddGuidePath = async (path: GuidePath) => {
-  const addData = { ...localToService(path), caseId: params.caseId, guidePathId: undefined }
+  const addData = { ...localToService(path), fusionId: params.caseId, guidePathId: undefined }
    const serviceData = await axios.post<ServiceGuidePath>(INSERT_GUIDE_PATH, addData)
    return serviceToLocal(serviceData)
 }

+ 17 - 2
src/api/guide.ts

@@ -11,6 +11,10 @@ interface ServiceGuide {
   fusionGuideId: number
   cover: string
   title: string
+  showTaggings?: boolean
+  showMeasure?: boolean
+  showMonitor?: boolean
+  showPath?: boolean
 }
 
 export interface Guide {
@@ -18,27 +22,38 @@ export interface Guide {
   cover: string
   title: string
   recoveryContent?: string
+  changeAnimationStatus?: boolean
+
+  showTagging: boolean
+  showMeasure: boolean
+  showMonitor: boolean
+  showPath: boolean
 }
 
 export type Guides = Guide[]
 
 const serviceToLocal = (serviceGuide: ServiceGuide): Guide => ({
+  showMeasure: true,
+  showMonitor: true,
+  showPath: true,
   ...serviceGuide,
+  showTagging: serviceGuide.showTaggings === undefined ? true : serviceGuide.showTaggings,
   id: serviceGuide.fusionGuideId.toString(),
 })
 
 const localToService = (guide: Guide): ServiceGuide => ({
   ...guide,
+  showTaggings: guide.showTagging,
   fusionGuideId: Number(guide.id),
 })
 
 export const fetchGuides = async () => {
-  const guides = await axios.get<ServiceGuide[]>(GUIDE_LIST, { params: { caseId: params.caseId } })
+  const guides = await axios.get<ServiceGuide[]>(GUIDE_LIST, { params: { fusionId: params.caseId } })
   return guides.map(serviceToLocal)
 }
 
 export const postAddGuide = async (guide: Guide) => {
-  const addData = { ...localToService(guide), caseId: params.caseId, fusionGuideId: undefined }
+  const addData = { ...localToService(guide), fusionId: params.caseId, fusionGuideId: undefined }
    const serviceData = await axios.post<ServiceGuide>(INSERT_GUIDE, addData)
    return serviceToLocal(serviceData)
 }

+ 3 - 1
src/api/index.ts

@@ -35,4 +35,6 @@ export * from './view'
 export * from './folder-type'
 export * from './floder'
 export * from './setting'
-export * from './path'
+export * from './path'
+export * from './animation'
+export * from './monitor'

+ 6 - 6
src/api/instance.ts

@@ -47,10 +47,10 @@ addReqErrorHandler((err) => {
   // Message.error(err.message)
   console.error(err);
   hideLoad();
-  gotoLogin();
 });
 
 addResErrorHandler((response, data) => {
+  console.log('hahah')
   if (response && response.status !== 200) {
     Message.error(response.statusText);
   } else if (data) {
@@ -59,12 +59,12 @@ addResErrorHandler((response, data) => {
         ? ResCodeDesc[data.code]
         : data?.message || data?.msg;
     if (data.code === ResCode.TOKEN_INVALID) {
-      gotoLogin();
+      // gotoLogin();
     } else if (data.code === ResCode.UN_AUTH) {
-      Dialog.alert({content: msg, okText: ui18n.t('sys.ok')}).then(() => {
-        gotoLogin();
-      })
-      throw msg
+      // Dialog.alert({content: msg, okText: '我知道了'}).then(() => {
+      //   gotoLogin();
+      // })
+      // throw msg
     } else {
       Message.error(msg || ui18n.t('sys.serviceErr'));
     }

+ 24 - 0
src/api/map-tile.ts

@@ -0,0 +1,24 @@
+import axios from "./instance";
+import { MAP_TILE_LIST } from "./constant";
+
+type ServiceMapTile = {
+  id: number;
+  name: string,
+  mapUrl: string;
+  coord: string;
+};
+
+export type MapTile = {
+  id: number;
+  name: string,
+  mapUrls: {tempUrl: string, maximumLevel: number}[];
+  coord: string;
+};
+
+export const fetchMapTiles = async () => {
+  const items = await axios.get<ServiceMapTile[]>(MAP_TILE_LIST);
+  return items.map((item) => ({
+    ...item,
+    mapUrls: JSON.parse(item.mapUrl),
+  }));
+};

+ 94 - 30
src/api/material.ts

@@ -5,37 +5,55 @@ import {
   DEL_MATERIAL,
   MATERIAL_GROUP_LIST,
   MATERIAL_PAG,
+  MATERIAL_TA_GROUP_LIST,
+  SYNC_MATERIAL,
   UPLOAD_HEADS,
 } from "./constant";
 import axios from "./instance";
+import { params } from "@/env";
 
 type ServiceMaterialGroup = {
   dictKey: string;
   dictName: string;
+  useType: string;
   id: number;
+  dictIconList?: {
+    iconUrl: string;
+  }[];
 };
 type ServiceMaterial = {
   createTime: string;
   dictId: number;
-  status: number,
+  status: number;
   dictName: string;
   fileFormat: string;
   fileName: string;
   fileSize: string;
-  fileType: string;
+  fileType: FileType;
   fileUrl: string;
   id: number;
   name: string;
   newFileName: string;
   typeKey: string;
+  content?: string
   updateTime: string;
   uploadId: number;
 };
 export type MaterialGroup = {
   id: number;
   name: string;
+  useType: string;
+  icons: string[];
 };
 
+export enum FileType {
+  IMAGE = 0,
+  VIDEO = 1,
+  MUSIC = 2,
+  MODEL = 3,
+  OTHER = 4,
+  DOC = 5
+}
 export type Material = {
   id: number;
   name: string;
@@ -46,46 +64,93 @@ export type Material = {
   status: number;
   group: string;
   uploadId?: number;
+  isSystem?: number;
+  dictId?: number
   modelId?: number;
+  fileType: FileType,
+  dictIconList?: ({id :number})[]
+  content?: {
+    collectedTime: string;
+    createAccount: string;
+    feature: string;
+    leftPosition: string;
+    collectionModeName: string,
+    status: number;
+    title: string;
+  };
 };
 
 export type MaterialPageProps = PagingRequest<
-  Partial<Material> & { groupIds: number[], formats: string[] }
+  Partial<Material> & {
+    groupIds: number[];
+    formats: string[];
+    useType?: string;
+  }
 >;
 export const fetchMaterialPage = async (params: MaterialPageProps) => {
-  //
-  const material = await axios.post<PagingResult<ServiceMaterial[]>>(MATERIAL_PAG, {
-    pageNum: params.pageNum,
-    pageSize: params.pageSize,
-    name: params.name,
-    dictIds: params.groupIds,
-    fileFormats: params.formats
-  });
+  const material = await axios.post<PagingResult<ServiceMaterial[]>>(
+    MATERIAL_PAG,
+    {
+      pageNum: params.pageNum,
+      pageSize: params.pageSize,
+      name: params.name,
+      dictIds: params.groupIds,
+      fileFormats: params.formats,
+      // useType: params.useType
+    }
+  );
   const nm = {
     ...material,
-    list: material.list.map((item): Material => ({
-      id: item.id,
-      name: item.name,
-      format: item.fileFormat,
-      url: item.fileUrl,
-      size: Number(item.fileSize),
-      groupId: item.dictId,
-      status: item.status,
-      group: item.dictName,
-      uploadId: item.uploadId
-    }))
-  }
-  
+    list: material.list.map(
+      (item): Material => ({
+        ...item,
+        id: item.id,
+        name: item.name,
+        format: item.fileFormat,
+        url: item.fileUrl,
+        size: Number(item.fileSize),
+        groupId: item.dictId,
+        status: item.status,
+        group: item.dictName,
+        uploadId: item.uploadId,
+        content: item.content && JSON.parse(item.content)
+      })
+    ),
+  };
+
   return nm;
 };
 
-export const fetchMaterialGroups = async () => {
-  return (await axios.get<ServiceMaterialGroup[]>(MATERIAL_GROUP_LIST)).map(
-    (item) => ({
+export const fetchMaterialGroups = async (useType?: string) => {
+  if (useType !== "trace_evidence") {
+    return (await axios.get<ServiceMaterialGroup[]>(MATERIAL_GROUP_LIST)).map(
+      (item) => ({
+        name: item.dictName,
+        useType: item.useType,
+        key: item.dictKey,
+        id: item.id,
+        icons: item.dictIconList
+          ? item.dictIconList.map((item) => item.iconUrl)
+          : [],
+      })
+    ) as MaterialGroup[];
+  } else {
+    return (
+      await axios.get<ServiceMaterialGroup[]>(MATERIAL_TA_GROUP_LIST)
+    ).map((item) => ({
       name: item.dictName,
+      useType: item.useType,
+      key: item.dictKey,
       id: item.id,
-    })
-  ) as MaterialGroup[];
+      icons: item.dictIconList
+        ? item.dictIconList.map((item) => item.iconUrl)
+        : [],
+    })) as MaterialGroup[];
+  }
+};
+
+export const syncMaterialAll = async () => {
+  await axios.get(SYNC_MATERIAL + params.caseId);
 };
 
 export const addMaterial = (file: File) => {
@@ -95,7 +160,6 @@ export const addMaterial = (file: File) => {
     data: jsonToForm({ file }),
     headers: { ...UPLOAD_HEADS },
   });
-  
 };
 
 export const delMaterial = (id: Material["id"]) => {

+ 65 - 0
src/api/monitor.ts

@@ -0,0 +1,65 @@
+import axios from "./instance";
+import { params } from "@/env";
+import {
+  GUIDE_MONITOR_LIST,
+  UPDATE_MONITOR,
+  DELETE_MONITOR,
+  INSERT_MONITOR,
+} from "./constant";
+
+import type { Guide } from "./guide";
+
+interface ServiceMonitor {
+  id: string;
+  title: string;
+  content: string;
+}
+
+export interface Monitor {
+  id: string;
+  title: string;
+  content: string;
+}
+
+export type Monitors = Monitor[];
+
+const serviceToLocal = (servicePath: ServiceMonitor): Monitor => ({
+  ...servicePath,
+});
+
+const localToService = (path: Monitor): ServiceMonitor => ({
+  ...path,
+});
+
+export const fetchMonitors = async () => {
+  return [
+    {
+      id: 1,
+      title: "室内监控",
+      content: "",
+    },
+    {
+      id: 2,
+      title: "室内监控",
+      content: "",
+    },
+  ];
+
+  const monitors = await axios.get<ServiceMonitor[]>(GUIDE_MONITOR_LIST);
+  return monitors.map(serviceToLocal);
+};
+export const postInsertMonitor = async (monitor: Monitor) => {
+  const smonitor = await axios.post<ServiceMonitor>(INSERT_MONITOR, {
+    ...localToService(monitor),
+    fusionId: params.caseId,
+  });
+  return serviceToLocal(smonitor);
+};
+
+export const postUpdateMonitor = async (monitor: Monitor) => {
+  return axios.post<undefined>(UPDATE_MONITOR, { ...localToService(monitor) });
+};
+
+export const postDeleteMonitor = (id: Monitor["id"]) => {
+  return axios.post<undefined>(DELETE_MONITOR, { id: Number(id) });
+};

+ 3 - 3
src/api/path.ts

@@ -44,7 +44,7 @@ const localToService = (path: Path): ServerPath => ({
 
 export const fetchPaths = async () => {
   const staggings = await axios.get<ServerPath[]>(PATH_LIST, {
-    params: { caseId: params.caseId },
+    params: { fusionId: params.caseId },
   });
   return staggings.map(serviceToLocal);
 };
@@ -52,7 +52,7 @@ export const fetchPaths = async () => {
 export const postAddPath = async (path: Path) => {
   const stagging = await axios.post<ServerPath>(INSERT_PATH, {
     ...localToService(path),
-    caseId: params.caseId,
+    fusionId: params.caseId,
   });
   return serviceToLocal(stagging);
 };
@@ -60,7 +60,7 @@ export const postAddPath = async (path: Path) => {
 export const postUpdatePath = (path: Path) => {
   return axios.post<undefined>(UPDATE_PATH, {
     ...localToService(path),
-    caseId: params.caseId,
+    fusionId: params.caseId,
   });
 };
 

+ 2 - 2
src/api/record.ts

@@ -50,7 +50,7 @@ const toService = (record: Record, isUpdate = true): PartialProps<ServiceRecord,
 export type Records = Record[]
 
 export const fetchRecords = async () => {
-  const data = await axios.get<ServiceRecord[]>(RECORD_LIST, { params: { caseId: params.caseId } })
+  const data = await axios.get<ServiceRecord[]>(RECORD_LIST, { params: { fusionId: params.caseId } })
   return data.map(toLocal)
 }
 
@@ -82,7 +82,7 @@ export const postMegerRecord = (files: File[], recordId?: Record['id']) => {
     headers: { ...UPLOAD_HEADS },
     data: jsonToForm({
       ...(recordId ? { folderId: recordId && Number(recordId) } : {}),
-      caseId: params.caseId,
+      fusionId: params.caseId,
       files
     })
   })

+ 40 - 33
src/api/scene.ts

@@ -1,7 +1,8 @@
-import axios from './instance'
-import { MODEL_LIST, MODEL_SIGN, SCENE_LIST_ALL, SYNC_INFO } from './constant'
-import { params } from '@/env'
-import { lang } from '@/lang'
+import axios from "./instance";
+import { MODEL_LIST, MODEL_SIGN, SCENE_LIST_ALL, SYNC_INFO } from "./constant";
+import { params } from "@/env";
+import { caseProject } from "@/store";
+import { lang } from "@/lang";
 
 export enum SceneStatus {
   DEL = -1,
@@ -37,46 +38,52 @@ export interface Scene {
   raw: any,
   model3dgsUrl: string;
   modelShpUrl: string;
-  modelId: number
-  modelObjUrl: string
-  modelSize: number
-  status: SceneStatus
-  modelTitle: string
-  name: string
-  num: string
-  sceneName: string
-  snCode: string
-  thumb: string
-  title: string
-  type: SceneType
+  modelId: number;
+  modelObjUrl: string;
+  isObj: number
+  modelSize: number;
+  status: SceneStatus;
+  modelTitle: string;
+  name: string;
+  num: string;
+  sceneName: string;
+  snCode: string;
+  thumb: string;
+  title: string;
+  type: SceneType;
 }
 
-export type Scenes = Scene[]
+export type Scenes = Scene[];
 
 const toLocalScene = (scene: Scene) => ({
   ...scene,
   num: scene.type === SceneType.SWMX ? scene.modelId.toString() : scene.num,
   name: scene.name || scene.sceneName || scene.modelTitle,
-})
-
-
+});
 
 export const getSyncSceneInfo = async (scene: Scene) => {
-  return (await axios.post<string>(SYNC_INFO, { caseId: params.caseId, num: scene.raw.num }));
+  return await axios.post<string>(SYNC_INFO, {
+    fusionId: params.caseId,
+    num: scene.raw.num,
+  });
 };
 
 export const fetchScenes = async () => {
-  const scenes = await axios.get<Scenes>(MODEL_LIST, { params: { caseId: params.caseId } })
-  return scenes.map(toLocalScene)
-}
-
-export const fetchScenesAll = async (params: {numList: Scene['num'][], type: SceneType}) => {
-  const scenes = await axios.post<Scenes>(SCENE_LIST_ALL, params)
-  return scenes.map(toLocalScene)
-}
+  return caseProject.value?.sceneVoList || [];
+};
 
+export const fetchScenesAll = async (params: {
+  numList?: string[];
+  isObj: number;
+  pageNum: number;
+  pageSize: number;
+}) => {
+  const data = await axios.post<{ total: number, list: Scenes  }>(SCENE_LIST_ALL, {...params, sceneStatus: -2});
+  data.list = data.list.map(toLocalScene);
+  return data
+};
 
-export const fetchScene = async (modelId: Scene['modelId']) => {
-  const scene = await axios.get<Scene>(MODEL_SIGN, { params: { modelId } })
-  return toLocalScene(scene)
-}
+export const fetchScene = async (modelId: Scene["modelId"]) => {
+  const scene = await axios.get<Scene>(MODEL_SIGN, { params: { modelId } });
+  return toLocalScene(scene);
+};

+ 20 - 8
src/api/setting.ts

@@ -2,6 +2,7 @@ import { GET_SETTING, UPDATE_SETTING } from "./constant";
 import defaultCover from "@/assets/cover.png";
 import { params } from "@/env";
 import axios from "./instance";
+import { fetchMapTiles } from "./map-tile";
 
 
 type ServeSetting = {
@@ -9,11 +10,14 @@ type ServeSetting = {
   pose?: string;
   cover?: string;
   mapType?: 'satellite' | 'standard',
-  back?: string;
+  back?: string | null;
+  mapId?: number | null
 };
 
 export type Setting = {
   id?: string;
+  title?: string
+  initGPS?: string
   pose?: {
     position: SceneLocalPos;
     target: SceneLocalPos;
@@ -26,37 +30,45 @@ export type Setting = {
   };
   mapType: 'satellite' | 'standard',
   cover: string;
-  back: string;
+  back?: string | null;
   fov?: number;
   openCompass?: boolean;
+  mapId?: number | null
 };
 
 const toLocal = (serviceSetting: ServeSetting): Setting => ({
   id: serviceSetting.settingsId,
   pose: serviceSetting.pose && JSON.parse(serviceSetting.pose),
   cover: serviceSetting.cover || (params.static +"/profile/fusion/default/images/cover.png"),
-  back: serviceSetting.back || "map",
+  back: serviceSetting.back || undefined,
   mapType: serviceSetting.mapType || 'satellite',
+  mapId: serviceSetting.mapId || undefined,
 });
 
 const toService = (setting: Setting): ServeSetting => ({
   settingsId: setting.id,
+  mapId: setting.mapId  || null,
   pose: setting.pose && JSON.stringify(setting.pose),
   cover: setting.cover,
-  back: setting.back,
+  back: setting.back  || null,
   mapType: setting.mapType,
 });
 
 export const fetchSetting = async () => {
-  const data = await axios.get<ServeSetting[]>(GET_SETTING, {
-    params: { caseId: params.caseId },
+  let data = await axios.get<ServeSetting[]>(GET_SETTING, {
+    params: { fusionId: params.caseId },
   });
-  return toLocal(data[0] || {});
+  const tData = toLocal(data[0] || {})
+  if (!tData.back && !tData.mapId) {
+    const tiles = await fetchMapTiles()
+    tData.mapId = tiles[0].id
+  }
+  return tData
 };
 
 export const updateSetting = async (setting: Setting) => {
   await axios.post(UPDATE_SETTING, {
-    caseId: params.caseId,
+    fusionId: params.caseId,
     ...toService(setting),
   });
 };

+ 2 - 0
src/api/setup.ts

@@ -154,6 +154,7 @@ export const axiosFactory = () => {
 
   axiosRaw.interceptors.response.use(
     (response: AxiosResponse<ResData<any>>) => {
+      
       for (const hook of axiosConfig.hook) {
         hook.after && hook.after(response.config)
       }
@@ -170,6 +171,7 @@ export const axiosFactory = () => {
         if (response.data.code === ResCode.TOKEN_INVALID) {
           delToken()
         }
+        // console.error(response?.data?.message)
         throw new Error(response?.data?.message)
       } else {
         return response.data.data

+ 15 - 40
src/api/sys.ts

@@ -1,7 +1,8 @@
-import { UPLOAD_FILE, UPLOAD_HEADS, CASE_INFO, AUTH_PWD, CASE_FIRE_INFO } from "./constant";
+import { UPLOAD_FILE, UPLOAD_HEADS, CASE_INFO, AUTH_PWD, UPDATE_CASE_INFO } from "./constant";
 import { axios } from "./instance";
 import { jsonToForm } from "@/utils";
 import { params } from "@/env";
+import { Scene } from "./scene";
 
 type UploadFile = LocalFile | string;
 
@@ -24,50 +25,24 @@ export const uploadFile = async (file: UploadFile, suffix = ".png") => {
   }
 };
 
-export enum FireStatus {
-  incomplete = 0,
-  complete = 1,
-}
-
-export type FireProject = {
-  "id": number,
-  "caseId": number,
-  "commandTime": string,
-  "alarmTime": string,
-  "alarmName": string,
-  "inquestDept": string,
-  "assignDept": string,
-  "assignType": string,
-  "times": string[],
-  "inquestAddress": string,
-  "tbStatus": number,
-  "createTime": string,
-  "updateTime": string
-};
-
 export interface Case {
-  caseTitle: string;
-  latAndLong: string;
-  mapUrl: string;
-  showScenes: boolean
-  caseNum: string;
-  caseCategory: string;
-  caseRegion: string;
-  caseAddress: string;
-  homicideCase: number;
-  criminalCase: number;
-  tmProject?: FireProject;
+  sceneVoList: Scene[];
+  createTime: string
+  fusionTitle?: string
+  platformId: null;
 }
 
 export const getCaseInfo = async () => {
-  const [caseInfo, fireInfo] = await Promise.all([
-    axios.get<Case>(CASE_INFO, { params: { caseId: params.caseId } }),
-    axios.get<FireProject>(CASE_FIRE_INFO, { params: { caseId: params.caseId } })
-  ])
-  caseInfo.tmProject = fireInfo
-  return caseInfo
+  const [caseInfo] = await Promise.all([
+    axios.get<Case>(CASE_INFO, { params: { fusionId: params.caseId } }),
+  ]);
+  return caseInfo;
+};
+
+export const updateCaseInfo = async (data: Case) => {
+  return axios.post(UPDATE_CASE_INFO, { ...data })
 }
 
 // 校验密码
 export const authSharePassword = (randCode: string) =>
-  axios<boolean>(AUTH_PWD, { params: { randCode, caseId: params.caseId } });
+  axios<boolean>(AUTH_PWD, { params: { randCode, fusionId: params.caseId } });

+ 15 - 1
src/api/tagging-position.ts

@@ -24,6 +24,8 @@ interface ServicePosition {
   fontSize: number,
   lineHeight: number,
   visibilityRange: number
+  pose?: string;
+  
 }
 
 export enum TaggingPositionType {
@@ -43,6 +45,16 @@ export interface TaggingPosition {
 
   type: TaggingPositionType
   mat: Tagging3DProps['mat']
+  pose?: {
+    position: SceneLocalPos;
+    target: SceneLocalPos;
+    panoInfo?: {
+      panoId: any;
+      modelId: string;
+      posInModel: SceneLocalPos;
+      rotInModel: SceneLocalPos;
+    };
+  };
 }
 
 export type TaggingPositions = TaggingPosition[]
@@ -63,6 +75,7 @@ const serviceToLocal = (position: ServicePosition, taggingId?: Tagging['id']): T
   visibilityRange: position.visibilityRange || 30,
   fontSize: position.fontSize || 12,
   lineHeight: position.lineHeight || 1,
+  pose: position.pose && JSON.parse(position.pose)
 })
 
 const localToService = (position: TaggingPosition, update = false): PartialProps<ServicePosition, 'tagPointId'> => ({
@@ -76,7 +89,8 @@ const localToService = (position: TaggingPosition, update = false): PartialProps
   normal: JSON.stringify(position.normal),
   fontSize: position.fontSize,
   lineHeight: position.lineHeight,
-  visibilityRange: position.visibilityRange 
+  visibilityRange: position.visibilityRange ,
+  pose: position.pose && JSON.stringify(position.pose)
 })
 
 

+ 71 - 21
src/api/tagging-style.ts

@@ -4,10 +4,12 @@ import {
   INSERT_TAGGING_STYLE,
   DELETE_TAGGING_STYLE,
   UPLOAD_HEADS,
+  TAGGING_STYLE_TREE,
 } from "./constant";
 import { jsonToForm } from "@/utils";
 import { params } from "@/env";
 import { ui18n } from "@/lang";
+import { fetchMaterialGroups } from "./material";
 interface ServiceStyle {
   iconId: number;
   iconTitle: string;
@@ -39,23 +41,37 @@ export const styleTypes = [
   defStyleType
 ];
 
-export const getStyleTypeName = (id: number, all = styleTypes): string => {
+export const getStyleTypeName = (id: number, all: any = styleTypes): string => {
   for (const item of all) {
     if (id === item.id) {
-      return item.name
-    } else if ('children' in item && item.children) {
-      const cname = getStyleTypeName(id, item.children)
+      return item.name;
+    } else if ("children" in item && item.children) {
+      const cname = getStyleTypeName(id, item.children);
       if (cname) {
-        return cname
+        return cname;
       }
     }
   }
-  if (all === styleTypes) {
-    return defStyleType.name
-  } else {
-    return ''
+  return "";
+};
+
+export const getStyleTypeId = (dictId: number, all: any = styleTypes): number => {
+  for (const item of all) {
+    if (dictId === item.dictId) {
+      return item.id;
+    } else if ("children" in item && item.children) {
+      const cid = getStyleTypeId(dictId, item.children);
+      if (cid) {
+        return cid;
+      }
+    }
   }
-}
+  return -1;
+};
+export const getStyleDictId = (id: number, all: any = styleTypes): number => {
+  return __map[id]
+};
+
 
 export interface TaggingStyle {
   id: string;
@@ -65,13 +81,15 @@ export interface TaggingStyle {
   default: boolean;
 }
 
-const toLocal = (serviceStyle: ServiceStyle): TaggingStyle => ({
-  id: serviceStyle.iconId.toString(),
-  lastUse: serviceStyle.lastUse,
-  typeId: Number(serviceStyle.iconTitle) || defStyleType.id,
-  icon: serviceStyle.iconUrl,
-  default: Boolean(serviceStyle.isSystem),
-});
+const toLocal = (serviceStyle: ServiceStyle): TaggingStyle => {
+  return {
+    id: serviceStyle.iconId.toString(),
+    lastUse: serviceStyle.lastUse,
+    typeId: Number(serviceStyle.iconTitle),
+    icon: serviceStyle.iconUrl,
+    default: Boolean(serviceStyle.isSystem),
+  };
+};
 
 const toService = (style: TaggingStyle): ServiceStyle => ({
   iconId: Number(style.id),
@@ -83,12 +101,43 @@ const toService = (style: TaggingStyle): ServiceStyle => ({
 
 export type TaggingStyles = TaggingStyle[];
 
+const __map: Record<string, number> = {}
 export const fetchTaggingStyles = async () => {
-  const reqParams = params.share ? { caseId: params.caseId } : {};
-  const data = await axios.get<ServiceStyle[]>(TAGGING_STYLE_LIST, {
+  const reqParams = params.share ? { fusionId: params.caseId } : {};
+  const treeData = await axios.get<any>(TAGGING_STYLE_TREE, {
     params: reqParams,
   });
-  return data.map(toLocal);
+  const styles: any[] = [];
+  const genTree = (tree: any, parent?: any) => {
+    for (const item of tree) {
+      if (item.iconUrl) {
+        delete parent.children;
+        __map[item.iconId] = item.dictId || parent.dictId
+        styles.push(toLocal({ ...item, iconTitle: parent.id }));
+      } else {
+        const data = {
+          id: item.iconId,
+          dictId: item.dictId,
+          name: item.iconTitle,
+          children: [],
+        };
+        parent.children.push(data);
+        genTree(item.childrenList, data)
+      }
+    }
+  };
+  const tree: any = { children: [] };
+  genTree(treeData, tree);
+  styleTypes.length = 0
+  styleTypes.push(...tree.children)
+  Object.assign(defStyleType, tree.children[tree.children.length - 1])
+  
+  console.error('StyleT', styleTypes)
+
+  // const data = await axios.get<ServiceStyle[]>(TAGGING_STYLE_LIST, {
+  //   params: reqParams,
+  // });
+  return styles;
 };
 
 export const postAddTaggingStyle = async (props: {
@@ -102,7 +151,8 @@ export const postAddTaggingStyle = async (props: {
     data: jsonToForm({
       file: new File([props.file], `${props.iconTitle}.png`),
       iconTitle: props.iconTitle.toString(),
-      caseId: params.caseId,
+      parentId: props.iconTitle,
+      fusionId: params.caseId,
     }),
   });
   return toLocal(data);

+ 35 - 6
src/api/tagging.ts

@@ -4,18 +4,20 @@ import {
   TAGGING_LIST,
   DELETE_TAGGING,
   INSERT_TAGGING,
-  UPDATE_TAGGING
+  UPDATE_TAGGING,
+  TAGGING_TYPE_LIST
 } from './constant'
 
 import type { FuseModel } from './fuse-model'
 import { TaggingPosition, TaggingPositionType } from './tagging-position'
-import type { TaggingStyle } from './tagging-style'
+import { getStyleDictId, type TaggingStyle } from './tagging-style'
 import { Tagging3DProps } from '@/sdk'
 
 interface ServerTagging {
   "hotIconId": number,
   "hotIconUrl": string,
   "getMethod": string,
+  dictId?: string
   "getUser": string,
   "tagId": number,
   "tagImgUrl": string,
@@ -26,6 +28,11 @@ interface ServerTagging {
   show3dTitle: number
   audio: string
   fileName: string
+
+  // 提取状态
+  tqStatus?: tqStatusEnum,
+  // 提取时间
+  tqTime?: string
 }
 
 export interface Tagging {
@@ -40,6 +47,17 @@ export interface Tagging {
   images: string[],
   audio: string
   audioName: string
+
+  // 提取状态
+  tqStatus?: tqStatusEnum,
+  // 提取时间
+  tqTime?: string
+}
+
+export enum tqStatusEnum {
+  UN = '未送检',
+  ING = '送检中',
+  END = '完成检验'
 }
 
 export type Taggings = Tagging[]
@@ -58,6 +76,10 @@ const serviceToLocal = (serviceTagging: ServerTagging): Tagging => ({
   principal: serviceTagging.getUser,
   audio: serviceTagging.audio,
   images: JSON.parse(serviceTagging.tagImgUrl),
+  // 提取状态
+  tqStatus: serviceTagging.tqStatus,
+  // 提取时间
+  tqTime: serviceTagging.tqTime
 })
 
 const localToService = (tagging: Tagging, update = false): PartialProps<ServerTagging, 'tagId' | 'hotIconUrl'> & { fusionId: number } => ({
@@ -65,6 +87,7 @@ const localToService = (tagging: Tagging, update = false): PartialProps<ServerTa
   "fusionId": params.caseId,
   "getMethod": tagging.method,
   show3dTitle: Number(tagging.show3dTitle),
+  dictId: getStyleDictId(Number(tagging.styleId))?.toString(),
   "getUser": tagging.principal,
   fileName: tagging.audioName,
   "hotIconUrl": "static/img_default/lQLPDhrvVzvNvTswMLAOU-UNqYnnZQG1YPJUwLwA_48_48.png",
@@ -73,22 +96,28 @@ const localToService = (tagging: Tagging, update = false): PartialProps<ServerTa
   "leaveBehind": tagging.part,
   "tagDescribe": tagging.desc,
   "tagTitle": tagging.title,
-  audio: tagging.audio
+  audio: tagging.audio,
+
+  // 提取状态
+  tqStatus: tagging.tqStatus,
+  // 提取时间
+  tqTime: tagging.tqTime
 })
 
 
 export const fetchTaggings = async () => {
-  const staggings = await axios.get<ServerTagging[]>(TAGGING_LIST, { params: { caseId: params.caseId } })
+  axios.get(TAGGING_TYPE_LIST)
+  const staggings = await axios.get<ServerTagging[]>(TAGGING_LIST, { params: { fusionId: params.caseId } })
   return staggings.map(serviceToLocal)
 }
 
 export const postAddTagging = async (tagging: Tagging) => {
-  const stagging = await axios.post<ServerTagging>(INSERT_TAGGING, { ...localToService(tagging), caseId: params.caseId })
+  const stagging = await axios.post<ServerTagging>(INSERT_TAGGING, { ...localToService(tagging), fusionId: params.caseId })
   return serviceToLocal(stagging, )
 }
 
 export const postUpdateTagging = (tagging: Tagging) => {
-  return axios.post<undefined>(UPDATE_TAGGING, { ...localToService(tagging, true), caseId: params.caseId })
+  return axios.post<undefined>(UPDATE_TAGGING, { ...localToService(tagging, true), fusionId: params.caseId })
 }
   
 

+ 2 - 2
src/api/view.ts

@@ -45,12 +45,12 @@ const toService = (view: View, isUpdate = true): PartialProps<ServiceView, 'view
 export type Views = View[]
 
 export const fetchViews = async () => {
-  const data = await axios.get<ServiceView[]>(VIEW_LIST, { params: { caseId: params.caseId } })
+  const data = await axios.get<ServiceView[]>(VIEW_LIST, { params: { fusionId: params.caseId } })
   return data.map(toLocal)
 }
 
 export const postAddView = async (view: View) => {
-  const serviceView = await axios.post<ServiceView>(INSERT_VIEW, { ...toService(view, false), caseId: params.caseId })
+  const serviceView = await axios.post<ServiceView>(INSERT_VIEW, { ...toService(view, false), fusionId: params.caseId })
   return toLocal(serviceView)
 }
 

+ 69 - 5
src/app.vue

@@ -1,5 +1,5 @@
 <template>
-  <ConfigProvider v-bind="config">
+  <ConfigProvider v-bind="config" v-if="!showLogin">
     <template v-for="needMount in needMounts">
       <Teleport :to="needMount[0]">
         <component
@@ -12,12 +12,17 @@
 
     <ui-editor-layout
       @click.stop
+      @contextmenu.prevent
       id="layout-app"
       class="editor-layout"
       :style="layoutStyles"
       :class="layoutClassNames"
     >
-      <div :ref="(el: any) => appEl = (el as HTMLDivElement)" v-if="loaded">
+      <div
+        :ref="(el: any) => appEl = (el as HTMLDivElement)"
+        v-if="loaded"
+        class="app-con"
+      >
         <router-view v-slot="{ Component }">
           <!-- <keep-alive> -->
           <component :is="Component" />
@@ -37,10 +42,17 @@
 
     <PwdModel v-if="inputPwd" @close="inputPwd = false" />
   </ConfigProvider>
+  <Login v-else />
 </template>
 
 <script lang="ts" setup>
-import { custom, params } from "@/env";
+import {
+  custom,
+  params,
+  showLeftPanoStack,
+  showRightCtrlPanoStack,
+  showRightPanoStack,
+} from "@/env";
 import { computed, ref, watch, watchEffect, nextTick } from "vue";
 import {
   isEdit,
@@ -53,11 +65,34 @@ import {
   scenes,
 } from "@/store";
 import router, { currentLayout, RoutesName } from "./router";
-import { asyncTimeout, loadPack, needMounts } from "@/utils";
+import { asyncTimeout, encodePwd, loadPack, needMounts } from "@/utils";
 import { ConfigProvider } from "ant-design-vue";
 import PwdModel from "@/layout/pwd.vue";
 import { config } from "./config";
 import { sdk, sdkLoaded } from "./sdk";
+import GAxios from "axios";
+
+import { addReqErrorHandler, addResErrorHandler, ResCode, setToken } from "./api";
+import { mergeFuns } from "./components/drawing/hook";
+import Login from "./views/login.vue";
+
+const gotoLogin = () => {
+  showLogin.value = true;
+};
+
+addResErrorHandler((data: any) => {
+  data = data.data;
+  if (data.code === ResCode.TOKEN_INVALID) {
+    gotoLogin();
+  } else if (data.code === ResCode.UN_AUTH) {
+    // console.log("--->");
+    gotoLogin();
+  } else if (data.code === ResCode.UN_EDIT_AUTH) {
+    router.replace(RoutesName.show);
+  }
+});
+addReqErrorHandler(gotoLogin);
+
 // https://192.168.0.13:7173/index.html?caseId=509&sign=vGxCu4X5321fkWpZN6HnqYBiE6iI71DDWzdgjEaUKIh7vDWo3o5yhqHdHhGr4Z3W#/show
 
 const isStandard = ref(true);
@@ -116,10 +151,10 @@ const stopWatch = watch(
     // }
 
     params.share = true;
+    await asyncTimeout(100);
     await refreshCase();
     if (caseProject.value) {
       await loadPack(initialSetting);
-      prefix.value = caseProject.value!.caseTitle;
     } else {
       await router.replace({ name: RoutesName.error });
     }
@@ -129,6 +164,10 @@ const stopWatch = watch(
   { immediate: true }
 );
 
+watchEffect(() => {
+  prefix.value = caseProject.value?.fusionTitle || "多元融合";
+});
+
 const layoutClassNames = computed(() => {
   return {
     [`sys-view-${custom.viewMode}`]: true,
@@ -136,6 +175,7 @@ const layoutClassNames = computed(() => {
     "setting-mode": custom.showToolbar,
     "hide-right-box-mode": !custom.showRightPano,
     "hide-left-box-mode": !custom.showLeftPano,
+    "full-view": custom.full,
     "show-bottom-box-mode": custom.showBottomBar,
     "hide-top-bar-mode": !custom.showHeadBar,
   };
@@ -148,6 +188,24 @@ const layoutStyles = computed(() => {
   }
   return styles;
 });
+
+const showLogin = ref(false);
+
+watch(
+  () => custom.full,
+  (full, _, onCleanup) => {
+    if (full) {
+      onCleanup(
+        mergeFuns(
+          showRightPanoStack.push(ref(false)),
+          showLeftPanoStack.push(ref(false)),
+          showRightCtrlPanoStack.push(ref(false)),
+          showRightCtrlPanoStack.push(ref(false))
+        )
+      );
+    }
+  }
+);
 </script>
 
 <style scoped lang="scss">
@@ -229,3 +287,9 @@ const layoutStyles = computed(() => {
   }
 }
 </style>
+
+<style lang="scss">
+.full-view #global-search {
+  left: 20px !important;
+}
+</style>

+ 97 - 0
src/below.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="below-layout">
+    <div class="content">
+      <img :src="a" alt="" />
+      <p>无法打开页面,请升级或更换浏览器后重新打开</p>
+      <span>建议使用以下浏览器</span>
+      <div class="list">
+        <div v-for="item in items">
+          <img :src="item.icon" />
+          <p @click="useClickHandler(item.link)"><img :src="b" /> {{ item.name }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+const a = "images/err.png";
+const b = "images/download.png";
+const items = [
+  {
+    name: "Firefox",
+    icon: "images/ff.png",
+    link: "http://www.firefox.com.cn/",
+  },
+  {
+    name: "Microsoft Edge",
+    icon: "images/eg.png",
+    link: "https://www.microsoft.com/en-us/edge",
+  },
+  // {
+  //   icon: "images/safar.png",
+  //   link: "https://www.apple.com/safari/",
+  // },
+  {
+    name: "Chrome",
+    icon: "images/chrome.png",
+    link: "https://www.google.com/chrome/",
+  },
+];
+
+const useClickHandler = (link: string) => {
+  console.log(link);
+  window.open(link);
+};
+</script>
+
+<style lang="scss" scoped>
+.below-layout {
+  position: fixed;
+  inset: 0;
+  background: #f7f7f7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .content {
+    color: #999;
+    text-align: center;
+    > img {
+      width: 200px;
+    }
+
+    p {
+      font-size: 16px;
+      margin: 20px 0;
+    }
+
+    span {
+      font-size: 14px;
+    }
+  }
+}
+
+.list {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 20px;
+
+  p {
+    color: #999;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    img {
+      width: 16px;
+    }
+  }
+  > div {
+    margin: 0 20px;
+    > img {
+      width: 70px !important;
+    }
+  }
+}
+</style>

+ 90 - 0
src/components/actions-merge/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="actions">
+    <span
+      v-for="(action, i) in items"
+      :class="{ active: equal(current as any, action), disabled: action.disabled }"
+      :key="action.key || i"
+      @click="emit('update:current', current === action ? null : action)"
+    >
+      <ui-icon :type="action.icon" class="icon" :tip="action.text" />
+    </span>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useActive } from "@/hook";
+import { ref, toRaw, watchEffect, onBeforeUnmount, nextTick, watch } from "vue";
+
+export type ActionsItem<T = any> = {
+  icon: string;
+  key?: T;
+  text: string;
+  disabled?: boolean;
+  action?: () => (() => void) | void;
+};
+export type ActionsProps = {
+  items: ActionsItem[];
+  current?: ActionsItem | null;
+  single?: boolean;
+};
+
+const props = defineProps<ActionsProps>();
+const emit = defineEmits<{ (e: "update:current", data: ActionsItem | null): void }>();
+const equal = (a: ActionsItem | null, b: ActionsItem | null) => toRaw(a) === toRaw(b);
+
+watch(
+  () => props.current,
+  (current) => {
+    if (!current) return;
+    if (props.single) {
+      emit("update:current", null);
+    }
+  },
+  { immediate: true }
+);
+watch(
+  () => props.current,
+  (current, p, onCleanup) => {
+    if (!current) return;
+    const fn = current.action && current.action();
+    fn && onCleanup(fn);
+  },
+  { immediate: true }
+);
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  gap: 3px;
+  background: rgba(27, 27, 28, 0.8);
+  box-shadow: inset 0px 0px 0px 2px rgba(255, 255, 255, 0.1);
+  border-radius: 4px 4px 4px 4px;
+  padding: 4px 10px;
+
+  span {
+    flex: 1;
+    height: 32px;
+    width: 32px;
+    border-radius: 4px 4px 4px 4px;
+    opacity: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: rgba(255, 255, 255, 0.6);
+    font-size: 14px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+
+    .icon {
+      font-size: 22px;
+    }
+
+    &:hover,
+    &.active {
+      background: rgba(0, 200, 175, 0.16);
+      color: #00c8af;
+    }
+  }
+}
+</style>

+ 52 - 44
src/components/actions/index.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="actions">
-    <span 
-      v-for="(action, i) in items" 
-      :class="{active: equal(selected, action)}"
-      :key="action.key || i" 
+    <span
+      v-for="(action, i) in items"
+      :class="{ active: equal(selected, action) }"
+      :key="action.key || i"
       @click="clickHandler(action)"
     >
       <ui-icon :type="action.icon" class="icon" />
@@ -13,79 +13,87 @@
 </template>
 
 <script lang="ts" setup>
-import { useActive } from '@/hook';
-import { ref, toRaw, watchEffect, onBeforeUnmount, nextTick, watch } from 'vue'
+import { useActive } from "@/hook";
+import { ref, toRaw, watchEffect, onBeforeUnmount, nextTick, watch } from "vue";
 
-export type ActionsItem<T = any> = { 
-  icon: string, 
-  key?: T, 
-  text: string,
-  action?: () => (() => void) | void
-}
-export type ActionsProps = { items: ActionsItem[], current?: ActionsItem | null, single?: boolean, }
+export type ActionsItem<T = any> = {
+  icon: string;
+  key?: T;
+  text: string;
+  action?: () => (() => void) | void;
+};
+export type ActionsProps = {
+  items: ActionsItem[];
+  current?: ActionsItem | null;
+  single?: boolean;
+};
 
-const props = defineProps<ActionsProps>()
-const emit = defineEmits<{ (e: 'update:current', data: ActionsItem | null): void }>()
-const equal = (a: ActionsItem | null, b: ActionsItem | null) => toRaw(a) === toRaw(b)
-const selected = ref<ActionsItem | null>(null)
+const props = defineProps<ActionsProps>();
+const emit = defineEmits<{ (e: "update:current", data: ActionsItem | null): void }>();
+const equal = (a: ActionsItem | null, b: ActionsItem | null) => toRaw(a) === toRaw(b);
+const selected = ref<ActionsItem | null>(null);
 const clickHandler = (select: ActionsItem) => {
-  selected.value = equal(selected.value, select) ? null : select
-  emit('update:current', selected.value)
+  selected.value = equal(selected.value, select) ? null : select;
+  emit("update:current", selected.value);
+  console.log("update:current", selected.value);
   if (props.single) {
-    nextTick(() => selected.value && clickHandler(selected.value))
+    setTimeout(() => selected.value && clickHandler(selected.value), 16);
   }
-}
+};
+
+// watch(
+//   () => props.current,
+//   () => {
+//     if (!props.current && selected.value) {
+//       clickHandler(selected.value)
+//     }
+//   }
+// )
 
 watch(
-  () => props.current, 
-  () => {
-    if (!props.current && selected.value) {
-      clickHandler(selected.value)
+  selected,
+  (_n, _o, onCleanup) => {
+    if (selected.value?.action) {
+      const cleanup = selected.value.action();
+      cleanup && onCleanup(cleanup);
     }
-  }
-)
-
-watch(selected, (_n, _o, onCleanup) => {
-  if (selected.value?.action) {
-    const cleanup = selected.value.action()
-    cleanup && onCleanup(cleanup)
-  }
-}, { flush: 'sync' })
+  },
+  { flush: "sync" }
+);
 
 onBeforeUnmount(() => {
-  selected.value = null
-})
+  selected.value = null;
+});
 </script>
 
 <style lang="scss" scoped>
 .actions {
   display: flex;
   gap: 10px;
-  
+
   span {
     flex: 1;
     height: 34px;
-    background: rgba(255,255,255,0.1);
+    background: rgba(255, 255, 255, 0.1);
     border-radius: 4px 4px 4px 4px;
     opacity: 1;
     display: flex;
     align-items: center;
     justify-content: center;
-    color: rgba(255,255,255,0.6);
+    color: rgba(255, 255, 255, 0.6);
     font-size: 14px;
     cursor: pointer;
-    transition: all .3s ease;
+    transition: all 0.3s ease;
 
     .icon {
       margin-right: 4px;
     }
 
-
     &:hover,
     &.active {
-      background: rgba(0,200,175,0.16);
-      color: #00C8AF;
+      background: rgba(0, 200, 175, 0.16);
+      color: #00c8af;
     }
   }
 }
-</style>
+</style>

+ 1 - 1
src/components/bill-ui/assets/scss/_base-vars.scss

@@ -6,7 +6,7 @@
   --colors-primary-hover: #008B7A;
   --colors-primary-click: #005046;
 
-  --colors-color: #999;
+  --colors-color: rgba(255,255,255,0.7);
   --colors-border-color: rgba(var(--colors-primary-fill), 0.16);
   --colors-content-color: rgb(--colors-primary-fill);
   

+ 1 - 1
src/components/bill-ui/assets/scss/editor/_toolbar.scss

@@ -14,5 +14,5 @@
     left: calc(var(--editor-menu-left) + var(--editor-menu-width));
     z-index: 2;
     backdrop-filter: blur(4px);
-    transition: all .3s ease;
+    // transition: all .3s ease;
 }

+ 739 - 3
src/components/bill-ui/components/icon/iconfont/demo_index.html

@@ -55,6 +55,198 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe7bf;</span>
+                <div class="name">orientation</div>
+                <div class="code-name">&amp;#xe7bf;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b4;</span>
+                <div class="name">window_n</div>
+                <div class="code-name">&amp;#xe7b4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b5;</span>
+                <div class="name">window_m</div>
+                <div class="code-name">&amp;#xe7b5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b2;</span>
+                <div class="name">screen_s</div>
+                <div class="code-name">&amp;#xe7b2;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b3;</span>
+                <div class="name">3D_scence</div>
+                <div class="code-name">&amp;#xe7b3;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7af;</span>
+                <div class="name">media</div>
+                <div class="code-name">&amp;#xe7af;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b0;</span>
+                <div class="name">rename</div>
+                <div class="code-name">&amp;#xe7b0;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7b1;</span>
+                <div class="name">display</div>
+                <div class="code-name">&amp;#xe7b1;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79e;</span>
+                <div class="name">line_d</div>
+                <div class="code-name">&amp;#xe79e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79d;</span>
+                <div class="name">a-label_s</div>
+                <div class="code-name">&amp;#xe79d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79c;</span>
+                <div class="name">guide_p</div>
+                <div class="code-name">&amp;#xe79c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79b;</span>
+                <div class="name">view</div>
+                <div class="code-name">&amp;#xe79b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe799;</span>
+                <div class="name">ratio</div>
+                <div class="code-name">&amp;#xe799;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79a;</span>
+                <div class="name">1b1</div>
+                <div class="code-name">&amp;#xe79a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe65a;</span>
+                <div class="name">reset</div>
+                <div class="code-name">&amp;#xe65a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe798;</span>
+                <div class="name">menu</div>
+                <div class="code-name">&amp;#xe798;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe797;</span>
+                <div class="name">keys_a</div>
+                <div class="code-name">&amp;#xe797;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe796;</span>
+                <div class="name">add_a</div>
+                <div class="code-name">&amp;#xe796;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78a;</span>
+                <div class="name">rectification</div>
+                <div class="code-name">&amp;#xe78a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78b;</span>
+                <div class="name">a-zoom</div>
+                <div class="code-name">&amp;#xe78b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78c;</span>
+                <div class="name">a-play</div>
+                <div class="code-name">&amp;#xe78c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78d;</span>
+                <div class="name">a-animation_s</div>
+                <div class="code-name">&amp;#xe78d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78e;</span>
+                <div class="name">keys</div>
+                <div class="code-name">&amp;#xe78e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe78f;</span>
+                <div class="name">a-anchor</div>
+                <div class="code-name">&amp;#xe78f;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe790;</span>
+                <div class="name">a-monitoring_s</div>
+                <div class="code-name">&amp;#xe790;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe791;</span>
+                <div class="name">a-rotate</div>
+                <div class="code-name">&amp;#xe791;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe792;</span>
+                <div class="name">a-move</div>
+                <div class="code-name">&amp;#xe792;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe793;</span>
+                <div class="name">a-path_s</div>
+                <div class="code-name">&amp;#xe793;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe794;</span>
+                <div class="name">a-pause</div>
+                <div class="code-name">&amp;#xe794;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe795;</span>
+                <div class="name">a-guide_s</div>
+                <div class="code-name">&amp;#xe795;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe788;</span>
+                <div class="name">magnify</div>
+                <div class="code-name">&amp;#xe788;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe789;</span>
+                <div class="name">reduce</div>
+                <div class="code-name">&amp;#xe789;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe778;</span>
                 <div class="name">pic_path</div>
                 <div class="code-name">&amp;#xe778;</div>
@@ -600,9 +792,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1734660652855') format('woff2'),
-       url('iconfont.woff?t=1734660652855') format('woff'),
-       url('iconfont.ttf?t=1734660652855') format('truetype');
+  src: url('iconfont.woff2?t=1756279097633') format('woff2'),
+       url('iconfont.woff?t=1756279097633') format('woff'),
+       url('iconfont.ttf?t=1756279097633') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -629,6 +821,294 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-orientation"></span>
+            <div class="name">
+              orientation
+            </div>
+            <div class="code-name">.icon-orientation
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-window_n"></span>
+            <div class="name">
+              window_n
+            </div>
+            <div class="code-name">.icon-window_n
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-window_m"></span>
+            <div class="name">
+              window_m
+            </div>
+            <div class="code-name">.icon-window_m
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-screen_s"></span>
+            <div class="name">
+              screen_s
+            </div>
+            <div class="code-name">.icon-screen_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-3D_scence"></span>
+            <div class="name">
+              3D_scence
+            </div>
+            <div class="code-name">.icon-a-3D_scence
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-media"></span>
+            <div class="name">
+              media
+            </div>
+            <div class="code-name">.icon-media
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-rename"></span>
+            <div class="name">
+              rename
+            </div>
+            <div class="code-name">.icon-rename
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-display"></span>
+            <div class="name">
+              display
+            </div>
+            <div class="code-name">.icon-display
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-line_d"></span>
+            <div class="name">
+              line_d
+            </div>
+            <div class="code-name">.icon-line_d
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-label_s"></span>
+            <div class="name">
+              a-label_s
+            </div>
+            <div class="code-name">.icon-a-label_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-guide_p"></span>
+            <div class="name">
+              guide_p
+            </div>
+            <div class="code-name">.icon-guide_p
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-view"></span>
+            <div class="name">
+              view
+            </div>
+            <div class="code-name">.icon-view
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-ratio"></span>
+            <div class="name">
+              ratio
+            </div>
+            <div class="code-name">.icon-ratio
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-1b1"></span>
+            <div class="name">
+              1b1
+            </div>
+            <div class="code-name">.icon-a-1b1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-reset"></span>
+            <div class="name">
+              reset
+            </div>
+            <div class="code-name">.icon-reset
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-menu"></span>
+            <div class="name">
+              menu
+            </div>
+            <div class="code-name">.icon-menu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-keys_a"></span>
+            <div class="name">
+              keys_a
+            </div>
+            <div class="code-name">.icon-keys_a
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-add_a"></span>
+            <div class="name">
+              add_a
+            </div>
+            <div class="code-name">.icon-add_a
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-rectification"></span>
+            <div class="name">
+              rectification
+            </div>
+            <div class="code-name">.icon-rectification
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-zoom"></span>
+            <div class="name">
+              a-zoom
+            </div>
+            <div class="code-name">.icon-a-zoom
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-play"></span>
+            <div class="name">
+              a-play
+            </div>
+            <div class="code-name">.icon-a-play
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-animation_s"></span>
+            <div class="name">
+              a-animation_s
+            </div>
+            <div class="code-name">.icon-a-animation_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-keys"></span>
+            <div class="name">
+              keys
+            </div>
+            <div class="code-name">.icon-keys
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-anchor"></span>
+            <div class="name">
+              a-anchor
+            </div>
+            <div class="code-name">.icon-a-anchor
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-monitoring_s"></span>
+            <div class="name">
+              a-monitoring_s
+            </div>
+            <div class="code-name">.icon-a-monitoring_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-rotate"></span>
+            <div class="name">
+              a-rotate
+            </div>
+            <div class="code-name">.icon-a-rotate
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-move"></span>
+            <div class="name">
+              a-move
+            </div>
+            <div class="code-name">.icon-a-move
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-path_s"></span>
+            <div class="name">
+              a-path_s
+            </div>
+            <div class="code-name">.icon-a-path_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-pause"></span>
+            <div class="name">
+              a-pause
+            </div>
+            <div class="code-name">.icon-a-pause
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-guide_s"></span>
+            <div class="name">
+              a-guide_s
+            </div>
+            <div class="code-name">.icon-a-guide_s
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-magnify"></span>
+            <div class="name">
+              magnify
+            </div>
+            <div class="code-name">.icon-magnify
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-reduce"></span>
+            <div class="name">
+              reduce
+            </div>
+            <div class="code-name">.icon-reduce
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-pic_path"></span>
             <div class="name">
               pic_path
@@ -1449,6 +1929,262 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-orientation"></use>
+                </svg>
+                <div class="name">orientation</div>
+                <div class="code-name">#icon-orientation</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-window_n"></use>
+                </svg>
+                <div class="name">window_n</div>
+                <div class="code-name">#icon-window_n</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-window_m"></use>
+                </svg>
+                <div class="name">window_m</div>
+                <div class="code-name">#icon-window_m</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-screen_s"></use>
+                </svg>
+                <div class="name">screen_s</div>
+                <div class="code-name">#icon-screen_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-3D_scence"></use>
+                </svg>
+                <div class="name">3D_scence</div>
+                <div class="code-name">#icon-a-3D_scence</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-media"></use>
+                </svg>
+                <div class="name">media</div>
+                <div class="code-name">#icon-media</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-rename"></use>
+                </svg>
+                <div class="name">rename</div>
+                <div class="code-name">#icon-rename</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-display"></use>
+                </svg>
+                <div class="name">display</div>
+                <div class="code-name">#icon-display</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-line_d"></use>
+                </svg>
+                <div class="name">line_d</div>
+                <div class="code-name">#icon-line_d</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-label_s"></use>
+                </svg>
+                <div class="name">a-label_s</div>
+                <div class="code-name">#icon-a-label_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-guide_p"></use>
+                </svg>
+                <div class="name">guide_p</div>
+                <div class="code-name">#icon-guide_p</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-view"></use>
+                </svg>
+                <div class="name">view</div>
+                <div class="code-name">#icon-view</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-ratio"></use>
+                </svg>
+                <div class="name">ratio</div>
+                <div class="code-name">#icon-ratio</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-1b1"></use>
+                </svg>
+                <div class="name">1b1</div>
+                <div class="code-name">#icon-a-1b1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-reset"></use>
+                </svg>
+                <div class="name">reset</div>
+                <div class="code-name">#icon-reset</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-menu"></use>
+                </svg>
+                <div class="name">menu</div>
+                <div class="code-name">#icon-menu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-keys_a"></use>
+                </svg>
+                <div class="name">keys_a</div>
+                <div class="code-name">#icon-keys_a</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-add_a"></use>
+                </svg>
+                <div class="name">add_a</div>
+                <div class="code-name">#icon-add_a</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-rectification"></use>
+                </svg>
+                <div class="name">rectification</div>
+                <div class="code-name">#icon-rectification</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-zoom"></use>
+                </svg>
+                <div class="name">a-zoom</div>
+                <div class="code-name">#icon-a-zoom</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-play"></use>
+                </svg>
+                <div class="name">a-play</div>
+                <div class="code-name">#icon-a-play</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-animation_s"></use>
+                </svg>
+                <div class="name">a-animation_s</div>
+                <div class="code-name">#icon-a-animation_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-keys"></use>
+                </svg>
+                <div class="name">keys</div>
+                <div class="code-name">#icon-keys</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-anchor"></use>
+                </svg>
+                <div class="name">a-anchor</div>
+                <div class="code-name">#icon-a-anchor</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-monitoring_s"></use>
+                </svg>
+                <div class="name">a-monitoring_s</div>
+                <div class="code-name">#icon-a-monitoring_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-rotate"></use>
+                </svg>
+                <div class="name">a-rotate</div>
+                <div class="code-name">#icon-a-rotate</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-move"></use>
+                </svg>
+                <div class="name">a-move</div>
+                <div class="code-name">#icon-a-move</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-path_s"></use>
+                </svg>
+                <div class="name">a-path_s</div>
+                <div class="code-name">#icon-a-path_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-pause"></use>
+                </svg>
+                <div class="name">a-pause</div>
+                <div class="code-name">#icon-a-pause</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-guide_s"></use>
+                </svg>
+                <div class="name">a-guide_s</div>
+                <div class="code-name">#icon-a-guide_s</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-magnify"></use>
+                </svg>
+                <div class="name">magnify</div>
+                <div class="code-name">#icon-magnify</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-reduce"></use>
+                </svg>
+                <div class="name">reduce</div>
+                <div class="code-name">#icon-reduce</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-pic_path"></use>
                 </svg>
                 <div class="name">pic_path</div>

+ 131 - 3
src/components/bill-ui/components/icon/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4647199 */
-  src: url('iconfont.woff2?t=1734660652855') format('woff2'),
-       url('iconfont.woff?t=1734660652855') format('woff'),
-       url('iconfont.ttf?t=1734660652855') format('truetype');
+  src: url('iconfont.woff2?t=1756279097633') format('woff2'),
+       url('iconfont.woff?t=1756279097633') format('woff'),
+       url('iconfont.ttf?t=1756279097633') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,134 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-orientation:before {
+  content: "\e7bf";
+}
+
+.icon-window_n:before {
+  content: "\e7b4";
+}
+
+.icon-window_m:before {
+  content: "\e7b5";
+}
+
+.icon-screen_s:before {
+  content: "\e7b2";
+}
+
+.icon-a-3D_scence:before {
+  content: "\e7b3";
+}
+
+.icon-media:before {
+  content: "\e7af";
+}
+
+.icon-rename:before {
+  content: "\e7b0";
+}
+
+.icon-display:before {
+  content: "\e7b1";
+}
+
+.icon-line_d:before {
+  content: "\e79e";
+}
+
+.icon-a-label_s:before {
+  content: "\e79d";
+}
+
+.icon-guide_p:before {
+  content: "\e79c";
+}
+
+.icon-view:before {
+  content: "\e79b";
+}
+
+.icon-ratio:before {
+  content: "\e799";
+}
+
+.icon-a-1b1:before {
+  content: "\e79a";
+}
+
+.icon-reset:before {
+  content: "\e65a";
+}
+
+.icon-menu:before {
+  content: "\e798";
+}
+
+.icon-keys_a:before {
+  content: "\e797";
+}
+
+.icon-add_a:before {
+  content: "\e796";
+}
+
+.icon-rectification:before {
+  content: "\e78a";
+}
+
+.icon-a-zoom:before {
+  content: "\e78b";
+}
+
+.icon-a-play:before {
+  content: "\e78c";
+}
+
+.icon-a-animation_s:before {
+  content: "\e78d";
+}
+
+.icon-keys:before {
+  content: "\e78e";
+}
+
+.icon-a-anchor:before {
+  content: "\e78f";
+}
+
+.icon-a-monitoring_s:before {
+  content: "\e790";
+}
+
+.icon-a-rotate:before {
+  content: "\e791";
+}
+
+.icon-a-move:before {
+  content: "\e792";
+}
+
+.icon-a-path_s:before {
+  content: "\e793";
+}
+
+.icon-a-pause:before {
+  content: "\e794";
+}
+
+.icon-a-guide_s:before {
+  content: "\e795";
+}
+
+.icon-magnify:before {
+  content: "\e788";
+}
+
+.icon-reduce:before {
+  content: "\e789";
+}
+
 .icon-pic_path:before {
   content: "\e778";
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
src/components/bill-ui/components/icon/iconfont/iconfont.js


+ 224 - 0
src/components/bill-ui/components/icon/iconfont/iconfont.json

@@ -6,6 +6,230 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "45357208",
+      "name": "orientation",
+      "font_class": "orientation",
+      "unicode": "e7bf",
+      "unicode_decimal": 59327
+    },
+    {
+      "icon_id": "44890707",
+      "name": "window_n",
+      "font_class": "window_n",
+      "unicode": "e7b4",
+      "unicode_decimal": 59316
+    },
+    {
+      "icon_id": "44890706",
+      "name": "window_m",
+      "font_class": "window_m",
+      "unicode": "e7b5",
+      "unicode_decimal": 59317
+    },
+    {
+      "icon_id": "44889955",
+      "name": "screen_s",
+      "font_class": "screen_s",
+      "unicode": "e7b2",
+      "unicode_decimal": 59314
+    },
+    {
+      "icon_id": "44889954",
+      "name": "3D_scence",
+      "font_class": "a-3D_scence",
+      "unicode": "e7b3",
+      "unicode_decimal": 59315
+    },
+    {
+      "icon_id": "44887769",
+      "name": "media",
+      "font_class": "media",
+      "unicode": "e7af",
+      "unicode_decimal": 59311
+    },
+    {
+      "icon_id": "44887770",
+      "name": "rename",
+      "font_class": "rename",
+      "unicode": "e7b0",
+      "unicode_decimal": 59312
+    },
+    {
+      "icon_id": "44887771",
+      "name": "display",
+      "font_class": "display",
+      "unicode": "e7b1",
+      "unicode_decimal": 59313
+    },
+    {
+      "icon_id": "43973886",
+      "name": "line_d",
+      "font_class": "line_d",
+      "unicode": "e79e",
+      "unicode_decimal": 59294
+    },
+    {
+      "icon_id": "43913586",
+      "name": "a-label_s",
+      "font_class": "a-label_s",
+      "unicode": "e79d",
+      "unicode_decimal": 59293
+    },
+    {
+      "icon_id": "43886962",
+      "name": "guide_p",
+      "font_class": "guide_p",
+      "unicode": "e79c",
+      "unicode_decimal": 59292
+    },
+    {
+      "icon_id": "43623992",
+      "name": "view",
+      "font_class": "view",
+      "unicode": "e79b",
+      "unicode_decimal": 59291
+    },
+    {
+      "icon_id": "43616883",
+      "name": "ratio",
+      "font_class": "ratio",
+      "unicode": "e799",
+      "unicode_decimal": 59289
+    },
+    {
+      "icon_id": "43616882",
+      "name": "1b1",
+      "font_class": "a-1b1",
+      "unicode": "e79a",
+      "unicode_decimal": 59290
+    },
+    {
+      "icon_id": "25654903",
+      "name": "reset",
+      "font_class": "reset",
+      "unicode": "e65a",
+      "unicode_decimal": 58970
+    },
+    {
+      "icon_id": "43615153",
+      "name": "menu",
+      "font_class": "menu",
+      "unicode": "e798",
+      "unicode_decimal": 59288
+    },
+    {
+      "icon_id": "43559284",
+      "name": "keys_a",
+      "font_class": "keys_a",
+      "unicode": "e797",
+      "unicode_decimal": 59287
+    },
+    {
+      "icon_id": "43559283",
+      "name": "add_a",
+      "font_class": "add_a",
+      "unicode": "e796",
+      "unicode_decimal": 59286
+    },
+    {
+      "icon_id": "43549167",
+      "name": "rectification",
+      "font_class": "rectification",
+      "unicode": "e78a",
+      "unicode_decimal": 59274
+    },
+    {
+      "icon_id": "43549165",
+      "name": "a-zoom",
+      "font_class": "a-zoom",
+      "unicode": "e78b",
+      "unicode_decimal": 59275
+    },
+    {
+      "icon_id": "43549159",
+      "name": "a-play",
+      "font_class": "a-play",
+      "unicode": "e78c",
+      "unicode_decimal": 59276
+    },
+    {
+      "icon_id": "43549156",
+      "name": "a-animation_s",
+      "font_class": "a-animation_s",
+      "unicode": "e78d",
+      "unicode_decimal": 59277
+    },
+    {
+      "icon_id": "43549164",
+      "name": "keys",
+      "font_class": "keys",
+      "unicode": "e78e",
+      "unicode_decimal": 59278
+    },
+    {
+      "icon_id": "43549163",
+      "name": "a-anchor",
+      "font_class": "a-anchor",
+      "unicode": "e78f",
+      "unicode_decimal": 59279
+    },
+    {
+      "icon_id": "43549160",
+      "name": "a-monitoring_s",
+      "font_class": "a-monitoring_s",
+      "unicode": "e790",
+      "unicode_decimal": 59280
+    },
+    {
+      "icon_id": "43549162",
+      "name": "a-rotate",
+      "font_class": "a-rotate",
+      "unicode": "e791",
+      "unicode_decimal": 59281
+    },
+    {
+      "icon_id": "43549161",
+      "name": "a-move",
+      "font_class": "a-move",
+      "unicode": "e792",
+      "unicode_decimal": 59282
+    },
+    {
+      "icon_id": "43549158",
+      "name": "a-path_s",
+      "font_class": "a-path_s",
+      "unicode": "e793",
+      "unicode_decimal": 59283
+    },
+    {
+      "icon_id": "43549157",
+      "name": "a-pause",
+      "font_class": "a-pause",
+      "unicode": "e794",
+      "unicode_decimal": 59284
+    },
+    {
+      "icon_id": "43549155",
+      "name": "a-guide_s",
+      "font_class": "a-guide_s",
+      "unicode": "e795",
+      "unicode_decimal": 59285
+    },
+    {
+      "icon_id": "43549168",
+      "name": "magnify",
+      "font_class": "magnify",
+      "unicode": "e788",
+      "unicode_decimal": 59272
+    },
+    {
+      "icon_id": "43549166",
+      "name": "reduce",
+      "font_class": "reduce",
+      "unicode": "e789",
+      "unicode_decimal": 59273
+    },
+    {
       "icon_id": "42880568",
       "name": "pic_path",
       "font_class": "pic_path",

BIN=BIN
src/components/bill-ui/components/icon/iconfont/iconfont.ttf


BIN=BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff


BIN=BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff2


+ 26 - 15
src/components/bill-ui/components/input/checkbox.vue

@@ -1,20 +1,31 @@
 <template>
-    <div class="input checkbox" :style="{ width, height }">
-        <input :id="id" type="checkbox" class="replace-input" :checked="props.modelValue" @input="ev => emit('update:modelValue', ev.target.checked)" />
-        <span class="replace">
-            <icon type="checkbox" :size="width > height ? height : width" />
-        </span>
-    </div>
-    <label class="label" v-if="props.label" :for="id">
-        {{ props.label }}
-    </label>
+  <div class="input checkbox" :style="{ width, height }">
+    <input
+      :id="id"
+      type="checkbox"
+      class="replace-input"
+      :checked="props.modelValue"
+      @input="updateInput"
+    />
+    <span class="replace">
+      <icon type="checkbox" :size="width > height ? height : width" />
+    </span>
+  </div>
+  <label class="label" v-if="props.label" :for="id">
+    {{ props.label }}
+  </label>
 </template>
 
 <script setup>
-import icon from '../icon'
-import { checkboxPropsDesc } from './state'
-import { randomId } from '../../utils'
-const props = defineProps(checkboxPropsDesc)
-const emit = defineEmits(['update:modelValue'])
-const id = randomId(4)
+import icon from "../icon";
+import { checkboxPropsDesc } from "./state";
+import { randomId } from "../../utils";
+const props = defineProps(checkboxPropsDesc);
+const emit = defineEmits(["update:modelValue"]);
+const id = randomId(4);
+
+const updateInput = (ev) => {
+  console.error("ev.target.checked", ev.target.checked);
+  emit("update:modelValue", ev.target.checked);
+};
 </script>

+ 4 - 3
src/components/bill-ui/components/message/index.js

@@ -37,12 +37,13 @@ Message.use = function use(app) {
     const existsShows = []
     const oneShow = config => {
         const key = config.type + config.msg
+        console.log(existsShows, key)
         if (!existsShows.includes(key)) {
-            const index = existsShows.length
-            existsShows[index] = key
+            existsShows.push(key)
             Message.show(config)
             setTimeout(() => {
-                existsShows.splice(index, 1)
+                const index = existsShows.indexOf(key)
+                ~index && existsShows.splice(index, 1)
             }, config.time + 1000)
         }
     }

+ 94 - 93
src/components/bill-ui/components/slide/index.vue

@@ -1,114 +1,115 @@
 <template>
-    <div class="ui-slide" :class="{'stop-animation': stopAmimation}" v-if="items.length">
-        <Gate :index="extendIndex">
-            <GateContent v-for="(item, i) in extendItems">
-                <slot :raw="item" :active="items[index]" :index="getIndex(i)" />
-            </GateContent>
-        </Gate>
-        <template v-if="showCtrl">
-            <span class="left fun-ctrl" @click="prevHandler"><UIIcon type="left1" /></span>
-            <span class="right fun-ctrl" @click="nextHandler"><UIIcon type="right" /></span>
-        </template>
-        <slot name="attach" :active="items[index]" />
+  <div class="ui-slide" :class="{ 'stop-animation': stopAmimation }" v-if="items.length">
+    <Gate :index="extendIndex">
+      <GateContent v-for="(item, i) in extendItems">
+        <slot :raw="item" :active="items[index]" :index="getIndex(i)" />
+      </GateContent>
+    </Gate>
+    <template v-if="showCtrl">
+      <span class="left fun-ctrl" @click="prevHandler"><UIIcon type="left1" /></span>
+      <span class="right fun-ctrl" @click="nextHandler"><UIIcon type="right" /></span>
+    </template>
+    <slot name="attach" :active="items[index]" />
 
-        <span class="infos" v-if="showInfos">
-            <span class="tj">
-                <span>{{ index + 1 }}</span> / {{ items.length }}
-            </span>
-        </span>
-    </div>
+    <span class="infos" v-if="showInfos">
+      <span class="tj">
+        <span>{{ index + 1 }}</span> / {{ items.length }}
+      </span>
+    </span>
+  </div>
 </template>
 
 <script setup>
-import { Gate, GateContent } from '../gate'
-import { ref, watchEffect, computed } from 'vue'
-import UIIcon from '../icon'
-import { nextTick } from 'vue';
+import { Gate, GateContent } from "../gate";
+import { ref, watchEffect, computed } from "vue";
+import UIIcon from "../icon";
+import { nextTick } from "vue";
 
 const props = defineProps({
-    items: Array,
-    currentIndex: {
-        type: Number,
-        default: 0,
-    },
-    showCtrl: {
-        type: Boolean,
-    },
-    showInfos: {
-        type: Boolean,
-    },
-})
-const emit = defineEmits(['change'])
-const extendIndex = ref()
-const extendLength = computed(() => props.items.length > 1 ? 1 : 0)
+  items: Array,
+  currentIndex: {
+    type: Number,
+    default: 0,
+  },
+  showCtrl: {
+    type: Boolean,
+  },
+  showInfos: {
+    type: Boolean,
+  },
+});
+const emit = defineEmits(["change"]);
+const extendIndex = ref();
+// const extendLength = computed(() => (props.items.length > 1 ? 1 : 0));
+const extendLength = computed(() => 0);
 const getIndex = (extendIndex) => {
-    const len = props.items.length
-    const diff = extendIndex - extendLength.value
+  const len = props.items.length;
+  const diff = extendIndex - extendLength.value;
 
-    if (diff < 0) {
-        return diff + len
-    } else if (diff >= len) {
-        return diff % len
-    } else {
-        return diff
-    }
-}
+  if (diff < 0) {
+    return diff + len;
+  } else if (diff >= len) {
+    return diff % len;
+  } else {
+    return diff;
+  }
+};
 const extendItems = computed(() => {
-    if (extendLength.value) {
-        const reverItems = [...props.items].reverse()
-        return [
-            ...reverItems.slice(0, extendLength.value),
-            ...props.items,
-            ...props.items.slice(0, extendLength.value)
-        ]
-    } else {
-        return props.items
-    }
-})
+  if (extendLength.value) {
+    const reverItems = [...props.items].reverse();
+    return [
+      ...reverItems.slice(0, extendLength.value),
+      ...props.items,
+      ...props.items.slice(0, extendLength.value),
+    ];
+  } else {
+    return props.items;
+  }
+});
 
-const index = computed(() => getIndex(extendIndex.value))
+const index = computed(() => getIndex(extendIndex.value));
 
 watchEffect(() => {
-    extendIndex.value = props.currentIndex + extendLength.value
-})
+  extendIndex.value = props.currentIndex + extendLength.value;
+});
 
-const stopAmimation = ref(false)
-let prevent = false
+const stopAmimation = ref(false);
+let prevent = false;
 const openPrevent = (fn) => {
-    prevent = true
-    setTimeout(() => {
-        stopAmimation.value = true
-        nextTick(() => {
-            fn()
-            setTimeout(() => {
-                stopAmimation.value = false
-                prevent = false
-            }, 50)
-        })
-    }, 300)
-}
+  prevent = true;
+  setTimeout(() => {
+    stopAmimation.value = true;
+    nextTick(() => {
+      fn();
+      setTimeout(() => {
+        stopAmimation.value = false;
+        prevent = false;
+      }, 50);
+    });
+  }, 300);
+};
 const prevHandler = () => {
-    if (prevent) return;
-    if (index.value === 0) {
-        openPrevent(() => {
-            extendIndex.value = extendLength.value + props.items.length  - 1
-        })
-    }
-    extendIndex.value--
-    emit('change', index.value)
-}
+  if (prevent) return;
+  if (index.value === 0) {
+    openPrevent(() => {
+      extendIndex.value = extendLength.value + props.items.length - 1;
+    });
+  }
+  extendIndex.value--;
+  emit("change", index.value);
+};
 const nextHandler = () => {
-    if (prevent) return;
-    if (index.value === props.items.length - 1) {
-        openPrevent(() => {
-            extendIndex.value = extendLength.value
-        })
-    }
-    extendIndex.value++
-    emit('change', index.value)
-}
+  if (prevent) return;
+  if (index.value === props.items.length - 1) {
+    openPrevent(() => {
+      extendIndex.value = extendLength.value;
+    });
+  }
+  extendIndex.value++;
+  emit("change", index.value);
+};
 </script>
 
 <script>
-export default { name: 'ui-slide' }
+export default { name: "ui-slide" };
 </script>

+ 94 - 0
src/components/drawing-time-line/action.vue

@@ -0,0 +1,94 @@
+<template>
+  <template v-for="(rect, i) in rects">
+    <v-rect
+      :config="{ ...rect, fill: activeNdx === i ? '#00c8af' : '#fff' }"
+      :ref="(s: any) => actionShapes[i] = s"
+    />
+    <v-text :config="getTextConfig(i)" />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import {
+  useGlobalVar,
+  useViewerInvertTransformConfig,
+  useViewerTransform,
+} from "../drawing/hook";
+import { DC } from "../drawing/dec";
+import { Rect } from "konva/lib/shapes/Rect";
+import { Transform } from "konva/lib/Util";
+import { lineLen } from "../drawing/math";
+
+const { misPixel } = useGlobalVar();
+
+const props = defineProps<{
+  items: { time: number; duration: number; name: string }[];
+  activeNdx?: number;
+  top: number;
+}>();
+
+const fontSize = 14;
+const viewerMat = useViewerTransform();
+const getTextConfig = (ndx: number) => {
+  const rect = rects.value[ndx];
+  let name = props.items[ndx].name;
+  const rectWidth = lineLen(
+    viewerMat.value.point(rect),
+    viewerMat.value.point({ x: rect.x + rect.width, y: rect.y })
+  );
+  const nameWidth = name.length * fontSize;
+
+  if (rectWidth > 14) {
+    if (nameWidth > rectWidth) {
+      const len = Math.floor(rectWidth / fontSize) - 4;
+      name = name.substring(0, len) + "...";
+    }
+  } else {
+    name = "";
+  }
+
+  const dec = new Transform()
+    .translate(rect.x, rect.y)
+    .scale(invConfig.value.scaleX, 1)
+    .decompose();
+  return {
+    ...dec,
+    width: rect.width / invConfig.value.scaleX,
+    height: rect.height / invConfig.value.scaleY,
+    fill: ndx === props.activeNdx ? "#00c8af" : "#fff",
+    fontSize: 14,
+    listening: false,
+    align: "center",
+    verticalAlign: "middle",
+    text: name,
+  };
+};
+
+const invConfig = useViewerInvertTransformConfig();
+const rects = computed(() => {
+  return props.items.map((item) => {
+    const origin = { x: item.time * misPixel, y: props.top + 1 };
+    const end = {
+      x: (item.time + item.duration) * misPixel,
+      y: props.top + 28,
+    };
+
+    return {
+      ...origin,
+      cornerRadius: 5 * invConfig.value.scaleX,
+      width: end.x - origin.x,
+      fill: "#fff",
+      opacity: 0.16,
+      height: end.y - origin.y,
+    };
+  });
+});
+
+const actionShapes = ref<DC<Rect>[]>([]);
+defineExpose({
+  get shapes() {
+    return actionShapes.value;
+  },
+});
+</script>

+ 99 - 0
src/components/drawing-time-line/check.ts

@@ -0,0 +1,99 @@
+import { round } from "@/utils";
+
+export type TLItem = { time: number; duration?: number };
+
+export const getAddTLItemAttr = (
+  items: TLItem[],
+  cur: number,
+  maxDur: number,
+  minDur: number
+) => {
+  items = [...items].sort((a, b) => a.time - b.time);
+
+  let last: TLItem | null = null;
+  let start: TLItem | null = null;
+  for (let i = 0; i < items.length; i++) {
+    if (
+      cur >= items[i].time &&
+      cur <= items[i].time + (items[i].duration || 0)
+    ) {
+      return null;
+    }
+    if (items[i].time > cur) {
+      last = items[i];
+      break;
+    }
+    if (items[i].time + (items[i].duration || 0) < cur) {
+      start = items[i];
+    }
+  }
+
+  let dur: number = 0;
+  if (!last) {
+    return { time: cur, duration: maxDur };
+  } else {
+    dur = Math.min(maxDur, last.time - cur);
+    if (dur < minDur) {
+      return null;
+    } else {
+      return { time: cur, duration: round(dur, 2) };
+    }
+  }
+};
+
+export const checkTLItem = (items: TLItem[], cur: TLItem, i?: number) => {
+  if (cur.time < 0) return false
+  const exRects = items
+    .filter((_, ndx) => ndx !== i)
+    .sort((a, b) => a.time - b.time)
+    .map((item) => ({
+      x: item.time,
+      xe: item.time + (item.duration || 0.5),
+    }));
+
+  const curRect = { x: cur.time, xe: cur.time + (cur.duration || 0.5) };
+  for (let i = 0; i < exRects.length; i++) {
+    const exRect = exRects[i];
+    if (
+      (curRect.x > exRect.x && curRect.x < exRect.xe) ||
+      (curRect.xe > exRect.x && curRect.xe < exRect.xe) ||
+      (exRect.x > curRect.x && exRect.x < curRect.xe) ||
+      (exRect.xe > curRect.x && exRect.xe < curRect.xe)
+    ) {
+      return false;
+    }
+  }
+  return true;
+};
+
+export const getAddTLItemTime = (items: TLItem[], refNdx = 0, dur = 0.5) => {
+  const ref = items[refNdx];
+  return getAddTLItemTimeByTime(items, ref.time + (ref.duration || 0) + 0.001, dur)
+
+  // items = [...items].sort((a, b) => a.time - b.time);
+  // refNdx = items.indexOf(ref);
+
+  // console.log(items, refNdx)
+  // let time = 0;
+  // for (let i = refNdx; i < items.length; i++) {
+  //   time = items[i].time + (items[i].duration || 0.5);
+  //   if (checkTLItem([...items].splice(i), { time, duration: dur })) {
+  //     break;
+  //   }
+  // }
+
+  // return time;
+};
+
+
+export const getAddTLItemTimeByTime = (items: TLItem[], curTime: number, dur: number): ReturnType<typeof getAddTLItemAttr> => {
+  let tl
+  if (tl = getAddTLItemAttr(items, curTime, dur, Math.min(1, dur))) {
+    return tl
+  }
+
+  items = [...items].sort((a, b) => a.time - b.time);
+  let refNdx = items.findIndex(item => item.time >= curTime)
+  refNdx = ~refNdx ? refNdx : items.length - 1
+  return getAddTLItemTimeByTime(items, items[refNdx].time + (items[refNdx].duration || 0.01) + 0.001, dur)
+};

+ 22 - 0
src/components/drawing-time-line/empty.vue

@@ -0,0 +1,22 @@
+<template>
+  <v-text
+    :config="{
+      text: '未添加动画',
+      height: size?.height,
+      width: size?.width,
+      fill: '#fff',
+      fontSize: 14,
+      opacity: 0.3,
+      align: 'center',
+      verticalAlign: 'middle',
+      ...invConfig,
+      y: 20,
+    }"
+  />
+</template>
+<script lang="ts" setup>
+import { useGlobalResize, useViewerInvertTransformConfig } from "../drawing/hook";
+
+const { size } = useGlobalResize();
+const invConfig = useViewerInvertTransformConfig();
+</script>

+ 61 - 0
src/components/drawing-time-line/frame.vue

@@ -0,0 +1,61 @@
+<template>
+  <template v-for="(l, i) in lines">
+    <v-line
+      :config="{ ...l, fill: activeNdx === i ? '#00c8af' : '#fff' }"
+      :ref="(s: any) => frameShapes[i] = s"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from "vue";
+import {
+  flatPositions,
+  useGlobalVar,
+  useViewerInvertTransformConfig,
+} from "../drawing/hook";
+import { Transform } from "konva/lib/Util";
+import { Line, LineConfig } from "konva/lib/shapes/Line";
+import { DC } from "../drawing/dec";
+
+const { misPixel } = useGlobalVar();
+
+const props = defineProps<{
+  items: { time: number }[];
+  top: number;
+  activeNdx?: number;
+}>();
+const s = 6;
+const invConfig = useViewerInvertTransformConfig();
+const lines = computed(() => {
+  return props.items.map((item) => {
+    const origin = { x: item.time * misPixel, y: props.top + 10 };
+    const points = [
+      { x: origin.x - s / 2, y: origin.y },
+      { x: origin.x + s / 2, y: origin.y },
+      { x: origin.x + s / 2, y: origin.y + s * 1.5 },
+      { x: origin.x, y: origin.y + s * 2 },
+      { x: origin.x - s / 2, y: origin.y + s * 1.5 },
+    ];
+
+    return {
+      points: flatPositions(points),
+      closed: true,
+      fill: "#fff",
+      hitStrokeWidth: 5,
+      ...new Transform()
+        .translate(origin.x, 0)
+        .scale(invConfig.value.scaleX, 1)
+        .translate(-origin.x, 0)
+        .decompose(),
+    } as LineConfig;
+  });
+});
+
+const frameShapes = ref<DC<Line>[]>([]);
+defineExpose({
+  get shapes() {
+    return frameShapes.value;
+  },
+});
+</script>

+ 125 - 0
src/components/drawing-time-line/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <v-rect
+    v-if="background"
+    :config="{
+      width: size?.width,
+      height: height,
+      fill: background ? background : '#000',
+      opacity: opacity,
+      ...bgConfig,
+    }"
+  />
+
+  <component
+    :is="itemsRenderer"
+    :items="items"
+    :top="top"
+    :activeNdx="active ? items.indexOf(active) : -1"
+    :ref="(r: any) => itemShapes = r ? r.shapes : []"
+  />
+  <template v-for="(itemShape, i) in itemShapes">
+    <Operate
+      v-if="itemShape"
+      :target="itemShape"
+      :menus="[
+        {
+          label: '复制',
+          handler: () => copyHandler(i),
+        },
+        {
+          label: '删除',
+          handler: () => delHandler(i),
+        },
+      ]"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch, watchEffect } from "vue";
+import {
+  useDrag,
+  useGlobalResize,
+  useGlobalVar,
+  useViewerInvertTransform,
+} from "../drawing/hook";
+import { Transform } from "konva/lib/Util";
+import { DC, EntityShape } from "../drawing/dec";
+import Operate from "../drawing/operate.vue";
+import { checkTLItem, getAddTLItemTime, TLItem } from "./check";
+
+const { misPixel } = useGlobalVar();
+const { size } = useGlobalResize();
+
+const props = defineProps<{
+  items: TLItem[];
+  itemsRenderer: any;
+  background?: string;
+  height: number;
+  opacity?: number;
+  top: number;
+  active?: TLItem;
+}>();
+const emit = defineEmits<{
+  (e: "update:active", data: TLItem | undefined): void;
+  (e: "update", data: { ndx: number; time: number }): void;
+  (e: "add", data: any): void;
+  (e: "del", ndx: number): void;
+}>();
+
+const invMat = useViewerInvertTransform();
+const bgConfig = computed(() => {
+  return new Transform()
+    .multiply(invMat.value.copy())
+    .translate(0, props.top)
+    .decompose();
+});
+
+const itemShapes = ref<DC<EntityShape>[]>([]);
+const { drag } = useDrag(itemShapes);
+let total = { x: 0, y: 0 };
+watch(drag, (drag) => {
+  if (!drag) {
+    total = { x: 0, y: 0 };
+    return;
+  }
+  const cur = props.items[drag.ndx];
+  if (
+    checkTLItem(
+      props.items,
+      { ...cur, time: cur.time + (total.x + drag.x) / misPixel },
+      drag.ndx
+    )
+  ) {
+    const curX = cur.time * misPixel + total.x + drag.x;
+    emit("update", { ndx: drag.ndx, time: curX / misPixel });
+    total = { x: 0, y: 0 };
+  } else {
+    total.x += drag.x;
+    total.y += drag.y;
+  }
+});
+
+watchEffect((onCleanup) => {
+  for (let i = 0; i < itemShapes.value.length; i++) {
+    const $shape = itemShapes.value[i]?.getNode();
+    if (!$shape) continue;
+    $shape.on("click.uactive", () => {
+      emit("update:active", props.active === props.items[i] ? undefined : props.items[i]);
+    });
+    onCleanup(() => $shape.off("click.uactive"));
+  }
+});
+
+const copyHandler = (ndx: number) => {
+  const newFrame = {
+    ...props.items[ndx],
+    ...getAddTLItemTime(props.items, ndx, props.items[ndx].duration)!
+  };
+  emit("add", newFrame);
+};
+
+const delHandler = (ndx: number) => {
+  emit("del", ndx);
+};
+</script>

+ 108 - 0
src/components/drawing-time/current.vue

@@ -0,0 +1,108 @@
+<template>
+  <v-arrow
+    :ref="(r: any) => arrow[0] = r"
+    :config="{
+      fill: currentColor,
+      stroke: currentColor,
+      strokeWidth: 1,
+      pointerLength: 10,
+      hitStrokeWidth: 10,
+      pointerWidth: 10,
+      ...currentMat,
+    }"
+  />
+  <v-line
+    v-if="!hideLine"
+    :ref="(r: any) => arrow[1] = r"
+    :config="{
+      stroke: currentColor,
+      strokeWidth: 2,
+      hitStrokeWidth: 4,
+      ...currentMat,
+    }"
+  />
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch, watchEffect } from "vue";
+import {
+  useDrag,
+  useGlobalResize,
+  useGlobalVar,
+  useViewer,
+  useViewerInvertTransformConfig,
+  useViewerTransform,
+} from "../drawing/hook";
+import { Transform } from "konva/lib/Util";
+import { Arrow } from "konva/lib/shapes/Arrow";
+import { DC } from "../drawing/dec";
+import { Line } from "konva/lib/shapes/Line";
+
+const props = withDefaults(
+  defineProps<{
+    currentTime: number;
+    follow?: boolean;
+    hideLine?: boolean;
+    currentColor?: string;
+  }>(),
+  {
+    follow: false,
+    currentColor: "#fff",
+  }
+);
+
+const currentColor = props.currentColor;
+const { misPixel } = useGlobalVar();
+const emit = defineEmits<{ (e: "update:currentTime", num: number): void }>();
+
+const arrow = ref<DC<Arrow>[]>([]);
+const { drag } = useDrag(arrow);
+watch(drag, (drag) => {
+  if (!drag) return;
+  const offsetX = drag.x;
+
+  const currentX = props.currentTime * misPixel + offsetX;
+  console.log(offsetX, currentX / misPixel);
+  currentX >= 0 && emit("update:currentTime", currentX / misPixel);
+});
+
+const invConfig = useViewerInvertTransformConfig();
+const currentX = computed(() => props.currentTime * misPixel);
+const currentMat = computed(() => {
+  return new Transform()
+    .translate(currentX.value, 0)
+    .scale(invConfig.value.scaleX, 1)
+    .translate(-currentX.value, 0)
+    .decompose();
+});
+
+const { viewer } = useViewer();
+const { size } = useGlobalResize();
+const viewerMat = useViewerTransform();
+watch(
+  () => {
+    if (!props.follow || !size.value) return;
+    return currentX.value;
+  },
+  (x) => {
+    if (typeof x !== "number") return;
+    const currentPixel = viewerMat.value.point({ x: currentX.value, y: 0 }).x;
+    const offsetX = size.value!.width / 2 - currentPixel;
+    viewer.movePixel({ x: offsetX, y: 0 });
+  }
+);
+
+watch(
+  () => ({
+    x: currentX.value,
+    h: size.value!.height,
+    shapes: arrow.value.map((item) => item.getNode()),
+  }),
+  ({ x, shapes }) => {
+    if (shapes[0] && shapes[1]) {
+      shapes[0].points([x, 0, x, 10]);
+      shapes[1].points([x, size.value!.height, x, 10]);
+    }
+  }
+);
+</script>

+ 120 - 0
src/components/drawing-time/time.vue

@@ -0,0 +1,120 @@
+<template>
+  <v-group v-if="shapeConfig">
+    <v-line v-for="line in shapeConfig.lines" :config="line" />
+    <v-text v-for="texConfig in shapeConfig.texts" :config="{ ...texConfig }" />
+    <v-line
+      :config="{
+        points: [0, 0, size?.width, 0],
+        strokeWidth: 30,
+        stroke: '#fff',
+        opacity: 0,
+        ...invConfig,
+      }"
+      ref="line"
+    />
+    <slot />
+  </v-group>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import {
+  useClickHandler,
+  useGlobalResize,
+  useGlobalVar,
+  useHoverPointer,
+  useStage,
+  useViewerInvertTransform,
+  useViewerInvertTransformConfig,
+  useViewerTransform,
+} from "../drawing/hook";
+import { formatDate } from "@/utils";
+import { TextConfig } from "konva/lib/shapes/Text";
+import { Transform } from "konva/lib/Util";
+import { Line, LineConfig } from "konva/lib/shapes/Line";
+import { DC } from "../drawing/dec";
+
+const { misPixel } = useGlobalVar();
+const emit = defineEmits<{ (e: "updateCurrentTime", num: number): void }>();
+
+const getText = (mis: number) => {
+  const date = new Date(0);
+  date.setHours(0);
+  date.setSeconds(mis);
+  const h = date.getHours();
+  return h ? formatDate(date, "hh:mm:ss") : formatDate(date, "mm:ss");
+};
+
+const viewMat = useViewerTransform();
+const invMat = useViewerInvertTransform();
+const invConfig = useViewerInvertTransformConfig();
+const { size } = useGlobalResize();
+
+const timeRange = computed(() => {
+  if (!size.value) return;
+  const start = viewMat.value.point({ x: 0, y: 0 }).x;
+  const lt = { x: start > 0 ? start : 0, y: 0 };
+  const rt = { x: size.value!.width, y: 0 };
+  const startPixel = invMat.value.point(lt).x;
+  const endPixel = invMat.value.point(rt).x;
+  const startTime = Math.floor(startPixel / misPixel);
+  const endTime = Math.ceil(endPixel / misPixel);
+
+  return { startTime, endTime };
+});
+
+const strokeWidth = 1;
+const color = "#999";
+
+const shapeConfig = computed(() => {
+  if (!timeRange.value) return;
+
+  const texts: TextConfig[] = [];
+  const lines: LineConfig[] = [];
+
+  for (let i = timeRange.value.startTime; i < timeRange.value.endTime; i++) {
+    const x = i * misPixel;
+    const line: LineConfig = {
+      ...new Transform()
+        .translate(x, 0)
+        .scale(invConfig.value.scaleX, 1)
+        .translate(-x, 0)
+        .decompose(),
+      stroke: color,
+      hitStrokeWidth: 5,
+      strokeWidth,
+    };
+
+    if (i % 10) {
+      line.points = [x, 0, x, 4];
+    } else {
+      line.points = [x, 0, x, 12];
+      texts.push({
+        ...new Transform()
+          .translate(x + 5 * invConfig.value.scaleX, 5)
+          .scale(invConfig.value.scaleX, 1)
+          .decompose(),
+        text: getText(i),
+        fontSize: strokeWidth * 12,
+        fill: color,
+        align: "left",
+        verticalAlign: "top",
+      });
+    }
+    lines.push(line);
+  }
+
+  return { texts, lines };
+});
+
+const line = ref<DC<Line>>();
+useHoverPointer(line);
+
+const stage = useStage();
+const clickHandler = () => {
+  const pos = stage.value!.getNode().pointerPos!;
+  const x = invMat.value.point(pos).x;
+  emit("updateCurrentTime", x / misPixel);
+};
+useClickHandler(line, clickHandler);
+</script>

+ 12 - 0
src/components/drawing/dec.d.ts

@@ -0,0 +1,12 @@
+import Konva from "konva";
+
+type DC<T extends any> = {
+	getNode: () => T,
+	getStage: () => T
+}
+
+type EntityShape = (Konva.Shape | Konva.Stage | Konva.Layer | Konva.Group) & { repShape?: EntityShape }
+
+
+export type Pos = { x: number; y: number };
+export type Size = { width: number, height: number }

+ 749 - 0
src/components/drawing/hook.ts

@@ -0,0 +1,749 @@
+import {
+  computed,
+  getCurrentInstance,
+  nextTick,
+  reactive,
+  Ref,
+  ref,
+  shallowRef,
+  toRaw,
+  watch,
+  WatchCallback,
+  watchEffect,
+  WatchOptions,
+  WatchSource,
+} from "vue";
+import { v4 as uuidRaw } from "uuid";
+import { DC, EntityShape, Pos, Size } from "./dec";
+import { Stage } from "konva/lib/Stage";
+import { Transform } from "konva/lib/Util";
+import { lineLen } from "./math";
+import { Viewer } from "./viewer";
+import { KonvaEventObject } from "konva/lib/Node";
+import { asyncTimeout } from "@/utils";
+
+export const rendererName = "renderer";
+export const rendererMap = new WeakMap<any, { unmounteds: (() => void)[] }>();
+export const uuid = uuidRaw
+export const useRendererInstance = () => {
+  let instance = getCurrentInstance()!;
+  while (instance.type.__name !== rendererName) {
+    if (instance.parent) {
+      instance = instance.parent;
+    } else {
+      throw "未发现渲染实例";
+    }
+  }
+  return instance;
+};
+
+export const installGlobalVar = <T>(
+  create: () => { var: T; onDestroy: () => void } | T,
+  key = Symbol("globalVar")
+) => {
+  const useGlobalVar = (): T => {
+    const instance = useRendererInstance() as any;
+    const { unmounteds } = rendererMap.get(instance)!;
+    if (!(key in instance)) {
+      let val = create() as any;
+      if (typeof val === "object" && "var" in val && "onDestroy" in val) {
+        console.error('val.onDestory', val, key, val.onDestroy)
+        val.onDestroy && unmounteds.push(val.onDestroy);
+        if (import.meta.env.DEV) {
+          unmounteds.push(() => {
+            console.log("销毁变量", key);
+          });
+        }
+        val = val.var;
+      }
+      instance[key] = val;
+    }
+    return instance[key];
+  };
+
+  return useGlobalVar;
+};
+
+export const useGlobalVar = installGlobalVar(() => {
+  return {
+    misPixel: 10,
+  };
+});
+
+export const onlyId = () => uuid();
+
+export const stackVar = <T>(init?: T, test = false) => {
+  const factory = (init: T) => ({ var: init, id: onlyId() });
+  const stack = reactive([]) as { var: T; id: string }[];
+  if (init) {
+    stack.push(factory(init));
+  }
+  const result = {
+    get value() {
+      return stack[stack.length - 1]?.var;
+    },
+    set value(val) {
+      stack[stack.length - 1].var = val;
+    },
+    push(data: T) {
+      test && console.error('push', data)
+      stack.push(factory(data));
+      const item = stack[stack.length - 1];
+      const pop = (() => {
+        const ndx = stack.findIndex(({ id }) => id === item.id);
+        if (~ndx) {
+          stack.splice(ndx, 1);
+        }
+      }) as (() => void) & { set: (data: T) => void };
+      pop.set = (data) => {
+        test && console.error('pop', data)
+        item.var = data;
+      };
+      return pop;
+    },
+    pop() {
+      test && console.error('pop')
+      if (stack.length - 1 > 0) {
+        stack.pop();
+      } else {
+        console.error("已到达栈顶");
+      }
+    },
+    cycle<R>(data: T, run: () => R): R {
+      result.push(data);
+      const r = run();
+      result.pop();
+      return r;
+    },
+  };
+  return result;
+};
+
+export const useCursor = installGlobalVar(
+  () => stackVar("default", false),
+  Symbol("cursor")
+);
+
+/**
+ * 多个函数合并成一个函数
+ * @param fns
+ * @returns
+ */
+export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => {
+  return () => {
+    fns.forEach((fn) => {
+      if (Array.isArray(fn)) {
+        fn.forEach((f) => f());
+      } else {
+        fn();
+      }
+    });
+  };
+};
+
+export const useStage = installGlobalVar(
+  () => shallowRef<DC<Stage> | undefined>(),
+  Symbol("stage")
+);
+
+export const listener = <
+  T extends HTMLElement | Window,
+  K extends keyof HTMLElementEventMap
+>(
+  target: T,
+  eventName: K,
+  callback: (this: T, ev: HTMLElementEventMap[K]) => any
+) => {
+  target.addEventListener(eventName, callback as any);
+  return () => {
+    target.removeEventListener(eventName, callback as any);
+  };
+};
+
+export const useGlobalResize = installGlobalVar(() => {
+  const stage = useStage();
+  const size = ref<Size>();
+  const setSize = async () => {
+    if (fix.value) return;
+    const container = stage.value?.getStage().container();
+    if (container) {
+      container.style.setProperty("display", "none");
+    }
+
+    const dom = stage.value!.getNode().container().parentElement!;
+    await asyncTimeout(16)
+    size.value = {
+      width: dom.offsetWidth,
+      height: dom.offsetHeight,
+    };
+    if (container) {
+      container.style.removeProperty("display");
+    }
+  };
+  const stopWatch = watchEffect(() => {
+    if (stage.value) {
+      setSize();
+      nextTick(() => stopWatch());
+    }
+  });
+  let unResize = listener(window, "resize", setSize);
+  const fix = ref(false);
+  let unWatch: (() => void) | null = null;
+
+  const setFixSize = (fixSize: { width: number; height: number } | null) => {
+    if (fixSize) {
+      size.value = { ...fixSize };
+      unWatch && unWatch();
+      unWatch = watchEffect(() => {
+        const $stage = stage.value?.getStage();
+        if ($stage) {
+          $stage.width(fixSize.width);
+          $stage.height(fixSize.height);
+          nextTick(() => unWatch && unWatch());
+        }
+      });
+    }
+    if (fix.value && !fixSize) {
+      unResize = listener(window, "resize", setSize);
+      fix.value = false;
+      nextTick(setSize);
+    } else if (!fix.value && fixSize) {
+      fix.value = true;
+      unResize();
+    }
+  };
+
+  return {
+    var: {
+      setFixSize: setFixSize,
+      updateSize: setSize,
+      size,
+      fix,
+    },
+    onDestroy: () => {
+      console.error('size onDest')
+      unResize();
+      unWatch && unWatch();
+    },
+  };
+}, Symbol("resize"));
+
+export const globalWatch = <T>(
+  source: WatchSource<T>,
+  cb: WatchCallback<T, T>,
+  options?: WatchOptions
+): (() => void) => {
+  let stop: () => void;
+  nextTick(() => {
+    stop = watch(source, cb as any, options as any);
+  });
+  return () => {
+    stop && stop();
+  };
+};
+
+export const getOffset = (
+  ev: MouseEvent | TouchEvent,
+  dom = ev.target! as HTMLElement,
+  ndx = 0
+) => {
+  const event = (window.TouchEvent && ev instanceof TouchEvent) ? ev.changedTouches[ndx] : ev;
+  const rect = dom.getBoundingClientRect();
+  const offsetX = event.clientX - rect.left;
+  const offsetY = event.clientY - rect.top;
+  return {
+    x: offsetX,
+    y: offsetY,
+  };
+};
+
+type DragProps = {
+  move?: (
+    info: Record<"start" | "prev" | "end", Pos> & { ev: PointerEvent }
+  ) => void;
+  down?: (pos: Pos, ev: PointerEvent) => void;
+  up?: (pos: Pos, ev: PointerEvent) => void;
+  notPrevent?: boolean;
+};
+export const dragListener = (
+  dom: HTMLElement,
+  props: DragProps | DragProps["move"] = {}
+) => {
+  if (typeof props === "function") {
+    props = { move: props };
+  }
+  const { move, up, down } = props;
+  const mount = document.documentElement;
+
+  if (!move && !up && !down) return () => {};
+
+  let moveHandler: any, endHandler: any;
+  let button: number = -1
+  const downHandler = (ev: PointerEvent) => {
+    button = ev.button
+    const start = getOffset(ev, dom);
+    let prev = start;
+    down && down(start, ev);
+    props.notPrevent || ev.preventDefault();
+
+    moveHandler = (ev: PointerEvent) => {
+      if (ev.buttons <= 0) {
+        endHandler()
+        return;
+      }
+      const end = getOffset(ev, dom);
+      move!({ start, end, prev, ev });
+      prev = end;
+
+      props.notPrevent || ev.preventDefault();
+    };
+    endHandler = (ev: PointerEvent) => {
+      up && up(getOffset(ev, dom), ev);
+      mount.removeEventListener("pointermove", moveHandler);
+      mount.removeEventListener("pointerup", endHandler);
+      props.notPrevent || ev.preventDefault();
+    };
+
+    move &&
+      mount.addEventListener("pointermove", moveHandler, { passive: false });
+    mount.addEventListener("pointerup", endHandler, { passive: false });
+  };
+
+  dom.addEventListener("pointerdown", downHandler, { passive: false });
+  return () => {
+    dom.removeEventListener("pointerdown", downHandler);
+    moveHandler && mount.removeEventListener("pointermove", moveHandler);
+    endHandler && mount.removeEventListener("pointerup", endHandler);
+  };
+};
+
+export const getTouchScaleProps = (
+  ev: TouchEvent,
+  dom = ev.target! as HTMLElement
+) => {
+  const start = getOffset(ev, dom, 0);
+  const end = getOffset(ev, dom, 1);
+  const center = {
+    x: (end.x + start.x) / 2,
+    y: (end.y + start.y) / 2,
+  };
+  const initDist = lineLen(start, end);
+  return {
+    center,
+    dist: initDist,
+  };
+};
+
+export const touchScaleListener = (
+  dom: HTMLElement,
+  cb: (props: { center: Pos; scale: number }) => void
+) => {
+  const mount = document.documentElement;
+  let moveHandler: (ev: TouchEvent) => void;
+  let endHandler: (ev: TouchEvent) => void;
+  const startHandler = (ev: TouchEvent) => {
+    if (ev.changedTouches.length <= 1) return;
+    let prevScale = getTouchScaleProps(ev, dom);
+    ev.preventDefault();
+
+    moveHandler = (ev: TouchEvent) => {
+      if (ev.changedTouches.length <= 1) return;
+      const curScale = getTouchScaleProps(ev, dom);
+      cb({ center: prevScale.center, scale: curScale.dist / prevScale.dist });
+      prevScale = curScale;
+      ev.preventDefault();
+    };
+    endHandler = (ev: TouchEvent) => {
+      mount.removeEventListener("touchmove", moveHandler);
+      mount.removeEventListener("touchend", endHandler);
+      ev.preventDefault();
+    };
+
+    mount.addEventListener("touchmove", moveHandler, {
+      passive: false,
+    });
+    mount.addEventListener("touchend", endHandler, {
+      passive: false,
+    });
+  };
+
+  dom.addEventListener("touchstart", startHandler, { passive: false });
+
+  return () => {
+    dom.removeEventListener("touchstart", startHandler);
+    mount.removeEventListener("touchmove", moveHandler);
+    mount.removeEventListener("touchend", endHandler);
+  };
+};
+
+export const wheelListener = (
+  dom: HTMLElement,
+  cb: (props: { center: Pos; scale: number }) => void
+) => {
+  const wheelHandler = (ev: WheelEvent) => {
+    const scale = 1 - ev.deltaY / 1000;
+    const center = { x: ev.offsetX, y: ev.offsetY };
+    cb({ center, scale });
+    ev.preventDefault();
+  };
+
+  dom.addEventListener("wheel", wheelHandler);
+  return () => {
+    dom.removeEventListener("wheel", wheelHandler);
+  };
+};
+
+export const scaleListener = (
+  dom: HTMLElement,
+  cb: (props: { center: Pos; scale: number }) => void
+) => mergeFuns(touchScaleListener(dom, cb), wheelListener(dom, cb));
+
+export const useViewer = installGlobalVar(() => {
+  const stage = useStage();
+  const viewer = new Viewer();
+  const transform = ref(new Transform());
+  const cursor = useCursor();
+
+  const init = (dom: HTMLDivElement) => {
+    viewer.move({x: 10, y: 0})
+    const dragDestroy = dragListener(dom, {
+      move: ({ end, prev, ev }) => {
+        if (cursor.value !== "move") {
+          viewer.movePixel({ x: end.x - prev.x, y: 0 });
+        }
+      },
+      notPrevent: true,
+    });
+    const scaleDestroy = scaleListener(dom, (info) => {
+      const currentScalex = viewer.viewMat.decompose().scaleX;
+      const finalScale = currentScalex * info.scale;
+      const scale = Math.min(Math.max(finalScale, 0.5), 8);
+      if (cursor.value !== "move") {
+        viewer.scalePixel(info.center, { x: scale / currentScalex, y: 1 });
+      }
+    });
+    viewer.bus.on("transformChange", (newTransform) => {
+      // console.log(newTransform.m)
+      transform.value = newTransform;
+    });
+    transform.value = viewer.transform;
+    return mergeFuns(dragDestroy, scaleDestroy);
+  };
+
+  return {
+    var: {
+      transform: transform,
+      viewer,
+    },
+    onDestroy: globalWatch(
+      () => stage.value?.getNode().container(),
+      (dom, _, onCleanup) => {
+        dom && onCleanup(init(dom));
+      },
+      { immediate: true }
+    ),
+  };
+}, Symbol("viewer"));
+
+export const useViewerTransform = installGlobalVar(() => {
+  const viewer = useViewer();
+  return viewer.transform;
+}, Symbol("viewTransform"));
+
+export const useViewerTransformConfig = () => {
+  const transform = useViewerTransform();
+  return computed(() => transform.value.decompose());
+};
+
+export const useViewerInvertTransform = () => {
+  const transform = useViewerTransform();
+  return computed(() => transform.value.copy().invert());
+};
+
+export const useViewerInvertTransformConfig = () => {
+  const transform = useViewerInvertTransform();
+  return computed(() => transform.value.decompose());
+};
+
+export const flatPositions = (positions: Pos[]) =>
+  positions.flatMap((p) => [p.x, p.y]);
+
+export type PausePack<T extends object> = T & {
+  pause: () => void;
+  resume: () => void;
+  isPause: boolean;
+};
+export const usePause = <T extends object>(api?: T): PausePack<T> => {
+  const isPause = ref(false);
+  const result = (api || {}) as PausePack<T>;
+
+  Object.defineProperty(result, "isPause", {
+    get() {
+      return isPause.value;
+    },
+    set(v) {
+      return true;
+    },
+  });
+
+  result.pause = () => (isPause.value = true);
+  result.resume = () => (isPause.value = false);
+
+  return result;
+};
+
+const hoverPointer = (shape: EntityShape, cursor: ReturnType<typeof useCursor>) => {
+  shape.on("pointerenter.hover", () => {
+    if (downing) return;
+    const pop = cursor.push("pointer");
+    shape.on("pointerleave.hover", () => {
+      pop();
+      shape.off("pointerleave.hover");
+    });
+  });
+  return () => {
+    shape.off("pointerenter.hover pointerleave.hover");
+  }
+}
+
+export const useClickHandler = (shape: Ref<DC<EntityShape> |  undefined>, callback: () => void) => {
+  watchEffect((onCleanup) => {
+    if (!shape.value) return;
+    
+    const $shape = shape.value.getNode()
+    let downPos:Pos | null
+    const downHandler = (ev: KonvaEventObject<any>) => {
+      downPos = getOffset(ev.evt)
+      $shape.on('pointerup.clickHandler', upHandler)
+
+    }
+    const upHandler = (ev: KonvaEventObject<any>) => {
+      const upPos = getOffset(ev.evt)
+      if (lineLen(downPos!, upPos) < 0.01) {
+        callback()
+      }
+      $shape.off('pointerup.clickHandler', upHandler)
+    }
+    $shape.on('pointerdown.clickHandler', downHandler)
+
+    onCleanup(() => {
+      $shape.off('pointerdown.clickHandler', downHandler)
+      $shape.off('pointerup.clickHandler', upHandler)
+    })
+  })
+}
+
+let downing = false
+export const useHoverPointer = (shape: Ref<DC<EntityShape> |  undefined>) => {
+  const cursor = useCursor()
+  watchEffect((onCleanup) => {
+    if (shape.value) {
+      console.error('shape.value', shape.value)
+      onCleanup(hoverPointer(shape.value.getNode(), cursor))
+    }
+  })
+  return cursor
+}
+
+export const useDrag = (
+  shape: Ref<DC<EntityShape> | DC<EntityShape>[] | undefined>,
+) => {
+  const cursor = useCursor();
+  const stage = useStage();
+  const drag = ref<Pos & { ndx: number }>();
+  const invMat = useViewerInvertTransform();
+
+  const init = (shape: EntityShape, dom: HTMLDivElement, ndx: number) => {
+    shape.on("pointerenter.drag", () => {
+      if (downing) return;
+      const pop = cursor.push("pointer");
+      shape.on("pointerleave.drag", () => {
+        pop();
+        shape.off("pointerleave.drag");
+      });
+    });
+
+    let pop: (() => void) | null = null;
+    let start = { x: 0, y: 0 }
+    shape.on("pointerdown.drag", (ev) => {
+      downing = true
+      pop = cursor.push("move")
+      start = invMat.value.point(getOffset(ev.evt, stage.value!.getNode().container()));
+      shape.draggable(true);
+    });
+
+    shape.dragBoundFunc(function (this: any, _: any, ev: MouseEvent) {
+      if (ev.buttons <= 0) {
+        upHandler()
+
+        return this.absolutePosition();
+      }
+      const current = invMat.value.point(getOffset(ev, stage.value!.getNode().container()));
+      drag.value = {
+        x: current.x - start.x,
+        y: current.y - start.y,
+        ndx,
+      };
+      start = current
+      return this.absolutePosition();
+    });
+    const upHandler = () => {
+      downing = false
+      pop && pop();
+      pop = null;
+      drag.value = undefined;
+      shape.draggable(false);
+    }
+
+    return mergeFuns(
+      listener(document.documentElement, "pointerup", upHandler),
+      () => {
+        shape.off("pointerenter.drag pointerleave.drag pointerdown.drag");
+        if (pop) {
+          pop();
+          shape.draggable(false);
+        }
+      }
+    );
+  };
+
+  const result = usePause({
+    drag,
+    stop: () => {
+      stopWatch();
+    },
+  });
+
+  const stopWatch = watch(
+    () => {
+      const shapes = shape.value
+        ? Array.isArray(shape.value)
+          ? [...shape.value]
+          : [shape.value]
+        : [];
+      return shapes.filter((item) => !!item)
+    },
+    (shapes, _, onCleanup) => {
+      onCleanup(
+        mergeFuns(
+          shapes.map((shape, ndx) =>
+            watchEffect((onCleanup) => {
+              if (!result.isPause && shape?.getNode() && stage.value?.getNode) {
+                onCleanup(
+                  init(
+                    shape?.getNode(),
+                    stage.value?.getNode().container(),
+                    ndx
+                  )
+                );
+              }
+            })
+          )
+        )
+      );
+    },
+    { immediate: true }
+  );
+  return result;
+};
+
+const stageHoverMap = new WeakMap<
+  Stage,
+  { result: Ref<EntityShape | undefined>; count: number; des: () => void }
+>();
+export const getHoverShape = (stage: Stage) => {
+  let isStop = false;
+  const stop = () => {
+    if (isStop || !stageHoverMap.has(stage)) return;
+    isStop = true;
+    const data = stageHoverMap.get(stage)!;
+    if (--data.count <= 0) {
+      data.des();
+    }
+  };
+
+  if (stageHoverMap.has(stage)) {
+    const data = stageHoverMap.get(stage)!;
+    ++data.count;
+    return [data.result, stop] as const;
+  }
+
+  const hover = ref<EntityShape>();
+  const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
+    const target = ev.target;
+    hover.value = target;
+    target.off(`pointerleave`, leaveHandler);
+    target.on(`pointerleave`, leaveHandler as any);
+  };
+
+  const leaveHandler = () => {
+    if (hover.value) {
+      hover.value.off(`pointerleave`, leaveHandler);
+      hover.value = undefined;
+    }
+  };
+
+  stage.on(`pointerenter`, enterHandler);
+  stageHoverMap.set(stage, {
+    result: hover,
+    count: 1,
+    des: () => {
+      stage.off(`pointerenter`, enterHandler);
+      leaveHandler();
+      stageHoverMap.delete(stage);
+    },
+  });
+  return [hover, stop] as const;
+};
+
+export const useShapeIsHover = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const stage = useStage();
+  const isHover = ref(false);
+  const stop = watch(
+    () => ({ stage: stage.value?.getNode(), shape: shape.value?.getNode() }),
+    ({ stage, shape }, _, onCleanup) => {
+      if (!stage || !shape || result.isPause) {
+        isHover.value = false;
+        return;
+      }
+
+      const [hoverShape, stopHoverListener] = getHoverShape(stage);
+
+      watchEffect(() => {
+        isHover.value = !!(
+          hoverShape.value && shapeTreeContain([shape], toRaw(hoverShape.value))
+        );
+      });
+      onCleanup(stopHoverListener);
+    },
+    { immediate: true }
+  );
+  const result = usePause([isHover, stop] as const);
+  return result;
+};
+
+export const shapeTreeContain = (
+  parent: EntityShape | EntityShape[],
+  target: EntityShape,
+  checked: EntityShape[] = []
+) => {
+  const eq = Array.isArray(parent)
+    ? (shape: EntityShape) => parent.includes(shape)
+    : (shape: EntityShape) => parent === shape;
+  return shapeParentsEq(target, eq, checked);
+};
+
+export const shapeParentsEq = (
+  target: EntityShape,
+  eq: (shape: EntityShape) => boolean,
+  checked: EntityShape[] = []
+) => {
+  while (target) {
+    if (checked.includes(target)) return null;
+    if (eq(target)) {
+      return target;
+    }
+    target = target.parent as any;
+  }
+  return null;
+};

+ 10 - 0
src/components/drawing/install-lib.ts

@@ -0,0 +1,10 @@
+import { App } from "vue";
+import VueKonva from "vue-konva";
+
+const installApps = new WeakSet<App>();
+export const install = (app: App) => {
+  if (installApps.has(app)) return;
+  console.error('use vue-konva', VueKonva)
+  app.use(VueKonva);
+  installApps.add(app);
+};

+ 606 - 0
src/components/drawing/math.ts

@@ -0,0 +1,606 @@
+import { Vector2, ShapeUtils, Box2 } from "three";
+import { Transform } from "konva/lib/Util";
+
+export type Pos = { x: number; y: number };
+export type Size = { width: number, height: number }
+
+/**
+ * 四舍五入
+ * @param num
+ * @param b 保留位数
+ * @returns
+ */
+export const round = (num: number, b: number = 2) => {
+  const scale = Math.pow(10, b);
+  return Math.round(num * scale) / scale;
+};
+
+export const vector = (pos: Pos = { x: 0, y: 0 }): Vector2 => {
+  return new Vector2(pos.x, pos.y);
+  // if (pos instanceof Vector2) {
+  //   return pos;
+  // } else {
+  //   return new Vector2(pos.x, pos.y);
+  // }
+};
+export const lVector = (line: Pos[]) => line.map(vector);
+
+export const zeroEq = (n: number) => Math.abs(n) < 0.0001;
+export const numEq = (p1: number, p2: number) => zeroEq(p1 - p2);
+export const vEq = (v1: Pos, v2: Pos) => numEq(v1.x, v2.x) && numEq(v1.y, v2.y);
+
+export const vsBound = (positions: Pos[]) => {
+  const box = new Box2();
+  box.setFromPoints(positions.map(vector));
+  return box;
+};
+
+/**
+ * 获取线段方向
+ * @param line 线段
+ * @returns 方向
+ */
+export const lineVector = (line: Pos[]) =>
+  vector(line[1]).sub(vector(line[0])).normalize();
+
+/**
+ * 点是否相同
+ * @param p1 点1
+ * @param p2 点2
+ * @returns 是否相等
+ */
+export const eqPoint = vEq;
+
+/**
+ * 方向是否相同
+ * @param p1 点1
+ * @param p2 点2
+ * @returns 是否相等
+ */
+export const eqNGDire = (p1: Pos, p2: Pos) =>
+  eqPoint(p1, p2) || eqPoint(p1, vector(p2).multiplyScalar(-1));
+
+/**
+ * 获取两点距离
+ * @param p1 点1
+ * @param p2 点2
+ * @returns 距离
+ */
+export const lineLen = (p1: Pos, p2: Pos) => vector(p1).distanceTo(p2);
+
+export const vectorLen = (dire: Pos) => vector(dire).length();
+
+/**
+ * 获取向量的垂直向量
+ * @param dire 原方向
+ * @returns 垂直向量
+ */
+export const verticalVector = (dire: Pos) =>
+  vector({ x: -dire.y, y: dire.x }).normalize();
+
+/**
+ * 获取旋转指定度数后的向量
+ * @param pos 远向量
+ * @param angleRad 旋转角度
+ * @returns 旋转后向量
+ */
+export const rotateVector = (pos: Pos, angleRad: number) =>
+  new Transform().rotate(angleRad).point(pos);
+
+/**
+ * 创建线段
+ * @param dire 向量
+ * @param start 起始点
+ * @param dis 长度
+ * @returns 线段
+ */
+
+export const getVectorLine = (
+  dire: Pos,
+  start: Pos = { x: 0, y: 0 },
+  dis: number = 1
+) => [start, vector(dire).multiplyScalar(dis).add(start)];
+
+/**
+ * 获取线段的垂直方向向量
+ * @param line 原线段
+ * @returns 垂直向量
+ */
+export const lineVerticalVector = (line: Pos[]) =>
+  verticalVector(lineVector(line));
+
+/**
+ * 获取向量的垂直线段
+ * @param dire 向量
+ * @param start 线段原点
+ * @param len 线段长度
+ */
+export const verticalVectorLine = (
+  dire: Pos,
+  start: Pos = { x: 0, y: 0 },
+  len: number = 1
+) => getVectorLine(verticalVector(dire), start, len);
+
+/**
+ * 获取两向量角度(从向量a出发)
+ * @param v1 向量a
+ * @param v2 向量b
+ * @returns 两向量夹角弧度, 逆时针为正,顺时针为负
+ */
+export const vector2IncludedAngle = (v1: Pos, v2: Pos) => {
+  const start = vector(v1);
+  const end = vector(v2);
+  const angle = start.angleTo(end);
+  return start.cross(end) > 0 ? angle : -angle;
+};
+
+// 判断多边形方向(Shoelace Formula)
+export function getPolygonDirection(points: Pos[]) {
+  let area = 0;
+  const numPoints = points.length;
+  for (let i = 0; i < numPoints; i++) {
+      const p1 = points[i];
+      const p2 = points[(i + 1) % numPoints];
+      area += (p2.x - p1.x) * (p2.y + p1.y);
+  }
+
+  // 如果面积为正,是逆时针;否则是顺时针
+  return area;
+}
+
+/**
+ * 获取两线段角度(从线段a出发)
+ * @param line1 线段a
+ * @param line2 线段b
+ * @returns 两线段夹角弧度
+ */
+export const line2IncludedAngle = (line1: Pos[], line2: Pos[]) =>
+  vector2IncludedAngle(lineVector(line1), lineVector(line2));
+
+/**
+ * 获取向量与X正轴角度
+ * @param v 向量
+ * @returns 夹角弧度
+ */
+const nXAxis = vector({ x: 1, y: 0 });
+export const vectorAngle = (v: Pos) => {
+  const start = vector(v);
+  return start.angleTo(nXAxis);
+};
+
+/**
+ * 获取线段与方向的夹角弧度
+ * @param line 线段
+ * @param dire 方向
+ * @returns 线段与方向夹角弧度
+ */
+export const lineAndVectorIncludedAngle = (line: Pos[], v: Pos) =>
+  vector2IncludedAngle(lineVector(line), v);
+
+/**
+ * 获取线段中心点
+ * @param line
+ * @returns
+ */
+export const lineCenter = (line: Pos[]) =>
+  vector(line[0]).add(line[1]).multiplyScalar(0.5);
+
+
+export const lineSpeed = (line: Pos[], step: number) => {
+  const p = vector(line[0])
+  const v = vector(line[1]).sub(line[0])
+  return p.add(v.multiplyScalar(step))
+}
+
+export const pointsCenter = (points: Pos[]) => {
+  if (points.length === 0) return { x: 0, y: 0 };
+  const v = vector(points[0]);
+  for (let i = 1; i < points.length; i++) {
+    v.add(points[i]);
+  }
+  return v.multiplyScalar(1 / points.length);
+};
+
+export const lineJoin = (l1: Pos[], l2: Pos[]) => {
+  const checks = [
+    [l1[0], l2[0]],
+    [l1[0], l2[1]],
+    [l1[1], l2[0]],
+    [l1[1], l2[1]],
+  ];
+  const ndx = checks.findIndex((line) => eqPoint(line[0], line[1]));
+  if (~ndx) {
+    return checks[ndx];
+  } else {
+    return false;
+  }
+};
+
+export const isLineEqual = (l1: Pos[], l2: Pos[]) =>
+  eqPoint(l1[0], l2[0]) && eqPoint(l1[1], l2[1]);
+
+export const isLineReverseEqual = (l1: Pos[], l2: Pos[]) =>
+  eqPoint(l1[0], l2[1]) && eqPoint(l1[1], l2[0]);
+
+export const isLineIntersect = (l1: Pos[], l2: Pos[]) => {
+  const s1 = l2[1].y - l2[0].y;
+  const s2 = l2[1].x - l2[0].x;
+  const s3 = l1[1].x - l1[0].x;
+  const s4 = l1[1].y - l1[0].y;
+  const s5 = l1[0].y - l2[0].y;
+  const s6 = l1[0].x - l2[0].x;
+
+  const denominator = s1 * s3 - s2 * s4;
+  const ua = round((s2 * s5 - s1 * s6) / denominator, 6);
+  const ub = round((s3 * s5 - s4 * s6) / denominator, 6);
+
+  if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
+    return true;
+  } else {
+    return false;
+  }
+};
+
+export const vectorParallel = (dire1: Pos, dire2: Pos) =>
+  zeroEq(vector(dire1).cross(dire2));
+
+export const lineParallelRelationship = (l1: Pos[], l2: Pos[]) => {
+  const dire1 = lineVector(l1);
+  const dire2 = lineVector(l2);
+
+  // 计算线段的法向量
+  const normal1 = verticalVector(dire1);
+  const normal2 = verticalVector(dire2);
+  const startDire = lineVector([l1[0], l2[0]]);
+
+  // 计算线段的参数方程
+  const t1 = round(normal2.dot(startDire) / normal2.dot(dire1), 6);
+  const t2 = round(normal1.dot(startDire) / normal1.dot(dire2), 6);
+
+  if (t1 === 0 && t2 === 0) {
+    return RelationshipEnum.Overlap;
+  }
+
+  if (eqPoint(normal1, normal2) || eqPoint(normal1, normal2.clone().negate())) {
+    return lineJoin(l1, l2)
+      ? RelationshipEnum.Overlap
+      : RelationshipEnum.Parallel;
+  }
+};
+
+export enum RelationshipEnum {
+  // 重叠
+  Overlap = "Overlap",
+  // 相交
+  Intersect = "Intersect",
+  // 延长相交
+  ExtendIntersect = "ExtendIntersect",
+  // 平行
+  Parallel = "Parallel",
+  // 首尾连接
+  Join = "Join",
+  // 一样
+  Equal = "Equal",
+  // 反向
+  ReverseEqual = "ReverseEqual",
+}
+/**
+ * 获取两线段是什么关系,(重叠、相交、平行、首尾相接等)
+ * @param l1
+ * @param l2
+ * @returns RelationshipEnum
+ */
+export const lineRelationship = (l1: Pos[], l2: Pos[]) => {
+  if (isLineEqual(l1, l2)) {
+    return RelationshipEnum.Equal;
+  } else if (isLineReverseEqual(l1, l2)) {
+    return RelationshipEnum.ReverseEqual;
+  }
+
+  const parallelRelationship = lineParallelRelationship(l1, l2);
+  if (parallelRelationship) {
+    return parallelRelationship;
+  } else if (lineJoin(l1, l2)) {
+    return RelationshipEnum.Join;
+  } else if (isLineIntersect(l1, l2)) {
+    return RelationshipEnum.Intersect; // 两线段相交
+  } else {
+    return RelationshipEnum.ExtendIntersect; // 延长可相交
+  }
+};
+
+export const createLine = (p: Pos, v: Pos, l?: number) => {
+  const line = [p];
+  if (l) {
+    v = vector(v).multiplyScalar(l);
+  }
+  line[1] = vector(line[0]).add(v);
+  return line;
+};
+
+/**
+ * 获取两线段交点,可延长相交
+ * @param l1 线段1
+ * @param l2 线段2
+ * @returns 交点坐标
+ */
+export const lineIntersection = (l1: Pos[], l2: Pos[]) => {
+  // 定义两条线段的起点和终点坐标
+  const [line1Start, line1End] = lVector(l1);
+  const [line2Start, line2End] = lVector(l2);
+
+  // 计算线段的方向向量
+  const dir1 = line1End.clone().sub(line1Start);
+  const dir2 = line2End.clone().sub(line2Start);
+
+  // 计算参数方程中的系数
+  const a = dir1.x;
+  const b = -dir2.x;
+  const c = dir1.y;
+  const d = -dir2.y;
+
+  const e = line2Start.x - line1Start.x;
+  const f = line2Start.y - line1Start.y;
+
+  // 求解参数t和s
+  const t = (d * e - b * f) / (a * d - b * c);
+  // 计算交点坐标
+  const p = line1Start.clone().add(dir1.clone().multiplyScalar(t));
+
+  if (isNaN(p.x) || !isFinite(p.x) || isNaN(p.y) || !isFinite(p.y)) return null;
+
+  return p;
+};
+
+/**
+ * 获取点是否在线上
+ * @param line 线段
+ * @param position 点
+ */
+export const lineInner = (line: Pos[], position: Pos) => {
+  // 定义线段的起点和终点坐标
+  const [A, B] = lVector(line);
+  // 定义一个点的坐标
+  const P = vector(position);
+
+  // 计算向量 AP 和 AB
+  const AP = P.clone().sub(A);
+  const AB = B.clone().sub(A);
+
+  // 计算叉积
+  const crossProduct = AP.x * AB.y - AP.y * AB.x;
+
+  // 如果叉积不为 0,说明点 P 不在直线 AB 上
+  if (!zeroEq(crossProduct)) {
+    return false;
+  }
+  // 检查点 P 的坐标是否在 A 和 B 的坐标范围内
+  return (
+    Math.min(A.x, B.x) <= P.x &&
+    P.x <= Math.max(A.x, B.x) &&
+    Math.min(A.y, B.y) <= P.y &&
+    P.y <= Math.max(A.y, B.y)
+  );
+};
+
+/**
+ * 获取点在线段上的投影
+ * @param line 线段
+ * @param position 点
+ * @returns 投影信息
+ */
+export const linePointProjection = (line: Pos[], position: Pos) => {
+  // 定义线段的起点和终点坐标
+  const [lineStart, lineEnd] = lVector(line);
+  // 定义一个点的坐标
+  const point = vector(position);
+
+  // 计算线段的方向向量
+  const lineDir = lineEnd.clone().sub(lineStart);
+  // 计算点到线段起点的向量
+  const pointToLineStart = point.clone().sub(lineStart);
+  // 计算点在线段方向上的投影长度
+  const t = pointToLineStart.dot(lineDir.normalize());
+  // 计算投影点的坐标
+  return lineStart.add(lineDir.multiplyScalar(t));
+};
+
+/**
+ * 获取点距离线段最近距离
+ * @param line 直线
+ * @param position 参考点
+ * @returns 距离
+ */
+export const linePointLen = (line: Pos[], position: Pos) =>
+  lineLen(position, linePointProjection(line, position));
+
+/**
+ * 计算多边形是否为逆时针
+ * @param points 多边形顶点
+ * @returns true | false
+ */
+export const isPolygonCounterclockwise = (points: Pos[]) =>
+  ShapeUtils.isClockWise(points.map(vector));
+
+/**
+ * 切割线段,返回连段切割点
+ * @param line 线段
+ * @param amount 切割份量
+ * @param unit 一份单位大小
+ * @returns 点数组
+ */
+export const lineSlice = (
+  line: Pos[],
+  amount: number,
+  unit = lineLen(line[0], line[1]) / amount
+) =>
+  new Array(unit)
+    .fill(0)
+    .map((_, i) => linePointProjection(line, { x: i * unit, y: i * unit }));
+
+/**
+ * 线段是否相交多边形
+ * @param polygon 多边形
+ * @param line 检测线段
+ * @returns
+ */
+export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
+  for (let i = 0; i < polygon.length; i++) {
+    if (isLineIntersect([polygon[i], polygon[i + 1]], line)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+/**
+ * 通过角度和两个点获取两者的连接点,
+ * @param p1
+ * @param p2
+ * @param rad
+ */
+export const joinPoint = (p1: Pos, p2: Pos, rad: number) => {
+  const lvector = new Vector2()
+    .subVectors(p1, p2)
+    .rotateAround({ x: 0, y: 0 }, rad);
+
+  return vector(p2).add(lvector);
+};
+
+/**
+ * 要缩放多少才能到达目标
+ * @param origin 缩放原点
+ * @param scaleDirection 缩放方向
+ * @param p1 当前点位
+ * @param p2 目标点位
+ * @returns
+ */
+export function calculateScaleFactor(
+  origin: Pos,
+  scaleDirection: Pos,
+  p1: Pos,
+  p2: Pos
+) {
+  const op1 = vector(p1).sub(origin);
+  const op2 = vector(p2).sub(origin);
+  const xZero = zeroEq(op1.x);
+  const yZero = zeroEq(op1.y);
+
+  if (zeroEq(op1.x) || zeroEq(op2.y)) return;
+  if (zeroEq(scaleDirection.x)) {
+    if (zeroEq(p2.x - p1.x)) {
+      return zeroEq(op1.y - op2.y) ? 1 : yZero ? null : op2.y / op1.y;
+    } else {
+      return;
+    }
+  }
+  if (zeroEq(scaleDirection.y)) {
+    if (zeroEq(p2.y - p1.y)) {
+      return zeroEq(op1.x - op2.x) ? 1 : xZero ? null : op2.x / op1.x;
+    } else {
+      return;
+    }
+  }
+  if (xZero && yZero) {
+    return null;
+  }
+  const xScaleFactor = op2.x / (op1.x * scaleDirection.x);
+  const yScaleFactor = op2.y / (op1.y * scaleDirection.y);
+
+  if (xZero) {
+    return yScaleFactor;
+  } else if (yZero) {
+    return xScaleFactor;
+  }
+  if (zeroEq(xScaleFactor - yScaleFactor)) {
+    return xScaleFactor;
+  }
+}
+
+// 获取两线段的矩阵关系
+export const getLineRelationMat = (l1: [Pos, Pos], l2: [Pos, Pos]) => {
+  // 提取点
+  const P1 = l1[0]; // l1 的起点
+  const P1End = l1[1]; // l1 的终点
+  const P2 = l2[0]; // l2 的起点
+  const P2End = l2[1]; // l2 的终点
+
+  // 计算方向向量
+  const d1 = { x: P1End.x - P1.x, y: P1End.y - P1.y };
+  const d2 = { x: P2End.x - P2.x, y: P2End.y - P2.y };
+
+  // 计算方向向量的长度
+  const lengthD1 = Math.sqrt(d1.x ** 2 + d1.y ** 2);
+  const lengthD2 = Math.sqrt(d2.x ** 2 + d2.y ** 2);
+
+  if (lengthD1 === 0 || lengthD2 === 0) return new Transform();
+
+  // 归一化方向向量
+  const unitD1 = { x: d1.x / lengthD1, y: d1.y / lengthD1 };
+  const unitD2 = { x: d2.x / lengthD2, y: d2.y / lengthD2 };
+
+  // 计算旋转角度
+  const angle = Math.atan2(unitD2.y, unitD2.x) - Math.atan2(unitD1.y, unitD1.x);
+  // 计算旋转矩阵
+  // 计算缩放因子
+  const scale = lengthD2 / lengthD1;
+  // 计算平移向量
+  const translation = [P2.x - P1.x, P2.y - P1.y];
+
+  const mat = new Transform()
+    .translate(translation[0], translation[1])
+    .translate(P1.x, P1.y)
+    .scale(scale, scale)
+    .rotate(angle)
+    .translate(-P1.x, -P1.y);
+
+  
+  if (!eqPoint(mat.point(P1), P2)) {
+    console.error('对准不正确 旋转后P1', mat.point(P1), P2)
+  }
+  if (!eqPoint(mat.point(P1End), P2End)) {
+    console.error('对准不正确 旋转后P2', mat.point(P1End), P1End)
+  }
+  return mat
+};
+
+// 判断两向量是否垂直
+export const isVertical = (v1: Pos, v2: Pos) => {
+  console.log(vector(v1).dot(v2))
+  return zeroEq(vector(v1).dot(v2))
+}
+
+
+/**
+ * 创建对齐端口矩阵
+ * @param targetView 目标窗口左上右下坐标
+ * @param originView 源窗口左上右下坐标
+ * @param retainScale 是否固定xy的缩放系数
+ * @param padding 留白区域
+ * @returns
+ */
+export const alignPortMat = (
+	targetView: Pos[],
+	originView: Pos[],
+	retainScale = false,
+	padding: Pos | number = 0
+) => {
+	padding = typeof padding === "number" ? { x: padding, y: padding } : padding;
+
+	const pad = vector(padding);
+	const real = vector(originView[1]).sub(originView[0]);
+	const screen = vector(targetView[1]).sub(targetView[0]);
+	const scale = screen.clone().sub(pad.multiplyScalar(2)).divide(real);
+
+	if (retainScale) {
+		scale.x = scale.y = Math.min(scale.x, scale.y); // 选择较小的比例以保持内容比例
+	}
+
+	const offset = screen
+		.clone()
+		.sub(real.clone().multiply(scale))
+		.divideScalar(2)
+		.sub(real.clone().multiply(scale));
+
+	return new Transform().translate(offset.x, offset.y).scale(scale.x, scale.y);
+};

+ 218 - 0
src/components/drawing/operate.vue

@@ -0,0 +1,218 @@
+<template>
+  <Teleport :to="`#draw-renderer`" v-if="stage">
+    <transition name="pointer-fade">
+      <div
+        v-if="pointer"
+        :style="{ transform: pointer }"
+        :size="8"
+        class="propertys-controller"
+        ref="layout"
+      >
+        <Menu>
+          <MenuItem v-for="menu in menus" @click="clickHandler(menu.handler)">
+            <ui-icon :type="menu.icon" />
+            <span>{{ menu.label }}</span>
+          </MenuItem>
+        </Menu>
+      </div>
+    </transition>
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { computed, nextTick, ref, watch, watchEffect } from "vue";
+import { Transform } from "konva/lib/Util";
+import { Menu, MenuItem } from "ant-design-vue";
+import { shapeTreeContain, useStage, useViewerTransformConfig } from "./hook.js";
+import { DC, EntityShape } from "./dec.js";
+
+const props = defineProps<{
+  target: DC<EntityShape> | undefined;
+  data?: Record<string, any>;
+  menus: Array<{ icon?: any; label?: string; handler: () => void }>;
+}>();
+const emit = defineEmits<{
+  (e: "show" | "hide"): void;
+}>();
+
+const layout = ref<HTMLDivElement>();
+const stage = useStage();
+// const status = useMouseShapeStatus(computed(() => props.target));
+const rightClick = ref(false);
+
+const hasRClick = (ev: MouseEvent) => {
+  if (ev.button !== 2) return false;
+  const shape = props.target?.getNode();
+  const pos = stage.value?.getNode().pointerPos;
+  const layer = stage.value?.getNode().children[0];
+  if (!shape || !pos || !layer) return false;
+  let clickShape = layer.getIntersection(pos);
+  ev.preventDefault();
+  return !!clickShape && shapeTreeContain(shape, clickShape) === shape;
+};
+
+watchEffect((onCleanup) => {
+  const dom = stage.value?.getStage().container();
+  if (!dom) return;
+
+  const clickHandler = async (ev: MouseEvent) => {
+    const show = hasRClick(ev);
+    if (show && rightClick.value) {
+      rightClick.value = false;
+      await nextTick();
+    }
+    rightClick.value = show;
+  };
+
+  dom.addEventListener("contextmenu", clickHandler);
+  dom.addEventListener("pointerdown", clickHandler);
+  onCleanup(() => {
+    dom.removeEventListener("contextmenu", clickHandler);
+    dom.removeEventListener("pointerdown", clickHandler);
+  });
+});
+
+const clickHandler = (handler: () => void) => {
+  handler();
+  rightClick.value = false;
+};
+
+const hidden = computed(
+  () =>
+    !stage.value?.getStage() ||
+    !props.target?.getNode() ||
+    // !status.value.hover ||
+    // status.value.active ||
+    // transformIngShapes.value.length !== 0
+    !rightClick.value
+);
+
+const move = new Transform();
+const pointer = ref<string | null>(null);
+const calcPointer = async () => {
+  if (hidden.value) {
+    pointer.value = null;
+    return;
+  } else if (pointer.value) {
+    return;
+  }
+  const $stage = stage.value!.getStage();
+  const mousePosition = $stage.pointerPos;
+  if (!mousePosition) {
+    pointer.value = null;
+    return;
+  }
+  const $shape = props.target!.getNode();
+  const shapeRect = $shape.getClientRect();
+
+  const shapeR = shapeRect.x + shapeRect.width;
+  const shapeB = shapeRect.y + shapeRect.height;
+  let x = Math.min(Math.max(mousePosition.x, shapeRect.x), shapeR);
+  let y = Math.min(Math.max(mousePosition.y, shapeRect.y), shapeB);
+
+  move.reset();
+  move.translate(x, y);
+  pointer.value = `matrix(${move.m.join(",")})`;
+  await nextTick();
+
+  const domRect = layout.value!.getBoundingClientRect();
+  x = x - domRect.width / 2;
+  x = Math.max(x, shapeRect.x);
+
+  if (x + domRect.width > shapeR) {
+    x = shapeR - domRect.width;
+  }
+  if (y + domRect.height > shapeB) {
+    y = y - domRect.height;
+  }
+
+  x = Math.max(x, 10);
+  y = Math.max(y, 10);
+  if (x + domRect.width > $stage.width() - 10) {
+    x = $stage.width() - domRect.width - 10;
+  }
+  if (y + domRect.height > $stage.height() - 10) {
+    y = $stage.height() - domRect.height - 10;
+  }
+
+  move.reset();
+  move.translate(x, y);
+  pointer.value = `matrix(${move.m.join(",")})`;
+};
+watch(
+  () => !!pointer.value,
+  (show) => {
+    emit(show ? "show" : "hide");
+  }
+);
+
+let timeout: any;
+const resetPointer = () => {
+  if (hidden.value) {
+    pointer.value = null;
+    return;
+  } else if (pointer.value) {
+    return;
+  }
+  clearTimeout(timeout);
+  timeout = setTimeout(calcPointer, 16);
+};
+
+watch(hidden, resetPointer);
+watch(useViewerTransformConfig(), () => {
+  pointer.value = null;
+  resetPointer();
+});
+// watch(() => props.data, resetPointer);
+</script>
+
+<style lang="scss">
+.propertys-controller {
+  pointer-events: none;
+  position: absolute;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+  background-color: #ffffff;
+  left: 0;
+  top: 0;
+  overflow: hidden;
+  color: #303133;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
+  min-width: 10px;
+  padding: 5px 0;
+  pointer-events: all;
+  font-size: 14px;
+  border: 1px solid var(--el-border-color-light);
+  box-shadow: var(--el-dropdown-menu-box-shadow);
+  background-color: var(--el-bg-color-overlay);
+  border-radius: var(--el-border-radius-base);
+  --el-menu-base-level-padding: 16px;
+  --el-menu-item-height: 32px;
+  --el-menu-item-font-size: 14px;
+
+  .el-menu-item {
+    align-items: center;
+    padding: 5px 16px 5px 6px !important;
+    color: var(--el-text-color-regular);
+  }
+  .el-menu-item [class^="el-icon"] {
+    margin: 0;
+    font-size: 1em;
+  }
+}
+
+.pointer-fade-enter-active,
+.pointer-fade-leave-active {
+  transition: opacity 0.3s ease;
+  .item {
+    pointer-events: none;
+  }
+}
+.pointer-fade-enter-from,
+.pointer-fade-leave-to {
+  opacity: 0;
+}
+</style>

+ 128 - 0
src/components/drawing/renderer.vue

@@ -0,0 +1,128 @@
+<template>
+  <div
+    class="draw-layout"
+    id="draw-renderer"
+    ref="layout"
+    :style="{ cursor: cursorStyle }"
+  >
+    <template v-if="layout">
+      <v-stage ref="stage" :config="size">
+        <v-layer :config="viewerConfig" id="formal">
+          <v-rect :config="config" ref="shape" />
+          <slot />
+        </v-layer>
+      </v-stage>
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, getCurrentInstance, onUnmounted, ref, watch, watchEffect } from "vue";
+import {
+  mergeFuns,
+  rendererMap,
+  useCursor,
+  useGlobalResize,
+  useGlobalVar,
+  useStage,
+  useViewer,
+  useViewerInvertTransform,
+  useViewerInvertTransformConfig,
+  useViewerTransform,
+  useViewerTransformConfig,
+} from "./hook";
+import { install } from "./install-lib";
+import { Pos } from "./dec";
+
+const props = defineProps<{ scale: { value: number; center: Pos } }>();
+const emit = defineEmits<{ (e: "update:scaleValue", v: number): void }>();
+
+const instance = getCurrentInstance();
+install(instance!.appContext.app);
+
+rendererMap.set(instance, { unmounteds: [] });
+onUnmounted(() => {
+  mergeFuns(rendererMap.get(instance)!.unmounteds)();
+});
+
+const stage = useStage();
+const { size } = useGlobalResize();
+const layout = ref();
+const viewerConfig = useViewerTransformConfig();
+
+const cursor = useCursor();
+const cursorStyle = computed(() => {
+  if (cursor.value.includes(".")) {
+    return `url(${cursor.value}), auto`;
+  } else {
+    return cursor.value;
+  }
+});
+
+const viewer = useViewer();
+watch(
+  () => props.scale,
+  (scale) => {
+    if (size.value) {
+      viewer.viewer.scalePixel(scale.center, {
+        x: scale.value / viewerConfig.value.scaleX,
+        y: 1,
+      });
+    }
+  },
+  { deep: true }
+);
+watchEffect(
+  () => {
+    if (Math.abs(props.scale.value - viewerConfig.value.scaleX) > 0.001) {
+      emit("update:scaleValue", viewerConfig.value.scaleX);
+    }
+  },
+  { flush: "post" }
+);
+
+const invConfig = useViewerInvertTransformConfig();
+const viewMat = useViewerTransform();
+const viewInvertMat = useViewerInvertTransform();
+const config = computed(
+  () =>
+    size.value && {
+      ...invConfig.value,
+      ...size.value,
+    }
+);
+const { updateSize } = useGlobalResize();
+const { misPixel } = useGlobalVar();
+defineExpose({
+  updateSize,
+  getTimeScreen(time: number) {
+    return viewMat.value.point({ x: time * misPixel, y: 0 }).x;
+  },
+});
+</script>
+
+<style scoped lang="scss">
+.draw-layout {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .draw-content {
+    position: relative;
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.mount-mask {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  z-index: 999;
+}
+</style>

+ 174 - 0
src/components/drawing/viewer.ts

@@ -0,0 +1,174 @@
+import { Transform } from "konva/lib/Util";
+import mitt from 'mitt'
+import { alignPortMat, Pos } from "./math";
+import { MathUtils } from "three";
+
+export type ViewerProps = {
+	size?: number[];
+	bound?: number[];
+	padding: number | number[];
+	retain: boolean;
+};
+
+export class Viewer {
+	props: ViewerProps;
+	viewMat: Transform;
+	partMat: Transform = new Transform();
+	bus = mitt<{ transformChange: Transform }>()
+
+	constructor(props: Partial<ViewerProps> = {}) {
+		this.props = {
+			padding: 0,
+			retain: true,
+			...props,
+		}
+		this.viewMat = new Transform();
+	}
+
+	get bound() {
+		return this.props.bound
+	}
+
+	setBound(bound: number[], size?: number[], padding?: number | number[], retain?: boolean) {
+		this.props.bound = bound
+		if (padding) {
+			this.props.padding = padding
+		}
+		if (retain) {
+			this.props.retain = retain
+		}
+		if (size) {
+			this.props.size = size
+		}
+		padding = this.props.padding
+		retain = this.props.retain
+		size = this.props.size
+
+		if (!size) {
+			throw '缺少视窗size'
+		}
+
+		this.partMat = alignPortMat(
+			[
+				{x: bound[0], y: bound[1]},
+				{x: bound[2], y: bound[3]},
+			],
+			[
+				{x: 0, y: 0},
+				{x: size[0], y: size[1]},
+			],
+			retain,
+			typeof padding === "number"
+				? padding
+				: {x: padding[0], y: padding[1]}
+		);
+	}
+
+	move(position: Pos, initMat = this.viewMat) {
+		this.mutMat(new Transform().translate(position.x, position.y), initMat);
+	}
+
+	movePixel(position: Pos, initMat = this.viewMat) {
+		if (isNaN(position.x) || isNaN(position.y)) {
+			console.error(`无效移动位置${position.x} ${position.y}`)
+			return;
+		}
+		const offsetX = position.x
+		const currentX = this.viewMat.decompose().x
+		const finX =  currentX + offsetX
+		const x = finX - currentX
+
+		const mat = initMat.copy().invert()
+		const p1 = mat.point({x: 0, y: 0})
+		const p2 = mat.point({x, y: position.y})
+		this.move({x: p2.x - p1.x, y: p2.y - p1.y})
+
+		// const info = initMat.decompose()
+		// const tf = new Transform()
+		// tf.rotate(info.rotation)
+		// tf.scale(info.scaleX, info.scaleY)
+		// this.move(tf.invert().point(position), this.viewMat)
+	}
+
+
+	scale(center: Pos, scale: Pos, initMat = this.viewMat) {
+		const base = initMat.decompose().scaleX
+		if (base * scale.x < 0.001 || base * scale.x > 1000) {
+			console.error('缩放范围0.001~1000 已超过范围无法缩放')
+			return;
+		}
+		if (isNaN(center.x) || isNaN(center.y)) {
+			console.error(`无效中心点${center.x} ${center.y}`)
+			return;
+		}
+
+		this.mutMat(
+			new Transform()
+				.translate(center.x, center.y)
+				.multiply(
+					new Transform()
+						.scale(scale.x, scale.y)
+						.multiply(new Transform().translate(-center.x, -center.y))
+				),
+			initMat
+		);
+	}
+
+	scalePixel(center: Pos, scale: Pos, initMat = this.viewMat) {
+		const pos = initMat.copy().invert().point(center)
+		this.scale(pos, scale, initMat)
+	}
+
+	rotate(center: Pos, angleRad: number, initMat = this.viewMat) {
+		this.mutMat(
+			new Transform()
+				.translate(center.x, center.y)
+				.multiply(
+					new Transform()
+						.rotate(angleRad)
+						.multiply(new Transform().translate(-center.x, -center.y))
+				),
+			initMat
+		);
+	}
+
+	rotatePixel(center: Pos, angleRad: number, initMat = this.viewMat) {
+		const pos = initMat.copy().invert().point(center)
+		this.rotate(pos, angleRad, initMat)
+	}
+
+	mutMat(mat: Transform, initMat = this.viewMat) {
+		// this.setViewMat(mat.copy().multiply(initMat))
+		this.setViewMat(initMat.copy().multiply(mat))
+	}
+
+	setViewMat(mat: number[] | Transform) {
+		if (mat instanceof Transform) {
+			this.viewMat = mat.copy();
+		} else {
+			this.viewMat = new Transform(mat)
+		}
+		let dec = this.viewMat.decompose()
+		if (dec.x > 10) {
+			dec.x = 10
+			this.viewMat = new Transform()
+				.translate(dec.x, dec.y)
+				.scale(dec.scaleX, dec.scaleY)
+				.rotate(MathUtils.degToRad(dec.rotation))
+
+		}
+		// while(dec.x > 0.01) {
+		// 	console.log(dec.x)
+		// 	this.viewMat = this.viewMat.copy().translate(-dec.x, 0)
+		// 	dec = this.viewMat.decompose()
+		// }
+		this.bus.emit('transformChange', this.transform)
+	}
+
+	get transform() {
+		return this.partMat.copy().multiply(this.viewMat);
+	}
+	get current() {
+		return this.viewMat.decompose()
+	}
+}

+ 11 - 0
src/components/global-search/guide.vue

@@ -0,0 +1,11 @@
+<template>
+  <GuideSign :guide="data" :edit="false" search />
+</template>
+
+<script lang="ts" setup>
+import { flyPlayGuide } from "@/hook/use-fly";
+import { Guide } from "@/store";
+import GuideSign from "@/views/guide/guide/sign.vue";
+
+defineProps<{ data: Guide }>();
+</script>

+ 264 - 0
src/components/global-search/index.vue

@@ -0,0 +1,264 @@
+<template>
+  <div
+    id="global-search"
+    v-if="custom.showSearch && !params.pure"
+    :class="{ nul: fuseModels.length === 0 }"
+  >
+    <Select
+      :filter-option="filter"
+      v-model:value="value"
+      size="large"
+      optionLabelProp="label"
+      :listHeight="440"
+      style="width: 340px"
+      show-search
+      @search="handleSearch"
+      :dropdownMatchSelectWidth="false"
+      :popupClassName="popupClassName || 'global-search-menu'"
+      allowClear
+      placeholder="搜索"
+    >
+      <template v-for="item in options" :key="item.key">
+        <SelectOptGroup v-if="item.options.length" class="group-item" :key="item.key">
+          <template #label>
+            <span class="group-item-title">{{ item.name }}</span>
+          </template>
+          <SelectOption
+            v-for="(option, ndx) in item.options"
+            :value="item.key + option.id"
+            :label="item.getLabel(option as any)"
+            :key="item.key + option.id"
+            :class="{ 'last-item': ndx + 1 === item.options.length }"
+          >
+            <component
+              :is="item.comp"
+              :data="(option as any)"
+              @update:data="(data: any) => emit('update:data', data)"
+            />
+          </SelectOption>
+        </SelectOptGroup>
+      </template>
+    </Select>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import TaggingComp from "./tagging.vue";
+import PathComp from "./path.vue";
+import MeasureComp from "./measure.vue";
+import GuideComp from "./guide.vue";
+import ViewComp from "./view.vue";
+import MonitorComp from "./monitor.vue";
+import MapComp from "./map.vue";
+import ModelComp from "./model.vue";
+import {
+  FuseModel,
+  Guide,
+  guides,
+  Measure,
+  measures,
+  Monitor,
+  monitors,
+  Path,
+  paths,
+  Tagging,
+  taggings,
+  View,
+  views,
+} from "@/store";
+import { Select, SelectOptGroup, SelectOption } from "ant-design-vue";
+import { computed, ref } from "vue";
+import { custom, params } from "@/env";
+import { debounce } from "@/utils";
+import { searchAddress, Address } from "@/store/map";
+import { fuseModels } from "@/store";
+
+const props = defineProps<{ popupClassName?: string; enable?: string }>();
+const emit = defineEmits<{ (e: "update:data", val: any): void }>();
+
+const addressItems = ref<Address[]>([]);
+const optionsAll = computed(() => [
+  {
+    key: "mode-",
+    name: "模型",
+    options: fuseModels.value,
+    getLabel: (tag: FuseModel) => tag.title,
+    comp: ModelComp,
+  },
+  {
+    key: "tagging-",
+    name: "标签",
+    options: taggings.value,
+    getLabel: (tag: Tagging) => tag.title,
+    comp: TaggingComp,
+  },
+  {
+    key: "path-",
+    name: "路径",
+    options: paths.value,
+    getLabel: (tag: Path) => tag.name,
+    comp: PathComp,
+  },
+  {
+    key: "measure-",
+    name: "测量",
+    options: measures.value,
+    getLabel: (tag: Measure) => tag.title,
+    comp: MeasureComp,
+  },
+  {
+    key: "guide-",
+    name: "导览",
+    options: guides.value,
+    getLabel: (tag: Guide) => tag.title,
+    comp: GuideComp,
+  },
+  // {
+  //   key: "view-",
+  //   name: "视图提取",
+  //   options: views.value,
+  //   getLabel: (tag: View) => tag.title,
+  //   comp: ViewComp,
+  // },
+  // {
+  //   key: "monitor-",
+  //   name: "监控",
+  //   options: monitors.value,
+  //   getLabel: (tag: Monitor) => tag.title,
+  //   comp: MonitorComp,
+  // },
+  {
+    key: "map-",
+    name: "地址",
+    options: [...addressItems.value],
+    getLabel: (tag: Address) => tag.address,
+    comp: MapComp,
+  },
+]);
+
+const options = computed(() =>
+  !props.enable
+    ? optionsAll.value
+    : optionsAll.value.filter((item) => item.key === props.enable)
+);
+
+const filterOption = (input: string, key: string) => {
+  if (key.indexOf("map-") === 0) return true;
+  const option = options.value.find((option) => key.indexOf(option.key) === 0);
+  if (!option) return false;
+  const id = key.substring(option.key.length);
+  const item = option.options.find((item) => item.id.toString() === id)!;
+  if (!item) return false;
+  return option.getLabel(item as any).indexOf(input) >= 0;
+};
+
+const filter = (input: string, option: any) => {
+  if (option.options) {
+    const fOptions = option.options.filter((option: any) =>
+      filterOption(input, option.value)
+    );
+    return fOptions.length === option.options.length;
+  } else {
+    return filterOption(input, option.value);
+  }
+};
+
+const value = ref();
+const fetchIng = ref(false);
+let timeout: any;
+let count = 0;
+const handleSearch = (val: string) => {
+  if (!val) {
+    addressItems.value = [];
+  }
+  const currentCount = ++count;
+  clearTimeout(timeout);
+  timeout = setTimeout(async () => {
+    const data = await searchAddress(val);
+    if (currentCount === count) {
+      addressItems.value = data;
+      fetchIng.value = false;
+    }
+  }, 500);
+};
+</script>
+<style lang="scss" scoped>
+#global-search {
+  position: absolute;
+  z-index: 99;
+  left: calc(var(--left-pano-left) + var(--left-pano-width) + 20px);
+
+  top: calc(var(--editor-head-height) + var(--header-top) + 20px);
+  // background: #000;
+  transition: all 0.3s ease;
+
+  &.nul {
+    left: 20px;
+  }
+}
+</style>
+
+<style lang="scss">
+#global-search {
+  .ant-select-selector {
+    background-color: var(--editor-toolbox-back);
+    backdrop-filter: blur(4px);
+
+    box-shadow: inset 0px 0px 0px 1px rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+
+    font-size: 14px;
+
+    padding-left: 30px;
+    &::before {
+      font-family: "iconfont" !important;
+      content: "\e64c";
+      position: absolute;
+      left: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      color: var(--colors-color);
+    }
+  }
+  input {
+    padding-left: 20px;
+  }
+  .anticon-search {
+    color: #fff;
+    display: none;
+  }
+  .ant-select-clear {
+    margin-right: 2px;
+  }
+  .ant-select-clear,
+  .ant-select-arrow {
+    font-size: 16px;
+  }
+  .ant-select-arrow {
+    display: none !important;
+  }
+}
+
+.global-search-menu {
+  background-color: var(--editor-toolbox-back);
+  backdrop-filter: blur(4px);
+  width: 340px;
+
+  .ant-empty-description {
+    color: rgba(255, 255, 255, 0.7);
+  }
+  .ant-empty-image * {
+    fill: rgba(255, 255, 255, 0.7);
+  }
+}
+
+.group-item-title {
+  font-weight: bold;
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+.last-item {
+  border-bottom: 1px solid rgba(var(--colors-primary-fill), 0.16);
+}
+</style>

+ 16 - 0
src/components/global-search/map.vue

@@ -0,0 +1,16 @@
+<template>
+  <p @click="fly">{{ data.address }}</p>
+</template>
+
+<script lang="ts" setup>
+import { flyLatLng } from "@/hook/use-fly";
+import { Address } from "@/store/map";
+
+const props = defineProps<{ data: Address }>();
+const emit = defineEmits<{ (e: "update:data", val: Address): void }>();
+
+const fly = () => {
+  flyLatLng(props.data.latlng);
+  emit("update:data", props.data);
+};
+</script>

+ 16 - 0
src/components/global-search/measure.vue

@@ -0,0 +1,16 @@
+<template>
+  <MeasureSign
+    :measure="data"
+    :edit="false"
+    search
+    @click="flyMeasure(data)"
+  />
+</template>
+
+<script lang="ts" setup>
+import { flyMeasure } from "@/hook/use-fly";
+import { Measure } from "@/store";
+import MeasureSign from "@/views/measure/sign.vue";
+
+defineProps<{ data: Measure }>();
+</script>

+ 11 - 0
src/components/global-search/model.vue

@@ -0,0 +1,11 @@
+<template>
+  <ModelSign :model="data" search @click="(mode: any) => flyModel(data, mode, true)" />
+</template>
+
+<script lang="ts" setup>
+import { flyModel } from "@/hook/use-fly";
+import { FuseModel } from "@/store";
+import ModelSign from "@/layout/model-list/sign.vue";
+
+defineProps<{ data: FuseModel }>();
+</script>

+ 10 - 0
src/components/global-search/monitor.vue

@@ -0,0 +1,10 @@
+<template>
+  <ViewSign :monitor="data" :edit="false" search />
+</template>
+
+<script lang="ts" setup>
+import { Monitor } from "@/store";
+import ViewSign from "@/views/tagging/monitor/sign.vue";
+
+defineProps<{ data: Monitor }>();
+</script>

+ 10 - 0
src/components/global-search/path.vue

@@ -0,0 +1,10 @@
+<template>
+  <PathSign :path="data" :edit="false" search />
+</template>
+
+<script lang="ts" setup>
+import { Path } from "@/store";
+import PathSign from "@/views/guide/path/sign.vue";
+
+defineProps<{ data: Path }>();
+</script>

+ 11 - 0
src/components/global-search/tagging.vue

@@ -0,0 +1,11 @@
+<template>
+  <TaggingSign :tagging="data" :edit="false" @select="flyTagging(data)" search />
+</template>
+
+<script lang="ts" setup>
+import { flyTagging } from "@/hook/use-fly";
+import { Tagging } from "@/store";
+import TaggingSign from "@/views/tagging/hot/sign.vue";
+
+defineProps<{ data: Tagging }>();
+</script>

+ 19 - 0
src/components/global-search/view.vue

@@ -0,0 +1,19 @@
+<template>
+  <ViewSign :view="data" :edit="false" search class="aitem" />
+</template>
+
+<script lang="ts" setup>
+import { View } from "@/store";
+import ViewSign from "@/views/view/sign.vue";
+
+defineProps<{ data: View }>();
+</script>
+
+<style>
+.aitem .content {
+  width: 100%;
+}
+.aitem .content .title {
+  flex: 1;
+}
+</style>

+ 23 - 20
src/components/list/index.vue

@@ -1,16 +1,19 @@
 <template>
   <ul class="list">
     <li class="header" v-if="title">
-      <h3>{{ title }}</h3>
-      <div class="action" v-if="$slots.action">
-        <slot name="action"></slot>
-      </div>
+      <template v-if="!$slots.header">
+        <h3>{{ title }}</h3>
+        <div class="action" v-if="$slots.action">
+          <slot name="action"></slot>
+        </div>
+      </template>
+      <slot name="header" v-else />
     </li>
     <ul class="content" v-if="showContent">
-      <li 
-        v-for="(item, i) in data" 
-        :key="rawKey ? item.raw[rawKey] : i" 
-        :class="{select: item.select}"
+      <li
+        v-for="(item, i) in data"
+        :key="rawKey ? item.raw[rawKey] : i"
+        :class="{ select: item.select }"
         @click="$emit('changeSelect', item)"
       >
         <div class="atom-content">
@@ -22,20 +25,22 @@
 </template>
 
 <script lang="ts" setup>
-type Item = Record<string, any> & {select?: boolean}
-type ListProps = { title?: string, rawKey?: string, data: Array<Item>, showContent?: boolean}
+type Item = Record<string, any> & { select?: boolean };
+type ListProps = {
+  title?: string;
+  rawKey?: string;
+  data: Array<Item>;
+  showContent?: boolean;
+};
 
-withDefaults(
-  defineProps<ListProps>(),
-  { showContent: true }
-)
+withDefaults(defineProps<ListProps>(), { showContent: true });
 
-defineEmits<{ (e: 'changeSelect', item: Item): void }>()
+defineEmits<{ (e: "changeSelect", item: Item): void }>();
 </script>
 
 <style lang="scss" scoped>
 .header {
-  border-bottom: 1px solid rgba(255,255,255,0.16);
+  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
   display: flex;
   justify-content: space-between;
   padding: 20px;
@@ -53,13 +58,12 @@ defineEmits<{ (e: 'changeSelect', item: Item): void }>()
     cursor: pointer;
 
     &.select {
-      background: rgba(0,200,175,0.1600);
+      background: rgba(0, 200, 175, 0.16);
     }
 
     .atom-content {
       padding: 20px 0;
-      border-bottom: 1px solid rgba(255,255,255,0.16);
-      
+      border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     }
   }
 }
@@ -68,4 +72,3 @@ defineEmits<{ (e: 'changeSelect', item: Item): void }>()
   list-style: none;
 }
 </style>
-

+ 135 - 55
src/components/materials/index.vue

@@ -17,7 +17,7 @@
           <span>( {{ rowSelection.selectedRowKeys.length }} )</span>
         </p>
         <div class="up-se">
-          <span class="upload fun-ctrls">
+          <span class="upload fun-ctrls" v-if="!readonly">
             <ui-input
               width="200px"
               class="input"
@@ -41,6 +41,15 @@
             allow-clear
             style="width: 244px"
           />
+          <Button
+            type="primary"
+            ghost
+            v-if="useType === 'trace_evidence'"
+            style="margin-left: 10px"
+            @click="foreceRefresh"
+          >
+            刷新
+          </Button>
         </div>
       </div>
 
@@ -62,6 +71,14 @@
                 </span>
               </div>
             </template>
+            <template v-if="column.key === 'caseTitle'">
+              <div class="name">
+                <span>
+                  {{ record.caseTitle || "-" }}
+                </span>
+              </div>
+            </template>
+
             <template v-if="column.key === 'size'">
               {{ getSizeStr(record.size) }}
             </template>
@@ -78,7 +95,7 @@
               <span class="group-str">{{ record.group }}</span>
             </template>
             <template v-else-if="column.key === 'action'">
-              <span>
+              <span v-if="record.useType !== 'animation'">
                 <a @click="delHandler(record.id)">{{ $t("sys.del") }}</a>
               </span>
             </template>
@@ -98,7 +115,7 @@
 
 <script lang="ts" setup>
 import { Modal, Input, Table, Empty, TableProps, Button } from "ant-design-vue";
-import { computed, reactive, ref, watch, watchEffect } from "vue";
+import { computed, reactive, readonly, ref, watch, watchEffect } from "vue";
 import { createLoadPack, debounceStack, getSizeStr } from "@/utils";
 import type { PagingResult } from "@/api";
 import {
@@ -109,6 +126,7 @@ import {
   Material,
   MaterialGroup,
   MaterialPageProps,
+  syncMaterialAll,
 } from "@/api/material";
 import Message from "bill/components/message/message.vue";
 import { Dialog } from "bill/expose-common";
@@ -120,6 +138,9 @@ const props = defineProps<{
   maxSize?: number;
   visible: boolean;
   count?: number;
+  readonly?: boolean;
+  groupIds: number[];
+  useType?: string;
   afterClose?: () => void;
 }>();
 
@@ -138,9 +159,11 @@ const Search = Input.Search;
 const params = reactive({
   pageNum: 1,
   pageSize: 10,
-  groupIds: [],
   formats: props.format,
+  useType: props.useType,
+  groupIds: props.groupIds,
 }) as MaterialPageProps;
+console.error(params);
 const origin = ref<PagingResult<Material[]>>({
   list: [],
   pageNum: 1,
@@ -148,26 +171,32 @@ const origin = ref<PagingResult<Material[]>>({
   total: 0,
 });
 const groups = ref<MaterialGroup[]>([]);
-const selectKeys = ref<Material["id"][]>([]);
+// const selectKeys = ref<Material["id"][]>([]);
 const allData: Record<Material["id"], Material> = reactive({});
 const btn = ref();
 watchEffect(() => {
   console.log("===>", btn.value);
 });
 
-const rowSelection: any = ref({
-  selectedRowKeys: selectKeys,
+const rowSelection = ref({
+  selectedRowKeys: [] as Material["id"][],
   onChange: (ids: number[]) => {
-    const otherPageKeys = selectKeys.value.filter(
+    console.log(ids);
+    const otherPageKeys = rowSelection.value.selectedRowKeys.filter(
       (key) => !origin.value.list.some((item) => key === item.id)
     );
     const newKeys = Array.from(new Set([...otherPageKeys, ...ids]));
     if (typeof props.count !== "number" || props.count >= newKeys.length) {
-      selectKeys.value = newKeys;
+      rowSelection.value.selectedRowKeys = newKeys;
     } else {
       Message.error(ui18n.t("material.selectCount", props));
     }
   },
+  hideSelectAll: true,
+  onSelectAll: (selected: boolean, selectedRows: Material[], changeRows: Material[]) => {
+    // 显式处理全选逻辑
+    console.log("全选状态变化:", selected);
+  },
   getCheckboxProps: (record: Material) => {
     return {
       disabled:
@@ -177,49 +206,94 @@ const rowSelection: any = ref({
     };
   },
 });
-const cloumns = computed(() => [
-  {
-    title: ui18n.t("material.tabs.name"),
-    dataIndex: "name",
-    key: "name",
-  },
-  {
-    width: "100px",
-    title: ui18n.t("material.tabs.format"),
-    dataIndex: "format",
-    key: "format",
-  },
-  {
-    width: "100px",
-    title: ui18n.t("material.tabs.size"),
-    dataIndex: "size",
-    key: "size",
-  },
-  {
-    width: "100px",
-    title: ui18n.t("material.tabs.status"),
-    dataIndex: "status",
-    key: "status",
-  },
-  {
-    width: "100px",
-    title: ui18n.t("material.tabs.group"),
-    dataIndex: "group",
-    key: "group",
-    filters: groups.value.map((g) => ({
-      text: g.name,
-      value: g.id,
-    })),
-  },
-  {
-    width: "100px",
-    title: ui18n.t("material.tabs.action"),
-    key: "action",
-  },
-]);
+const cloumns = computed(() => {
+  const groupIds = params.groupIds;
+  const cloumns =
+    props.useType === "trace_evidence"
+      ? [
+          {
+            title: ui18n.t("material.tabs.name"),
+            dataIndex: "name",
+            key: "name",
+          },
+          {
+            width: "200px",
+            title: "案件",
+            dataIndex: "caseTitle",
+            key: "caseTitle",
+          },
+          {
+            width: "100px",
+            title: ui18n.t("material.tabs.group"),
+            dataIndex: "group",
+            key: "group",
+            filteredValue: groupIds,
+            filters: props.readonly
+              ? undefined
+              : groups.value.map((g) => ({
+                  text: g.name,
+                  value: g.id,
+                })),
+            // filters: groups.value.map((g) => ({
+            //   text: g.name,
+            //   value: g.id,
+            // })),
+          },
+        ]
+      : [
+          {
+            title: ui18n.t("material.tabs.name"),
+            dataIndex: "name",
+            key: "name",
+          },
+          {
+            width: "100px",
+            title: ui18n.t("material.tabs.format"),
+            dataIndex: "format",
+            key: "format",
+          },
+          {
+            width: "100px",
+            title: ui18n.t("material.tabs.size"),
+            dataIndex: "size",
+            key: "size",
+          },
+          {
+            width: "100px",
+            title: ui18n.t("material.tabs.status"),
+            dataIndex: "status",
+            key: "status",
+          },
+          {
+            width: "100px",
+            title: ui18n.t("material.tabs.group"),
+            dataIndex: "group",
+            key: "group",
+            filteredValue: groupIds,
+            filters: props.readonly
+              ? undefined
+              : groups.value.map((g) => ({
+                  text: g.name,
+                  value: g.id,
+                })),
+            // filters: groups.value.map((g) => ({
+            //   text: g.name,
+            //   value: g.id,
+            // })),
+          },
+        ];
+  if (!props.readonly) {
+    cloumns.push({
+      width: "100px",
+      title: ui18n.t("material.tabs.action"),
+      key: "action",
+    } as any);
+  }
+  return cloumns;
+});
 
 const fetchData = createLoadPack(() =>
-  Promise.all([fetchMaterialGroups(), fetchMaterialPage(params)])
+  Promise.all([fetchMaterialGroups(props.useType), fetchMaterialPage(params)])
 );
 const refresh = debounceStack(
   fetchData,
@@ -235,6 +309,11 @@ const refresh = debounceStack(
   160
 );
 
+const foreceRefresh = async () => {
+  await syncMaterialAll();
+  await refresh();
+};
+
 const uploadHandler = async (file: File) => {
   await addMaterial(file);
   refresh();
@@ -249,10 +328,10 @@ const addHandler = async (file: File) => {
 const delHandler = async (id: Material["id"]) => {
   if (await Dialog.confirm(ui18n.t("sys.delConfrm"))) {
     await delMaterial(id);
-    const ndx = selectKeys.value.indexOf(id);
-    console.log(selectKeys.value, id);
+    const ndx = rowSelection.value.selectedRowKeys.indexOf(id);
+    console.log(rowSelection.value.selectedRowKeys, id);
     if (~ndx) {
-      selectKeys.value.splice(ndx, 1);
+      rowSelection.value.selectedRowKeys.splice(ndx, 1);
     }
     refresh();
   }
@@ -261,13 +340,13 @@ const delHandler = async (id: Material["id"]) => {
 const okHandler = () => {
   emit(
     "selectMaterials",
-    selectKeys.value.map((id) => allData[id])
+    rowSelection.value.selectedRowKeys.map((id) => allData[id])
   );
 };
 const handleTableChange: TableProps["onChange"] = (pag, filters) => {
   params.pageSize = pag.pageSize!;
   params.pageNum = pag.current!;
-  params.groupIds = filters.group as number[];
+  params.groupIds = (filters.group || props.groupIds) as number[];
 };
 </script>
 
@@ -283,6 +362,7 @@ const handleTableChange: TableProps["onChange"] = (pag, filters) => {
   max-height: 500px;
   overflow-y: auto;
 }
+
 .up-se {
   display: flex;
   align-items: center;

+ 4 - 0
src/components/materials/quisk.ts

@@ -6,8 +6,12 @@ import { reactive } from "vue";
 export const selectMaterials = async (props: {
   uploadFormat?: string[],
   format?: string[];
+  isSystem?: number,
   maxSize?: number;
   count?: number
+  readonly?: boolean;
+  groupIds?: number[]
+  useType?: string
 } = {}) => {
   return new Promise<Material[] | null>((resolve) => {
     const mprops = reactive({

+ 29 - 0
src/components/monitor-exit/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="exit fun-ctrl" @click="onClick">
+    <ui-icon type="close" />
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ onClick: () => void }>();
+</script>
+
+<style lang="scss" scoped>
+.exit {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  width: 50px;
+  height: 50px;
+  background-color: rgba(0, 0, 0, 0.5);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 99999;
+
+  .icon {
+    font-size: 18px;
+  }
+}
+</style>

+ 12 - 11
src/components/path/list.vue

@@ -1,16 +1,17 @@
 <template>
-  <template v-for="(path, index) in paths" :key="path.id">
-    <Sign
-      v-if="getPathIsShow(path)"
-      @delete="deletePath(path)"
-      @updatePoints="(data) => updatePosition(index, data)"
-      @updateLinePosition="(data) => updateLinePosition(index, data)"
+  <!-- <template v-for="(path, index) in paths" :key="path.id">
+    v-if="getPathIsShow(path)" -->
+  <Sign
+    v-for="(path, index) in paths"
+    @delete="deletePath(path)"
+    @updatePoints="(data) => updatePosition(index, data)"
+    @updateLinePosition="(data) => updateLinePosition(index, data)"
     @updateLineHeight="(data) => (paths[index].lineAltitudeAboveGround = data)"
-      :ref="(node: any) => nodeHandler(index, node)"
-      :path="path"
-      :key="path.id"
-    />
-  </template>
+    :ref="(node: any) => nodeHandler(index, node)"
+    :path="path"
+    :key="path.id"
+  />
+  <!-- </template> -->
 </template>
 
 <script lang="ts" setup>

+ 1 - 1
src/components/path/sign.vue

@@ -71,10 +71,10 @@ path.bus.on("changePoints", (points) => {
   emit("updatePoints", currentPoints);
 });
 path.bus.on("changeLineHeight", (val) => {
-  console.log("changeHeight");
   emit("updateLineHeight", val);
   clearTimeout(changLineTimeout);
 });
+
 watchEffect(() => {
   path.changeName(props.path.name);
 });

+ 36 - 0
src/components/right-menu/index.ts

@@ -0,0 +1,36 @@
+import { mount } from "@/utils";
+import { Pos } from "../drawing/dec";
+import RMenuComp from "./index.vue";
+import { reactive } from "vue";
+
+export type RMenu = {
+  label: string;
+  icon: string;
+  handler: () => void;
+};
+
+export const useRMenus = (
+  pixel: Pos,
+  menus: RMenu[],
+  dom = document.querySelector("#app")!
+) => {
+  let unMountRaw = mount(
+    dom as HTMLDivElement,
+    RMenuComp,
+    reactive({
+      pixel,
+      menus,
+      onClose() {
+        unMount()
+      }
+    })
+  );
+  let isUn = false
+  const unMount = () => {
+    if (!isUn) {
+      unMountRaw()
+      isUn = true
+    }
+  }
+  return unMount
+};

+ 44 - 0
src/components/right-menu/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <Dropdown v-model:open="open" overlayClassName="down-item">
+    <span class="proce" :style="{ left: pixel.x + 'px', top: pixel.y + 'px' }"></span>
+    <template #overlay>
+      <Menu>
+        <MenuItem @click="clickHandler(menu)" v-for="menu in menus">
+          <template #icon><ui-icon :type="menu.icon" /></template>
+          {{ menu.label }}
+        </MenuItem>
+      </Menu>
+    </template>
+  </Dropdown>
+</template>
+
+<script lang="ts" setup>
+import { Dropdown, Menu, MenuItem } from "ant-design-vue";
+import { RMenu } from ".";
+import { Pos } from "../drawing/dec";
+import { ref, watchEffect } from "vue";
+
+const props = defineProps<{ menus: RMenu[]; pixel: Pos; onClose: () => void }>();
+
+const open = ref(true);
+watchEffect(() => {
+  if (!open.value) {
+    props.onClose();
+  }
+});
+
+const clickHandler = (menu: RMenu) => {
+  menu.handler();
+  props.onClose();
+};
+</script>
+
+<style lang="scss" scoped>
+.proce {
+  position: absolute;
+  width: 5px;
+  height: 5px;
+  transform: translate(-50%, -50%);
+  z-index: 99;
+}
+</style>

+ 70 - 0
src/components/subtitle/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <div
+    :style="{
+      visibility: show ? 'visible' : 'hidden',
+    }"
+    class="subtitle"
+    ref="dom"
+  >
+    <div :style="{ background: data.background }" v-html="data.content" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { AnimationModelSubtitle } from "@/api";
+import { Pos, Size } from "../drawing/dec";
+import { ref, watch, watchEffect } from "vue";
+
+const props = defineProps<{
+  data: AnimationModelSubtitle;
+  pixel: Pos;
+  show: boolean;
+  sizeChang: (csize: Size) => void;
+}>();
+
+// watchEffect(() => {
+
+//   if (!dom.value) return;
+//   dom.value.style.transform = `translate(${props.pixel.x}px, ${props.pixel.y}px)`;
+// });
+const dom = ref<HTMLDivElement>();
+watch(
+  () => props.data.content,
+  () => {
+    watch(
+      dom,
+      (dom, _, onCleanup) => {
+        if (!dom) return;
+        const timeout = setTimeout(() => {
+          props.sizeChang({ width: dom.offsetWidth, height: dom.offsetHeight });
+        });
+        onCleanup(() => clearTimeout(timeout));
+      },
+      { immediate: true }
+    );
+  }
+);
+</script>
+
+<style lang="scss" scoped>
+.subtitle {
+  position: absolute;
+  z-index: 1;
+  transition: transform 0.3s linear;
+  will-change: transform;
+  left: 0;
+  top: 0;
+  text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
+  pointer-events: none;
+
+  > div {
+    max-width: 280px;
+    border-radius: 4px;
+    font-size: 14px;
+    padding: 10px 20px;
+    color: #ffffff;
+    line-height: 16px;
+    word-break: break-all;
+  }
+}
+</style>

+ 3 - 1
src/components/tagging/list.vue

@@ -4,6 +4,7 @@
       v-if="getTaggingPositionIsShow(pos)"
       @delete="deletePosition(pos)"
       @changePosition="(data: any) => updatePosition(index, data)"
+      @change-line-height="(data: number) => positions[index].lineHeight = data"
       :tagging="tagging"
       :ref="(node: any) => nodes[index] = ({ node, id: pos.id })"
       :scene-pos="pos"
@@ -36,11 +37,12 @@ const deletePosition = (pos: TaggingPosition) => {
 };
 const updatePosition = (
   ndx: number,
-  data: { position: SceneLocalPos; modelId: string; normal: SceneLocalPos }
+  data: { position: SceneLocalPos; modelId: string; normal: SceneLocalPos; pose: any }
 ) => {
   positions.value[ndx].localPos = data.position;
   positions.value[ndx].modelId = data.modelId;
   positions.value[ndx].normal = data.normal;
+  positions.value[ndx].pose = data.pose;
 };
 
 defineExpose(nodes);

+ 47 - 21
src/components/tagging/sign-new.vue

@@ -26,22 +26,18 @@
           />
         </h2>
         <div class="content">
-          <div class="p">
-            <span v-if="defStyleType.id !== taggingStyle?.typeId">
-              {{ $t("tagging.tabs.typeId") }}:
-            </span>
+          <template v-if="defStyleType.id !== taggingStyle?.typeId && tagging.part">
+            <p><span>遗留部位:</span>{{ tagging.part }}</p>
+          </template>
+          <div class="p" v-if="tagging.desc">
+            <span v-if="defStyleType.id !== taggingStyle?.typeId"> 特征描述: </span>
             <div v-html="tagging.desc"></div>
           </div>
           <template v-if="defStyleType.id !== taggingStyle?.typeId">
-            <p>
-              <span>{{ $t("tagging.tabs.part") }}:</span>{{ tagging.part }}
-            </p>
-            <p>
-              <span>{{ $t("tagging.tabs.method") }}:</span>{{ tagging.method }}
-            </p>
-            <p>
-              <span>{{ $t("tagging.tabs.principal") }}:</span>{{ tagging.principal }}
-            </p>
+            <p v-if="tagging.method"><span>提取方法:</span>{{ tagging.method }}</p>
+            <p v-if="tagging.tqTime"><span>提取时间:</span>{{ tagging.tqTime }}</p>
+            <p v-if="tagging.principal"><span>提取人:</span>{{ tagging.principal }}</p>
+            <p v-if="tagging.tqStatus"><span>委托状态:</span>{{ tagging.tqStatus }}</p>
           </template>
         </div>
         <Images
@@ -71,7 +67,7 @@
 <script lang="ts" setup>
 import { computed, markRaw, onUnmounted, ref, watch, watchEffect } from "vue";
 import UIBubble from "bill/components/bubble/index.vue";
-import Images from "@/views/tagging/images.vue";
+import Images from "@/views/tagging/hot/images.vue";
 import Preview from "../static-preview/index.vue";
 import { getTaggingStyle } from "@/store";
 import { getFileUrl } from "@/utils";
@@ -88,9 +84,10 @@ export type SignProps = { tagging: Tagging; scenePos: TaggingPosition; show?: bo
 const props = defineProps<SignProps>();
 const emit = defineEmits<{
   (e: "delete"): void;
+  (e: "changeLineHeight", val: number): void;
   (
     e: "changePosition",
-    val: { position: SceneLocalPos; modelId: string; normal: SceneLocalPos }
+    val: { position: SceneLocalPos; modelId: string; normal: SceneLocalPos; pose?: any }
   ): void;
 }>();
 
@@ -107,6 +104,7 @@ const queryItems = computed(() =>
     url: getResource(getFileUrl(image)),
   }))
 );
+console.log(props.tagging.styleId);
 const taggingStyle = computed(() => getTaggingStyle(props.tagging.styleId));
 const tag = markRaw(
   sdk.createTagging({
@@ -131,10 +129,6 @@ const changePos = () => {
 watch(taggingStyle, (icon) => icon && tag.changeImage(getFileUrl(icon.icon)));
 watchEffect(() => tag.changeMat(props.scenePos.mat));
 watchEffect(() => tag.changeFontSize(props.scenePos.fontSize));
-watchEffect(() => {
-  tag.changeLineHeight(props.scenePos.lineHeight);
-  changePos();
-});
 watchEffect(() => tag.changeTitle(props.tagging.title));
 watchEffect(() => tag.visibilityTitle(props.tagging.show3dTitle));
 watchEffect(() => {
@@ -149,7 +143,6 @@ const getPosition = () => ({
 let currentPosition = getPosition();
 let changeTimeout: any;
 tag.bus.on("changePosition", (data) => {
-  console.error(data);
   clearTimeout(changeTimeout);
   emit(
     "changePosition",
@@ -161,6 +154,24 @@ tag.bus.on("changePosition", (data) => {
   );
   changePos();
 });
+tag.bus.on("scaleChanged", () => changePos());
+
+tag.bus.on("changePosition", (data) => {
+  clearTimeout(changeTimeout);
+  currentPosition = {
+    position: { ...data.pos },
+    normal: { ...data.normal },
+    modelId: data.modelId,
+  };
+  emit("changePosition", {
+    ...currentPosition,
+    pose: sdk.getPose({ modelId: data.modelId, isFlyToTag: true }),
+  });
+  changePos();
+});
+tag.bus.on("changeLineHeight", (lineHeight) => {
+  emit("changeLineHeight", lineHeight);
+});
 
 watch(getPosition, (p) => {
   changeTimeout = setTimeout(() => {
@@ -171,6 +182,17 @@ watch(getPosition, (p) => {
   }, 16);
 });
 
+watch(
+  () => props.scenePos.lineHeight,
+  () => {
+    changeTimeout = setTimeout(() => {
+      tag.changeLineHeight(props.scenePos.lineHeight);
+      changePos();
+    }, 16);
+  },
+  { immediate: true }
+);
+
 const [toCameraDistance] = useCameraChange(() => {
   return tag.getCameraDisSquared && tag.getCameraDisSquared();
 });
@@ -224,8 +246,12 @@ const iconClickHandler = () => {
   }
 };
 
+console.log("标签 创建", props.tagging.id);
 onUnmounted(() => {
-  tag.destory();
+  tag.destroy();
+  clearTimeout(timeout);
+  clearTimeout(changeTimeout);
+  console.error("标签 销毁", props.tagging.id);
 });
 
 defineExpose(tag);

+ 90 - 0
src/components/view-setting/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div v-if="custom.showViewSetting && currentModel === fuseModel && !params.pure">
+    <Dropdown placement="top">
+      <div class="strengthen show-setting">
+        <span>显示设置</span>
+        <DownOutlined />
+      </div>
+      <template #overlay>
+        <Menu>
+          <menu-item v-for="option in showOptions">
+            <ui-input
+              @click.stop
+              type="checkbox"
+              :modelValue="option.stack.value"
+              @update:modelValue="(s: boolean) => option.stack.value = s"
+              :label="option.text"
+            />
+          </menu-item>
+        </Menu>
+      </template>
+    </Dropdown>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Dropdown, Menu, MenuItem } from "ant-design-vue";
+import {
+  custom,
+  params,
+  showMeasuresStack,
+  showMonitorsStack,
+  showPathsStack,
+  showTaggingsStack,
+} from "@/env";
+import { DownOutlined } from "@ant-design/icons-vue";
+import { computed, watch, watchEffect } from "vue";
+import { selectPaths } from "@/store";
+import { currentModel, fuseModel } from "@/model";
+
+const props = defineProps<{ value?: Record<string, boolean> }>();
+const emit = defineEmits<{ (e: "update:value", v: Record<string, boolean>): void }>();
+
+const showOptions = [
+  { key: "showTagging", text: "标签", stack: showTaggingsStack.current.value },
+  { key: "showMeasure", text: "测量", stack: showMeasuresStack.current.value },
+  {
+    key: "showPath",
+    text: "路径",
+    stack: computed({
+      get: () => {
+        return selectPaths.all.value || selectPaths.selects.value.length > 0;
+      },
+      set: (val: boolean) => {
+        selectPaths.all.value = val;
+        console.log(selectPaths.selects.value);
+      },
+    }),
+  },
+];
+watch(
+  () => props.value,
+  () => {
+    if (!props.value) return;
+    showOptions.forEach((item) => {
+      item.stack.value = props.value![item.key];
+    });
+  },
+  { immediate: true }
+);
+
+watchEffect(() => {
+  emit(
+    "update:value",
+    Object.fromEntries(showOptions.map((item) => [item.key, item.stack.value] as const))
+  );
+});
+</script>
+
+<style lang="scss" scoped>
+.show-setting {
+  width: 160px;
+  height: 34px;
+  background: rgba(27, 27, 28, 0.9);
+  border-radius: 4px;
+  display: flex;
+  padding: 8px;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 32 - 6
src/env/index.ts

@@ -1,5 +1,5 @@
 import { stackFactory, flatStacksValue, strToParams } from "@/utils";
-import { reactive, ref } from "vue";
+import { reactive, ref, watch } from "vue";
 
 import type { FuseModel, Path, TaggingPosition, View } from "@/store";
 import { lang, langKey } from "@/lang";
@@ -8,7 +8,9 @@ export const showToolbarStack = stackFactory(ref<boolean>(false));
 export const showHeadBarStack = stackFactory(ref<boolean>(true));
 export const showRightPanoStack = stackFactory(ref<boolean>(true));
 export const showLeftPanoStack = stackFactory(ref<boolean>(false));
+export const moundLeftPanoStack = stackFactory(ref<boolean>(true));
 export const showLeftCtrlPanoStack = stackFactory(ref<boolean>(true));
+export const showAMsStack = stackFactory(ref<boolean>(false));
 export const showModeStack = stackFactory(ref<"pano" | "fuse">("fuse"));
 export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true));
 export const showBottomBarStack = stackFactory(ref<boolean>(false), true);
@@ -17,6 +19,12 @@ export const showTaggingsStack = stackFactory(ref<boolean>(true));
 export const showPathsStack = stackFactory(ref<boolean>(true));
 export const showPathStack = stackFactory(ref<Path["id"]>());
 export const showMeasuresStack = stackFactory(ref<boolean>(true));
+export const showMonitorsStack = stackFactory(ref<boolean>(true));
+export const showSearchStack = stackFactory(ref<boolean>(true));
+export const showFullStack = stackFactory(ref(false));
+export const showModeTabStack = stackFactory(ref<boolean>(true));
+
+export const showViewSettingStack = stackFactory(ref<boolean>(true));
 export const currentModelStack = stackFactory(ref<FuseModel | null>(null));
 export const showModelsMapStack = stackFactory(
   ref<WeakMap<FuseModel, boolean>>(new Map()),
@@ -33,9 +41,12 @@ export const custom = flatStacksValue({
   showToolbar: showToolbarStack,
   showRightPano: showRightPanoStack,
   showLeftPano: showLeftPanoStack,
+  showModeTab: showModeTabStack,
+  moundLeftPano: moundLeftPanoStack,
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
   showTaggings: showTaggingsStack,
+  showMonitors: showMonitorsStack,
   showPaths: showPathsStack,
   showPath: showPathStack,
   showMeasures: showMeasuresStack,
@@ -46,10 +57,15 @@ export const custom = flatStacksValue({
   showBottomBar: showBottomBarStack,
   bottomBarHeight: bottomBarHeightStack,
   showHeadBar: showHeadBarStack,
+  showAMs: showAMsStack,
   currentView: currentViewStack,
   showMode: showModeStack,
+  showSearch: showSearchStack,
+  showViewSetting: showViewSettingStack,
+  full: showFullStack,
 });
 
+export const paramsRaw = strToParams(location.search) as unknown as Params
 export const params = reactive(
   strToParams(location.search)
 ) as unknown as Params;
@@ -60,8 +76,6 @@ params.single = Boolean(Number(params.single));
 export type Params = {
   caseId: number;
   ga?: string
-  baseURL?: string;
-  modelId?: string;
   laserRoot: string;
   swssUrl: string;
   swkkUrl: string;
@@ -70,22 +84,27 @@ export type Params = {
   root: string;
   laserOSSRoot: string;
   service: string;
+  ip: string;
+  serviceUrl?: string;
+  baseURL?: string;
+  pure?: boolean;
+  modelId?: string;
+  mapKey?: string;
+  mapPlatform?: string;
   fileUrl?: string;
   sign?: string;
-  ip: string;
   type?: string;
-  serviceUrl?: string;
   testMap?: boolean;
   title?: string;
   m?: string;
   share?: boolean;
   single?: boolean;
   static: string;
-  token?: string;
 
   servicePort: string;
   swkkPort: string;
   laserServicePort: string;
+  token?: string;
 };
 
 export const baseURL = params.baseURL ? params.baseURL : "";
@@ -182,3 +201,10 @@ export const getResources = (uri: string) => {
   url.pathname = basePath + url.pathname;
   return url.href;
 };
+watch(
+  () => params.pure || false,
+  (pure, _, onCleanup) => {
+    onCleanup(showFullStack.push(ref(pure)));
+  },
+  { immediate: true }
+);

+ 66 - 0
src/hook/ids.ts

@@ -0,0 +1,66 @@
+import { debounce, diffArrayChange } from "@/utils";
+import { computed, ref, Ref, watch } from "vue";
+
+export const useSelects = <T extends { id: any }>(items: Ref<T[]>, test = false) => {
+  const selects = ref<T[]>([]);
+
+  const updateSelect = (item: T, select: boolean) => {
+    const ndx = selects.value.findIndex((s) => s.id === item.id);
+    test && console.error('updateSelect', item.id, select)
+    if (select) {
+      if (~ndx) {
+        selects.value[ndx] = item as any
+      } else {
+        selects.value.push(item as any);
+      } 
+    } else {
+      ~ndx && selects.value.splice(ndx, 1);
+    }
+  };
+  const updateSelectId = (id: any, select: boolean) => {
+    const item = items.value.find((s) => s.id === id);
+    if (item) {
+      updateSelect(item, select);
+    } else {
+      const ndx = selects.value.findIndex((s) => s.id === id);
+      if (~ndx) {
+        selects.value.splice(ndx, 1);
+      } 
+    }
+  };
+
+  const oldItems: T[] = [];
+  watch(
+    items,
+    (items) => {
+      const { added, deleted } = diffArrayChange(
+        items.map((item) => item.id),
+        oldItems.map((item) => item.id)
+      );
+      test && console.error('added', added)
+      test && console.error('deleted', deleted)
+      
+      added.forEach((id) => updateSelectId(id, true));
+      deleted.forEach((id) => updateSelectId(id, false));
+      oldItems.length = 0;
+      oldItems.push(...items);
+    },
+    { deep: true, flush: 'post', immediate: true }
+  );
+
+  return {
+    selects,
+    unSelects: computed(() => items.value.filter(item => !selects.value.find(s => s.id === item.id))),
+    all: computed({
+      get: () => items.value.length === selects.value.length,
+      set: (select: boolean) => {
+        test && console.error('select', select)
+        items.value.forEach(item => updateSelect(item, select))
+      } 
+    }),
+    include: (id: string) => selects.value.some(item => item.id === id),
+    updateSelect,
+    updateSelectId,
+  };
+};
+

+ 149 - 0
src/hook/use-fly.ts

@@ -0,0 +1,149 @@
+import { TaggingPosition } from "@/api";
+import { custom, showTaggingPositionsStack } from "@/env";
+import { currentModel, fuseModel, loadModel } from "@/model";
+import { sdk, getTaggingPosNode, setPose, getSceneMeasure, playSceneGuide, pauseSceneGuide, activeModel } from "@/sdk";
+import { getPathNode, pauseScenePath, playScenePath } from "@/sdk/association/path";
+import {
+  getFuseModel,
+  getFuseModelShowVariable,
+  getTaggingPositions,
+  Measure,
+  Tagging,
+  Path,
+  View,
+  viewToModelType,
+  Guide,
+  getGuidePaths,
+  FuseModel,
+} from "@/store";
+import { nextTick, ref } from "vue";
+
+let stopFly: (() => void) | null = null;
+export const flyTagging = (tagging: Tagging, callback?: () => void) => {
+  stopFly && stopFly();
+  const positions = getTaggingPositions(tagging);
+  console.error('fly tagging')
+  let timeout: any
+  let isStop = false;
+  const flyIndex = (i: number) => {
+    if (isStop || i >= positions.length) {
+      callback && nextTick(callback);
+      return;
+    }
+    const position = positions[i];
+    const model = getFuseModel(position.modelId);
+    if (!model || !getFuseModelShowVariable(model).value) {
+      flyIndex(i + 1);
+      return;
+    }
+
+    const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])));
+    flyTaggingPosition(position);
+
+    timeout = setTimeout(() => {
+      pop();
+      flyIndex(i + 1);
+    }, 2000);
+  };
+  flyIndex(0);
+  stopFly = () => {
+    clearTimeout(timeout)
+    isStop = true;
+    stopFly = null;
+  };
+  return stopFly;
+};
+
+export const flyTaggingPosition = (position: TaggingPosition) => {
+  if (position.pose) {
+    setPose({
+      modelId: position.modelId,
+      dur: 300,
+      // distance: 3,
+      maxDis: 15,
+      isFlyToTag: true,
+      focusPos: getTaggingPosNode(position)!.getImageCenter(),
+      ...position.pose
+    } as any, sdk, false);
+  } else {
+    sdk.comeTo({
+      focusPos: getTaggingPosNode(position)!.getImageCenter(),
+      modelId: position.modelId,
+      dur: 300,
+      // distance: 3,
+      maxDis: 15,
+      isFlyToTag: true,
+    } as any);
+  }
+};
+
+export const flyMeasure = (data: Measure) => {
+  stopFly && stopFly();
+  getSceneMeasure(data)?.fly();
+};
+
+export const flyPath = (path: Path) => {
+  stopFly && stopFly();
+  getPathNode(path.id)?.fly();
+  getPathNode(path.id)?.focus(true);
+};
+
+export const flyView = (view: View) => {
+  stopFly && stopFly();
+  let isStop = false;
+  stopFly = () => {
+    isStop = true;
+    stopFly = null
+  };
+  const modelType = viewToModelType(view);
+  loadModel(modelType).then((sdk) => {
+    if (!isStop) {
+      custom.currentView = view;
+      sdk.setView(view.flyData);
+    }
+  });
+  return stopFly;
+};
+
+export const flyPlayGuide = (guide: Guide) => {
+  console.error('flyPlay')
+  stopFly && stopFly()
+  stopFly = () => {
+    stopFly = null
+    pauseSceneGuide()
+  }
+  playSceneGuide(getGuidePaths(guide), undefined, true, guide)
+  return stopFly
+}
+
+export const flyPlayPath = (path: Path) => {
+  stopFly && stopFly()
+  stopFly = () => {
+    stopFly = null
+    pauseScenePath()
+  }
+  playScenePath(path, true);
+  return stopFly
+}
+
+export const flyLatLng = (latlng: number[]) => {
+  stopFly && stopFly();
+  sdk.comeToByLatLng(latlng)
+}
+
+export const flyModel = (model: FuseModel, mode: "pano" | "fuse", f = false) => {
+  stopFly && stopFly();
+
+  if (getFuseModelShowVariable(model).value) {
+    if (custom.currentModel === model && mode === custom.showMode) {
+      if (!f) return;
+      activeModel({ showMode: "fuse", active: undefined, fore: f });
+    } else {
+      activeModel({ showMode: mode, active: model, fore: f });
+    }
+  }
+
+  if (currentModel.value !== fuseModel) {
+    loadModel(fuseModel);
+  }
+};

+ 0 - 0
src/hook/use-pixel.ts


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio